Explorer: introduce Coingecko price cards on mint pages (#18653)

* feat: add coingecko prices, market caps, volume to token account

* feat: add updated time to price widget

* feat: add loading state to coingecko and break front page price data into widgets

* fix: prevent flicker on refresh
This commit is contained in:
Josh
2021-07-20 09:43:17 -07:00
committed by GitHub
parent c8442fd476
commit 8549c19f1a
5 changed files with 254 additions and 186 deletions

View File

@ -11,7 +11,7 @@ import { TableCardBody } from "components/common/TableCardBody";
import { Address } from "components/common/Address";
import { UnknownAccountCard } from "./UnknownAccountCard";
import { Cluster, useCluster } from "providers/cluster";
import { normalizeTokenAmount } from "utils";
import { abbreviatedNumber, normalizeTokenAmount } from "utils";
import { addressLabel } from "utils/tx";
import { reportError } from "utils/sentry";
import { useTokenRegistry } from "providers/mints/token-registry";
@ -19,6 +19,7 @@ import { BigNumber } from "bignumber.js";
import { Copyable } from "components/common/Copyable";
import { CoingeckoStatus, useCoinGecko } from "utils/coingecko";
import { displayTimestampWithoutDate } from "utils/date";
import { LoadingCard } from "components/common/LoadingCard";
const getEthAddress = (link?: string) => {
let address = "";
@ -95,129 +96,177 @@ function MintAccountCard({
}
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
{tokenInfo ? "Overview" : "Token Mint"}
</h3>
<button className="btn btn-white btn-sm" onClick={refresh}>
<span className="fe fe-refresh-cw mr-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-right">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>
{info.mintAuthority === null ? "Fixed Supply" : "Current Supply"}
</td>
<td className="text-lg-right">
{normalizeTokenAmount(info.supply, info.decimals).toLocaleString(
"en-US",
{
minimumFractionDigits: info.decimals,
}
)}
</td>
</tr>
{tokenPriceInfo?.price && (
<tr>
<td>Current Price</td>
<td className="text-lg-right">
$
{tokenPriceInfo.price.toLocaleString("en-US", {
minimumFractionDigits: 2,
})}
</td>
</tr>
<>
{tokenInfo?.extensions?.coingeckoId &&
coinInfo?.status === CoingeckoStatus.Loading && (
<LoadingCard message="Loading token price data" />
)}
{tokenInfo?.extensions?.website && (
<tr>
<td>Website</td>
<td className="text-lg-right">
<a
rel="noopener noreferrer"
target="_blank"
href={tokenInfo.extensions.website}
>
{tokenInfo.extensions.website}
<span className="fe fe-external-link ml-2"></span>
</a>
</td>
</tr>
)}
{info.mintAuthority && (
<tr>
<td>Mint Authority</td>
<td className="text-lg-right">
<Address pubkey={info.mintAuthority} alignRight link />
</td>
</tr>
)}
{info.freezeAuthority && (
<tr>
<td>Freeze Authority</td>
<td className="text-lg-right">
<Address pubkey={info.freezeAuthority} alignRight link />
</td>
</tr>
)}
<tr>
<td>Decimals</td>
<td className="text-lg-right">{info.decimals}</td>
</tr>
{!info.isInitialized && (
<tr>
<td>Status</td>
<td className="text-lg-right">Uninitialized</td>
</tr>
)}
{tokenInfo?.extensions?.bridgeContract && bridgeContractAddress && (
<tr>
<td>Bridge Contract</td>
<td className="text-lg-right">
<Copyable text={bridgeContractAddress}>
<a
href={tokenInfo.extensions.bridgeContract}
target="_blank"
rel="noreferrer"
>
{bridgeContractAddress}
</a>
</Copyable>
</td>
</tr>
)}
{tokenInfo?.extensions?.assetContract && assetContractAddress && (
<tr>
<td>Bridged Asset Contract</td>
<td className="text-lg-right">
<Copyable text={assetContractAddress}>
<a
href={tokenInfo.extensions.bridgeContract}
target="_blank"
rel="noreferrer"
>
{assetContractAddress}
</a>
</Copyable>
</td>
</tr>
)}
</TableCardBody>
{tokenPriceInfo && (
<p className="updated-time text-muted mr-4">
Price updated at{" "}
{displayTimestampWithoutDate(tokenPriceInfo.last_updated.getTime())}
</p>
<div className="row">
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<h4>
Price{" "}
<span className="ml-2 badge badge-primary rank">
Rank #{tokenPriceInfo.market_cap_rank}
</span>
</h4>
<h1 className="mb-0">
${tokenPriceInfo.price.toFixed(2)}{" "}
{tokenPriceInfo.price_change_percentage_24h > 0 && (
<small className="change-positive">
&uarr;{" "}
{tokenPriceInfo.price_change_percentage_24h.toFixed(2)}%
</small>
)}
{tokenPriceInfo.price_change_percentage_24h < 0 && (
<small className="change-negative">
&darr;{" "}
{tokenPriceInfo.price_change_percentage_24h.toFixed(2)}%
</small>
)}
{tokenPriceInfo.price_change_percentage_24h === 0 && (
<small>0%</small>
)}
</h1>
</div>
</div>
</div>
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<h4>24 Hour Volume</h4>
<h1 className="mb-0">
${abbreviatedNumber(tokenPriceInfo.volume_24)}
</h1>
</div>
</div>
</div>
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<h4>Market Cap</h4>
<h1 className="mb-0">
${abbreviatedNumber(tokenPriceInfo.market_cap)}
</h1>
<p className="updated-time text-muted">
Updated at{" "}
{displayTimestampWithoutDate(
tokenPriceInfo.last_updated.getTime()
)}
</p>
</div>
</div>
</div>
</div>
)}
</div>
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
{tokenInfo ? "Overview" : "Token Mint"}
</h3>
<button className="btn btn-white btn-sm" onClick={refresh}>
<span className="fe fe-refresh-cw mr-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-right">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>
{info.mintAuthority === null ? "Fixed Supply" : "Current Supply"}
</td>
<td className="text-lg-right">
{normalizeTokenAmount(info.supply, info.decimals).toLocaleString(
"en-US",
{
minimumFractionDigits: info.decimals,
}
)}
</td>
</tr>
{tokenInfo?.extensions?.website && (
<tr>
<td>Website</td>
<td className="text-lg-right">
<a
rel="noopener noreferrer"
target="_blank"
href={tokenInfo.extensions.website}
>
{tokenInfo.extensions.website}
<span className="fe fe-external-link ml-2"></span>
</a>
</td>
</tr>
)}
{info.mintAuthority && (
<tr>
<td>Mint Authority</td>
<td className="text-lg-right">
<Address pubkey={info.mintAuthority} alignRight link />
</td>
</tr>
)}
{info.freezeAuthority && (
<tr>
<td>Freeze Authority</td>
<td className="text-lg-right">
<Address pubkey={info.freezeAuthority} alignRight link />
</td>
</tr>
)}
<tr>
<td>Decimals</td>
<td className="text-lg-right">{info.decimals}</td>
</tr>
{!info.isInitialized && (
<tr>
<td>Status</td>
<td className="text-lg-right">Uninitialized</td>
</tr>
)}
{tokenInfo?.extensions?.bridgeContract && bridgeContractAddress && (
<tr>
<td>Bridge Contract</td>
<td className="text-lg-right">
<Copyable text={bridgeContractAddress}>
<a
href={tokenInfo.extensions.bridgeContract}
target="_blank"
rel="noreferrer"
>
{bridgeContractAddress}
</a>
</Copyable>
</td>
</tr>
)}
{tokenInfo?.extensions?.assetContract && assetContractAddress && (
<tr>
<td>Bridged Asset Contract</td>
<td className="text-lg-right">
<Copyable text={assetContractAddress}>
<a
href={tokenInfo.extensions.bridgeContract}
target="_blank"
rel="noreferrer"
>
{assetContractAddress}
</a>
</Copyable>
</td>
</tr>
)}
</TableCardBody>
</div>
</>
);
}

View File

@ -7,7 +7,7 @@ import {
usePerformanceInfo,
useStatsProvider,
} from "providers/stats/solanaClusterStats";
import { lamportsToSol, slotsToHumanString } from "utils";
import { abbreviatedNumber, lamportsToSol, slotsToHumanString } from "utils";
import { ClusterStatus, useCluster } from "providers/cluster";
import { TpsCard } from "components/TpsCard";
import { displayTimestampWithoutDate, displayTimestampUtc } from "utils/date";
@ -82,7 +82,7 @@ function StakingComponent() {
}
if (supply === Status.Idle || supply === Status.Connecting || !coinInfo) {
return <LoadingCard />;
return <LoadingCard message="Loading supply and price data" />;
} else if (typeof supply === "string") {
return <ErrorCard text={supply} retry={fetchData} />;
}
@ -105,10 +105,10 @@ function StakingComponent() {
}
return (
<div className="card staking-card">
<div className="card-body">
<div className="d-flex flex-md-row flex-column">
<div className="p-2 flex-fill">
<div className="row staking-card">
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<h4>Circulating Supply</h4>
<h1>
<em>{displayLamports(supply.circulating)}</em> /{" "}
@ -118,8 +118,11 @@ function StakingComponent() {
<em>{circulatingPercentage}%</em> is circulating
</h5>
</div>
<hr className="hidden-sm-up" />
<div className="p-2 flex-fill">
</div>
</div>
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
<h4>Active Stake</h4>
{activeStake && (
<h1>
@ -133,66 +136,65 @@ function StakingComponent() {
</h5>
)}
</div>
<hr className="hidden-sm-up" />
{solanaInfo && (
<div className="p-2 flex-fill">
<h4>
Price{" "}
<span className="ml-2 badge badge-primary rank">
Rank #{solanaInfo.market_cap_rank}
</span>
</h4>
<h1>
<em>${solanaInfo.price.toFixed(2)}</em>{" "}
{solanaInfo.price_change_percentage_24h > 0 && (
<small className="change-positive">
&uarr; {solanaInfo.price_change_percentage_24h.toFixed(2)}%
</small>
)}
{solanaInfo.price_change_percentage_24h < 0 && (
<small className="change-negative">
&darr; {solanaInfo.price_change_percentage_24h.toFixed(2)}%
</small>
)}
{solanaInfo.price_change_percentage_24h === 0 && (
<small>0%</small>
)}
</h1>
<h5>
24h Vol: <em>${abbreviatedNumber(solanaInfo.volume_24)}</em>{" "}
MCap: <em>${abbreviatedNumber(solanaInfo.market_cap)}</em>
</h5>
</div>
)}
{coinInfo.status === CoingeckoStatus.FetchFailed && (
<div className="p-2 flex-fill">
<h4>Price</h4>
<h1>
<em>$--.--</em>
</h1>
<h5>Error fetching the latest price information</h5>
</div>
)}
</div>
{solanaInfo && (
<p className="updated-time text-muted mb-0">
Updated at{" "}
{displayTimestampWithoutDate(solanaInfo.last_updated.getTime())}
</p>
)}
</div>
<div className="col-12 col-lg-4 col-xl">
<div className="card">
<div className="card-body">
{solanaInfo && (
<>
<h4>
Price{" "}
<span className="ml-2 badge badge-primary rank">
Rank #{solanaInfo.market_cap_rank}
</span>
</h4>
<h1>
<em>${solanaInfo.price.toFixed(2)}</em>{" "}
{solanaInfo.price_change_percentage_24h > 0 && (
<small className="change-positive">
&uarr; {solanaInfo.price_change_percentage_24h.toFixed(2)}
%
</small>
)}
{solanaInfo.price_change_percentage_24h < 0 && (
<small className="change-negative">
&darr; {solanaInfo.price_change_percentage_24h.toFixed(2)}
%
</small>
)}
{solanaInfo.price_change_percentage_24h === 0 && (
<small>0%</small>
)}
</h1>
<h5>
24h Vol: <em>${abbreviatedNumber(solanaInfo.volume_24)}</em>{" "}
MCap: <em>${abbreviatedNumber(solanaInfo.market_cap)}</em>
</h5>
</>
)}
{coinInfo.status === CoingeckoStatus.FetchFailed && (
<>
<h4>Price</h4>
<h1>
<em>$--.--</em>
</h1>
<h5>Error fetching the latest price information</h5>
</>
)}
{solanaInfo && (
<p className="updated-time text-muted">
Updated at{" "}
{displayTimestampWithoutDate(solanaInfo.last_updated.getTime())}
</p>
)}
</div>
</div>
</div>
</div>
);
}
const abbreviatedNumber = (value: number, fixed = 1) => {
if (value < 1e3) return value;
if (value >= 1e3 && value < 1e6) return +(value / 1e3).toFixed(fixed) + "K";
if (value >= 1e6 && value < 1e9) return +(value / 1e6).toFixed(fixed) + "M";
if (value >= 1e9 && value < 1e12) return +(value / 1e9).toFixed(fixed) + "B";
if (value >= 1e12) return +(value / 1e12).toFixed(fixed) + "T";
};
function displayLamports(value: number) {
return abbreviatedNumber(lamportsToSol(value));
}

View File

@ -379,8 +379,11 @@ pre.json-wrap {
}
p.updated-time {
position: absolute;
font-size: 0.66rem;
text-align: right;
margin: .375rem;
right: 0;
bottom: 0;
}
.change-positive {

View File

@ -9,6 +9,7 @@ const CoinGeckoClient = new CoinGecko();
export enum CoingeckoStatus {
Success,
FetchFailed,
Loading,
}
export interface CoinInfo {
@ -49,7 +50,12 @@ export function useCoinGecko(coinId?: string): CoinGeckoResult | undefined {
React.useEffect(() => {
let interval: NodeJS.Timeout | undefined;
if (coinId) {
const getCoinInfo = () => {
const getCoinInfo = (refresh = false) => {
if (!refresh) {
setCoinInfo({
status: CoingeckoStatus.Loading,
});
}
CoinGeckoClient.coins
.fetch(coinId)
.then((info: CoinInfoResult) => {
@ -75,7 +81,7 @@ export function useCoinGecko(coinId?: string): CoinGeckoResult | undefined {
getCoinInfo();
interval = setInterval(() => {
getCoinInfo();
getCoinInfo(true);
}, PRICE_REFRESH);
}
return () => {

View File

@ -125,3 +125,11 @@ export function camelToTitleCase(str: string): string {
const result = str.replace(/([A-Z])/g, " $1");
return result.charAt(0).toUpperCase() + result.slice(1);
}
export function abbreviatedNumber(value: number, fixed = 1) {
if (value < 1e3) return value;
if (value >= 1e3 && value < 1e6) return +(value / 1e3).toFixed(fixed) + "K";
if (value >= 1e6 && value < 1e9) return +(value / 1e6).toFixed(fixed) + "M";
if (value >= 1e9 && value < 1e12) return +(value / 1e9).toFixed(fixed) + "B";
if (value >= 1e12) return +(value / 1e12).toFixed(fixed) + "T";
}