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:
@ -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">
|
||||
↑{" "}
|
||||
{tokenPriceInfo.price_change_percentage_24h.toFixed(2)}%
|
||||
</small>
|
||||
)}
|
||||
{tokenPriceInfo.price_change_percentage_24h < 0 && (
|
||||
<small className="change-negative">
|
||||
↓{" "}
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
↑ {solanaInfo.price_change_percentage_24h.toFixed(2)}%
|
||||
</small>
|
||||
)}
|
||||
{solanaInfo.price_change_percentage_24h < 0 && (
|
||||
<small className="change-negative">
|
||||
↓ {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">
|
||||
↑ {solanaInfo.price_change_percentage_24h.toFixed(2)}
|
||||
%
|
||||
</small>
|
||||
)}
|
||||
{solanaInfo.price_change_percentage_24h < 0 && (
|
||||
<small className="change-negative">
|
||||
↓ {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));
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 () => {
|
||||
|
@ -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";
|
||||
}
|
||||
|
Reference in New Issue
Block a user