Add token2 support to explorer (#11905)
This commit is contained in:
parent
befd99edac
commit
0b47cd1c67
|
@ -11,10 +11,9 @@ import { coerce } from "superstruct";
|
|||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { Address } from "components/common/Address";
|
||||
import { UnknownAccountCard } from "./UnknownAccountCard";
|
||||
import { useFetchTokenSupply, useTokenSupply } from "providers/mints/supply";
|
||||
import { FetchStatus } from "providers/cache";
|
||||
import { TokenRegistry } from "tokenRegistry";
|
||||
import { useCluster } from "providers/cluster";
|
||||
import { normalizeTokenAmount } from "utils";
|
||||
|
||||
export function TokenAccountSection({
|
||||
account,
|
||||
|
@ -58,35 +57,7 @@ function MintAccountCard({
|
|||
const { cluster } = useCluster();
|
||||
const mintAddress = account.pubkey.toBase58();
|
||||
const fetchInfo = useFetchAccountInfo();
|
||||
const supply = useTokenSupply(mintAddress);
|
||||
const fetchSupply = useFetchTokenSupply();
|
||||
const refreshSupply = () => fetchSupply(account.pubkey);
|
||||
const refresh = () => {
|
||||
fetchInfo(account.pubkey);
|
||||
refreshSupply();
|
||||
};
|
||||
|
||||
let renderSupply;
|
||||
const supplyTotal = supply?.data?.uiAmount;
|
||||
if (supplyTotal === undefined) {
|
||||
if (!supply || supply?.status === FetchStatus.Fetching) {
|
||||
renderSupply = (
|
||||
<>
|
||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||
Loading
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
renderSupply = "Fetch failed";
|
||||
}
|
||||
} else {
|
||||
const unit = TokenRegistry.get(mintAddress, cluster)?.symbol;
|
||||
renderSupply = unit ? `${supplyTotal} ${unit}` : supplyTotal;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!supply) refreshSupply();
|
||||
}, [mintAddress]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
const refresh = () => fetchInfo(account.pubkey);
|
||||
|
||||
const tokenInfo = TokenRegistry.get(mintAddress, cluster);
|
||||
return (
|
||||
|
@ -109,8 +80,14 @@ function MintAccountCard({
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Supply</td>
|
||||
<td className="text-lg-right">{renderSupply}</td>
|
||||
<td>
|
||||
{info.mintAuthority === null ? "Fixed Supply" : "Current Supply"}
|
||||
</td>
|
||||
<td className="text-lg-right">
|
||||
{normalizeTokenAmount(info.supply, info.decimals).toFixed(
|
||||
info.decimals
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
{tokenInfo && (
|
||||
<tr>
|
||||
|
@ -127,6 +104,22 @@ function MintAccountCard({
|
|||
</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>
|
||||
|
@ -137,14 +130,6 @@ function MintAccountCard({
|
|||
<td className="text-lg-right">Uninitialized</td>
|
||||
</tr>
|
||||
)}
|
||||
{info.owner && (
|
||||
<tr>
|
||||
<td>Owner</td>
|
||||
<td className="text-lg-right">
|
||||
<Address pubkey={info.owner} alignRight link />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</TableCardBody>
|
||||
</div>
|
||||
);
|
||||
|
@ -160,9 +145,23 @@ function TokenAccountCard({
|
|||
const refresh = useFetchAccountInfo();
|
||||
const { cluster } = useCluster();
|
||||
|
||||
const balance = info.tokenAmount?.uiAmount;
|
||||
const unit =
|
||||
TokenRegistry.get(info.mint.toBase58(), cluster)?.symbol || "tokens";
|
||||
let unit, balance;
|
||||
if (info.isNative) {
|
||||
unit = "SOL";
|
||||
balance = (
|
||||
<>
|
||||
◎
|
||||
<span className="text-monospace">
|
||||
{new Intl.NumberFormat("en-US", { maximumFractionDigits: 9 }).format(
|
||||
info.tokenAmount.uiAmount
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
balance = <>{info.tokenAmount.uiAmount}</>;
|
||||
unit = TokenRegistry.get(info.mint.toBase58(), cluster)?.symbol || "tokens";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
|
@ -199,15 +198,30 @@ function TokenAccountCard({
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Balance ({unit})</td>
|
||||
<td>Token balance ({unit})</td>
|
||||
<td className="text-lg-right">{balance}</td>
|
||||
</tr>
|
||||
{!info.isInitialized && (
|
||||
{info.state === "uninitialized" && (
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td className="text-lg-right">Uninitialized</td>
|
||||
</tr>
|
||||
)}
|
||||
{info.rentExemptReserve && (
|
||||
<tr>
|
||||
<td>Rent-exempt reserve (SOL)</td>
|
||||
<td className="text-lg-right">
|
||||
<>
|
||||
◎
|
||||
<span className="text-monospace">
|
||||
{new Intl.NumberFormat("en-US", {
|
||||
maximumFractionDigits: 9,
|
||||
}).format(info.rentExemptReserve.uiAmount)}
|
||||
</span>
|
||||
</>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</TableCardBody>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,6 @@ import { PublicKey, TokenAccountBalancePair } from "@solana/web3.js";
|
|||
import { LoadingCard } from "components/common/LoadingCard";
|
||||
import { ErrorCard } from "components/common/ErrorCard";
|
||||
import { Address } from "components/common/Address";
|
||||
import { useTokenSupply } from "providers/mints/supply";
|
||||
import {
|
||||
useTokenLargestTokens,
|
||||
useFetchTokenLargestAccounts,
|
||||
|
@ -11,10 +10,12 @@ import {
|
|||
import { FetchStatus } from "providers/cache";
|
||||
import { TokenRegistry } from "tokenRegistry";
|
||||
import { useCluster } from "providers/cluster";
|
||||
import { useMintAccountInfo } from "providers/accounts";
|
||||
import { normalizeTokenAmount } from "utils";
|
||||
|
||||
export function TokenLargestAccountsCard({ pubkey }: { pubkey: PublicKey }) {
|
||||
const mintAddress = pubkey.toBase58();
|
||||
const supply = useTokenSupply(mintAddress);
|
||||
const mintInfo = useMintAccountInfo(mintAddress);
|
||||
const largestAccounts = useTokenLargestTokens(mintAddress);
|
||||
const fetchLargestAccounts = useFetchTokenLargestAccounts();
|
||||
const refreshLargest = () => fetchLargestAccounts(pubkey);
|
||||
|
@ -26,10 +27,11 @@ export function TokenLargestAccountsCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
if (!largestAccounts) refreshLargest();
|
||||
}, [mintAddress]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const supplyTotal = supply?.data?.uiAmount;
|
||||
if (supplyTotal === undefined || !largestAccounts) {
|
||||
return null;
|
||||
}
|
||||
// Largest accounts hasn't started fetching
|
||||
if (largestAccounts === undefined) return null;
|
||||
|
||||
// This is not a mint account
|
||||
if (mintInfo === undefined) return null;
|
||||
|
||||
if (largestAccounts?.data === undefined) {
|
||||
if (largestAccounts.status === FetchStatus.Fetching) {
|
||||
|
@ -49,6 +51,7 @@ export function TokenLargestAccountsCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
return <ErrorCard text="No holders found" />;
|
||||
}
|
||||
|
||||
const supplyTotal = normalizeTokenAmount(mintInfo.supply, mintInfo.decimals);
|
||||
return (
|
||||
<>
|
||||
<div className="card">
|
||||
|
|
|
@ -13,6 +13,12 @@ import { InstructionCard } from "../InstructionCard";
|
|||
import { Address } from "components/common/Address";
|
||||
import { IX_STRUCTS, TokenInstructionType, IX_TITLES } from "./types";
|
||||
import { ParsedInfo } from "validators";
|
||||
import {
|
||||
useTokenAccountInfo,
|
||||
useMintAccountInfo,
|
||||
useFetchAccountInfo,
|
||||
} from "providers/accounts";
|
||||
import { normalizeTokenAmount } from "utils";
|
||||
|
||||
type DetailsProps = {
|
||||
tx: ParsedTransaction;
|
||||
|
@ -48,6 +54,50 @@ type InfoProps = {
|
|||
};
|
||||
|
||||
function TokenInstruction(props: InfoProps) {
|
||||
const { mintAddress: infoMintAddress, tokenAddress } = React.useMemo(() => {
|
||||
let mintAddress: string | undefined;
|
||||
let tokenAddress: string | undefined;
|
||||
|
||||
// No sense fetching accounts if we don't need to convert an amount
|
||||
if (!("amount" in props.info)) return {};
|
||||
|
||||
if ("mint" in props.info && props.info.mint instanceof PublicKey) {
|
||||
mintAddress = props.info.mint.toBase58();
|
||||
} else if (
|
||||
"account" in props.info &&
|
||||
props.info.account instanceof PublicKey
|
||||
) {
|
||||
tokenAddress = props.info.account.toBase58();
|
||||
} else if (
|
||||
"source" in props.info &&
|
||||
props.info.source instanceof PublicKey
|
||||
) {
|
||||
tokenAddress = props.info.source.toBase58();
|
||||
}
|
||||
return {
|
||||
mintAddress,
|
||||
tokenAddress,
|
||||
};
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const tokenInfo = useTokenAccountInfo(tokenAddress);
|
||||
const mintAddress = infoMintAddress || tokenInfo?.mint.toBase58();
|
||||
const mintInfo = useMintAccountInfo(mintAddress);
|
||||
const fetchAccountInfo = useFetchAccountInfo();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (tokenAddress && !tokenInfo) {
|
||||
fetchAccountInfo(new PublicKey(tokenAddress));
|
||||
}
|
||||
}, [fetchAccountInfo, tokenAddress]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mintAddress && !mintInfo) {
|
||||
fetchAccountInfo(new PublicKey(mintAddress));
|
||||
}
|
||||
}, [fetchAccountInfo, mintAddress]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const decimals = mintInfo?.decimals;
|
||||
const attributes = [];
|
||||
for (let key in props.info) {
|
||||
const value = props.info[key];
|
||||
|
@ -56,6 +106,12 @@ function TokenInstruction(props: InfoProps) {
|
|||
let tag;
|
||||
if (value instanceof PublicKey) {
|
||||
tag = <Address pubkey={value} alignRight link />;
|
||||
} else if (key === "amount") {
|
||||
if (decimals === undefined) {
|
||||
tag = <>(raw) {value}</>;
|
||||
} else {
|
||||
tag = <>{normalizeTokenAmount(value, decimals).toFixed(decimals)}</>;
|
||||
}
|
||||
} else {
|
||||
tag = <>{value}</>;
|
||||
}
|
||||
|
|
|
@ -5,25 +5,29 @@ import {
|
|||
number,
|
||||
optional,
|
||||
array,
|
||||
pick,
|
||||
nullable,
|
||||
} from "superstruct";
|
||||
import { Pubkey } from "validators/pubkey";
|
||||
|
||||
const InitializeMint = object({
|
||||
const InitializeMint = pick({
|
||||
mint: Pubkey,
|
||||
amount: number(),
|
||||
decimals: number(),
|
||||
owner: optional(Pubkey),
|
||||
account: optional(Pubkey),
|
||||
mintAuthority: Pubkey,
|
||||
rentSysvar: Pubkey,
|
||||
freezeAuthority: optional(Pubkey),
|
||||
});
|
||||
|
||||
const InitializeAccount = object({
|
||||
const InitializeAccount = pick({
|
||||
account: Pubkey,
|
||||
mint: Pubkey,
|
||||
owner: Pubkey,
|
||||
rentSysvar: Pubkey,
|
||||
});
|
||||
|
||||
const InitializeMultisig = object({
|
||||
const InitializeMultisig = pick({
|
||||
multisig: Pubkey,
|
||||
rentSysvar: Pubkey,
|
||||
signers: array(Pubkey),
|
||||
m: number(),
|
||||
});
|
||||
|
@ -53,11 +57,20 @@ const Revoke = object({
|
|||
signers: optional(array(Pubkey)),
|
||||
});
|
||||
|
||||
const SetOwner = object({
|
||||
owned: Pubkey,
|
||||
newOwner: Pubkey,
|
||||
owner: optional(Pubkey),
|
||||
multisigOwner: optional(Pubkey),
|
||||
const AuthorityType = enums([
|
||||
"mintTokens",
|
||||
"freezeAccount",
|
||||
"accountOwner",
|
||||
"closeAccount",
|
||||
]);
|
||||
|
||||
const SetAuthority = object({
|
||||
mint: optional(Pubkey),
|
||||
account: optional(Pubkey),
|
||||
authorityType: AuthorityType,
|
||||
newAuthority: nullable(Pubkey),
|
||||
authority: optional(Pubkey),
|
||||
multisigAuthority: optional(Pubkey),
|
||||
signers: optional(array(Pubkey)),
|
||||
});
|
||||
|
||||
|
@ -65,13 +78,14 @@ const MintTo = object({
|
|||
mint: Pubkey,
|
||||
account: Pubkey,
|
||||
amount: number(),
|
||||
owner: optional(Pubkey),
|
||||
multisigOwner: optional(Pubkey),
|
||||
mintAuthority: optional(Pubkey),
|
||||
multisigMintAuthority: optional(Pubkey),
|
||||
signers: optional(array(Pubkey)),
|
||||
});
|
||||
|
||||
const Burn = object({
|
||||
account: Pubkey,
|
||||
mint: Pubkey,
|
||||
amount: number(),
|
||||
authority: optional(Pubkey),
|
||||
multisigAuthority: optional(Pubkey),
|
||||
|
@ -94,7 +108,7 @@ export const TokenInstructionType = enums([
|
|||
"transfer",
|
||||
"approve",
|
||||
"revoke",
|
||||
"setOwner",
|
||||
"setAuthority",
|
||||
"mintTo",
|
||||
"burn",
|
||||
"closeAccount",
|
||||
|
@ -107,7 +121,7 @@ export const IX_STRUCTS = {
|
|||
transfer: Transfer,
|
||||
approve: Approve,
|
||||
revoke: Revoke,
|
||||
setOwner: SetOwner,
|
||||
setAuthority: SetAuthority,
|
||||
mintTo: MintTo,
|
||||
burn: Burn,
|
||||
closeAccount: CloseAccount,
|
||||
|
@ -120,7 +134,7 @@ export const IX_TITLES = {
|
|||
transfer: "Transfer",
|
||||
approve: "Approve",
|
||||
revoke: "Revoke",
|
||||
setOwner: "Set Owner",
|
||||
setAuthority: "Set Authority",
|
||||
mintTo: "Mint To",
|
||||
burn: "Burn",
|
||||
closeAccount: "Close Account",
|
||||
|
|
|
@ -108,7 +108,7 @@ function DetailsSections({ pubkey, tab }: { pubkey: PublicKey; tab?: string }) {
|
|||
},
|
||||
];
|
||||
|
||||
if (data && data?.name === "spl-token") {
|
||||
if (data && data?.program === "spl-token") {
|
||||
if (data.parsed.type === "mint") {
|
||||
tabs.push({
|
||||
slug: "largest",
|
||||
|
@ -141,7 +141,7 @@ function DetailsSections({ pubkey, tab }: { pubkey: PublicKey; tab?: string }) {
|
|||
|
||||
function InfoSection({ account }: { account: Account }) {
|
||||
const data = account?.details?.data;
|
||||
if (data && data.name === "stake") {
|
||||
if (data && data.program === "stake") {
|
||||
let stakeAccountType, stakeAccount;
|
||||
if ("accountType" in data.parsed) {
|
||||
stakeAccount = data.parsed;
|
||||
|
@ -158,7 +158,7 @@ function InfoSection({ account }: { account: Account }) {
|
|||
stakeAccountType={stakeAccountType}
|
||||
/>
|
||||
);
|
||||
} else if (data && data.name === "spl-token") {
|
||||
} else if (data && data.program === "spl-token") {
|
||||
return <TokenAccountSection account={account} tokenAccount={data.parsed} />;
|
||||
} else {
|
||||
return <UnknownAccountCard account={account} />;
|
||||
|
|
|
@ -8,18 +8,22 @@ import { TokensProvider, TOKEN_PROGRAM_ID } from "./tokens";
|
|||
import { coerce } from "superstruct";
|
||||
import { ParsedInfo } from "validators";
|
||||
import { StakeAccount } from "validators/accounts/stake";
|
||||
import { TokenAccount } from "validators/accounts/token";
|
||||
import {
|
||||
TokenAccount,
|
||||
MintAccountInfo,
|
||||
TokenAccountInfo,
|
||||
} from "validators/accounts/token";
|
||||
import * as Cache from "providers/cache";
|
||||
import { ActionType, FetchStatus } from "providers/cache";
|
||||
export { useAccountHistory } from "./history";
|
||||
|
||||
export type StakeProgramData = {
|
||||
name: "stake";
|
||||
program: "stake";
|
||||
parsed: StakeAccount | StakeAccountWasm;
|
||||
};
|
||||
|
||||
export type TokenProgramData = {
|
||||
name: "spl-token";
|
||||
program: "spl-token";
|
||||
parsed: TokenAccount;
|
||||
};
|
||||
|
||||
|
@ -108,7 +112,7 @@ async function fetchAccountInfo(
|
|||
parsed = wasm.StakeAccount.fromAccountData(result.data);
|
||||
}
|
||||
data = {
|
||||
name: "stake",
|
||||
program: "stake",
|
||||
parsed,
|
||||
};
|
||||
} catch (err) {
|
||||
|
@ -123,7 +127,7 @@ async function fetchAccountInfo(
|
|||
const info = coerce(result.data.parsed, ParsedInfo);
|
||||
const parsed = coerce(info, TokenAccount);
|
||||
data = {
|
||||
name: "spl-token",
|
||||
program: "spl-token",
|
||||
parsed,
|
||||
};
|
||||
} catch (err) {
|
||||
|
@ -166,17 +170,59 @@ export function useAccounts() {
|
|||
}
|
||||
|
||||
export function useAccountInfo(
|
||||
address: string
|
||||
address: string | undefined
|
||||
): Cache.CacheEntry<Account> | undefined {
|
||||
const context = React.useContext(StateContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(`useAccountInfo must be used within a AccountsProvider`);
|
||||
}
|
||||
|
||||
if (address === undefined) return;
|
||||
return context.entries[address];
|
||||
}
|
||||
|
||||
export function useMintAccountInfo(
|
||||
address: string | undefined
|
||||
): MintAccountInfo | undefined {
|
||||
const accountInfo = useAccountInfo(address);
|
||||
if (address === undefined) return;
|
||||
|
||||
try {
|
||||
const data = accountInfo?.data?.details?.data;
|
||||
if (!data) return;
|
||||
if (data.program !== "spl-token" || data.parsed.type !== "mint") {
|
||||
throw new Error("Expected mint");
|
||||
}
|
||||
|
||||
return coerce(data.parsed.info, MintAccountInfo);
|
||||
} catch (err) {
|
||||
Sentry.captureException(err, {
|
||||
tags: { address },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function useTokenAccountInfo(
|
||||
address: string | undefined
|
||||
): TokenAccountInfo | undefined {
|
||||
const accountInfo = useAccountInfo(address);
|
||||
if (address === undefined) return;
|
||||
|
||||
try {
|
||||
const data = accountInfo?.data?.details?.data;
|
||||
if (!data) return;
|
||||
if (data.program !== "spl-token" || data.parsed.type !== "account") {
|
||||
throw new Error("Expected token account");
|
||||
}
|
||||
|
||||
return coerce(data.parsed.info, TokenAccountInfo);
|
||||
} catch (err) {
|
||||
Sentry.captureException(err, {
|
||||
tags: { address },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function useFetchAccountInfo() {
|
||||
const dispatch = React.useContext(DispatchContext);
|
||||
if (!dispatch) {
|
||||
|
@ -186,7 +232,10 @@ export function useFetchAccountInfo() {
|
|||
}
|
||||
|
||||
const { url } = useCluster();
|
||||
return (pubkey: PublicKey) => {
|
||||
fetchAccountInfo(dispatch, pubkey, url);
|
||||
};
|
||||
return React.useCallback(
|
||||
(pubkey: PublicKey) => {
|
||||
fetchAccountInfo(dispatch, pubkey, url);
|
||||
},
|
||||
[dispatch, url]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ export function TokensProvider({ children }: ProviderProps) {
|
|||
}
|
||||
|
||||
export const TOKEN_PROGRAM_ID = new PublicKey(
|
||||
"TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
);
|
||||
|
||||
async function fetchAccountTokens(
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import React from "react";
|
||||
import { SupplyProvider } from "./supply";
|
||||
import { LargestAccountsProvider } from "./largest";
|
||||
|
||||
type ProviderProps = { children: React.ReactNode };
|
||||
export function MintsProvider({ children }: ProviderProps) {
|
||||
return (
|
||||
<SupplyProvider>
|
||||
<LargestAccountsProvider>{children}</LargestAccountsProvider>
|
||||
</SupplyProvider>
|
||||
);
|
||||
return <LargestAccountsProvider>{children}</LargestAccountsProvider>;
|
||||
}
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
import React from "react";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { useCluster } from "providers/cluster";
|
||||
import * as Cache from "providers/cache";
|
||||
import { ActionType, FetchStatus } from "providers/cache";
|
||||
import { TokenAmount, PublicKey, Connection } from "@solana/web3.js";
|
||||
|
||||
type State = Cache.State<TokenAmount>;
|
||||
type Dispatch = Cache.Dispatch<TokenAmount>;
|
||||
|
||||
const StateContext = React.createContext<State | undefined>(undefined);
|
||||
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
||||
|
||||
type ProviderProps = { children: React.ReactNode };
|
||||
export function SupplyProvider({ children }: ProviderProps) {
|
||||
const { url } = useCluster();
|
||||
const [state, dispatch] = Cache.useReducer<TokenAmount>(url);
|
||||
|
||||
// Clear cache whenever cluster is changed
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: ActionType.Clear, url });
|
||||
}, [dispatch, url]);
|
||||
|
||||
return (
|
||||
<StateContext.Provider value={state}>
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
</DispatchContext.Provider>
|
||||
</StateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchSupply(dispatch: Dispatch, pubkey: PublicKey, url: string) {
|
||||
dispatch({
|
||||
type: ActionType.Update,
|
||||
key: pubkey.toBase58(),
|
||||
status: Cache.FetchStatus.Fetching,
|
||||
url,
|
||||
});
|
||||
|
||||
let data;
|
||||
let fetchStatus;
|
||||
try {
|
||||
data = (await new Connection(url, "single").getTokenSupply(pubkey)).value;
|
||||
|
||||
fetchStatus = FetchStatus.Fetched;
|
||||
} catch (error) {
|
||||
Sentry.captureException(error, { tags: { url } });
|
||||
fetchStatus = FetchStatus.FetchFailed;
|
||||
}
|
||||
dispatch({
|
||||
type: ActionType.Update,
|
||||
status: fetchStatus,
|
||||
data,
|
||||
key: pubkey.toBase58(),
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
export function useFetchTokenSupply() {
|
||||
const dispatch = React.useContext(DispatchContext);
|
||||
if (!dispatch) {
|
||||
throw new Error(`useFetchTokenSupply must be used within a MintsProvider`);
|
||||
}
|
||||
|
||||
const { url } = useCluster();
|
||||
return (pubkey: PublicKey) => {
|
||||
fetchSupply(dispatch, pubkey, url);
|
||||
};
|
||||
}
|
||||
|
||||
export function useTokenSupply(
|
||||
address: string
|
||||
): Cache.CacheEntry<TokenAmount> | undefined {
|
||||
const context = React.useContext(StateContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(`useTokenSupply must be used within a MintsProvider`);
|
||||
}
|
||||
|
||||
return context.entries[address];
|
||||
}
|
|
@ -18,6 +18,16 @@ export function assertUnreachable(x: never): never {
|
|||
throw new Error("Unreachable!");
|
||||
}
|
||||
|
||||
export function normalizeTokenAmount(
|
||||
raw: string | number,
|
||||
decimals: number
|
||||
): number {
|
||||
let rawTokens: number;
|
||||
if (typeof raw === "string") rawTokens = parseInt(raw);
|
||||
else rawTokens = raw;
|
||||
return rawTokens / Math.pow(10, decimals);
|
||||
}
|
||||
|
||||
export function lamportsToSol(lamports: number | BN): number {
|
||||
if (typeof lamports === "number") {
|
||||
return Math.abs(lamports) / LAMPORTS_PER_SOL;
|
||||
|
|
|
@ -35,7 +35,7 @@ export const PROGRAM_IDS: { [key: string]: ProgramName } = {
|
|||
[SystemProgram.programId.toBase58()]: "System Program",
|
||||
Vest111111111111111111111111111111111111111: "Vest Program",
|
||||
[VOTE_PROGRAM_ID.toBase58()]: "Vote Program",
|
||||
TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o: "SPL Token",
|
||||
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: "SPL Token",
|
||||
};
|
||||
|
||||
const LOADER_IDS = {
|
||||
|
@ -49,6 +49,10 @@ const SYSVAR_ID: { [key: string]: string } = {
|
|||
Sysvar1111111111111111111111111111111111111: "SYSVAR",
|
||||
};
|
||||
|
||||
const WRAPPED_SOL: { [key: string]: string } = {
|
||||
So11111111111111111111111111111111111111112: "Wrapped SOL",
|
||||
};
|
||||
|
||||
export const SYSVAR_IDS = {
|
||||
[SYSVAR_CLOCK_PUBKEY.toBase58()]: "SYSVAR_CLOCK",
|
||||
SysvarEpochSchedu1e111111111111111111111111: "SYSVAR_EPOCH_SCHEDULE",
|
||||
|
@ -67,6 +71,7 @@ export function displayAddress(address: string, cluster: Cluster): string {
|
|||
LOADER_IDS[address] ||
|
||||
SYSVAR_IDS[address] ||
|
||||
SYSVAR_ID[address] ||
|
||||
WRAPPED_SOL[address] ||
|
||||
TokenRegistry.get(address, cluster)?.name ||
|
||||
address
|
||||
);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
object,
|
||||
StructType,
|
||||
number,
|
||||
optional,
|
||||
|
@ -16,30 +15,39 @@ import { Pubkey } from "validators/pubkey";
|
|||
export type TokenAccountType = StructType<typeof TokenAccountType>;
|
||||
export const TokenAccountType = enums(["mint", "account", "multisig"]);
|
||||
|
||||
export type TokenAccountState = StructType<typeof AccountState>;
|
||||
const AccountState = enums(["initialized", "uninitialized", "frozen"]);
|
||||
|
||||
const TokenAmount = pick({
|
||||
decimals: number(),
|
||||
uiAmount: number(),
|
||||
amount: string(),
|
||||
});
|
||||
|
||||
export type TokenAccountInfo = StructType<typeof TokenAccountInfo>;
|
||||
export const TokenAccountInfo = pick({
|
||||
isInitialized: boolean(),
|
||||
isNative: boolean(),
|
||||
mint: Pubkey,
|
||||
owner: Pubkey,
|
||||
tokenAmount: pick({
|
||||
decimals: number(),
|
||||
uiAmount: number(),
|
||||
amount: string(),
|
||||
}),
|
||||
delegate: nullable(optional(Pubkey)),
|
||||
delegatedAmount: optional(number()),
|
||||
tokenAmount: TokenAmount,
|
||||
delegate: optional(Pubkey),
|
||||
state: AccountState,
|
||||
isNative: boolean(),
|
||||
rentExemptReserve: optional(TokenAmount),
|
||||
delegatedAmount: optional(TokenAmount),
|
||||
closeAuthority: optional(Pubkey),
|
||||
});
|
||||
|
||||
export type MintAccountInfo = StructType<typeof MintAccountInfo>;
|
||||
export const MintAccountInfo = object({
|
||||
export const MintAccountInfo = pick({
|
||||
mintAuthority: nullable(Pubkey),
|
||||
supply: string(),
|
||||
decimals: number(),
|
||||
isInitialized: boolean(),
|
||||
owner: nullable(optional(Pubkey)),
|
||||
freezeAuthority: nullable(Pubkey),
|
||||
});
|
||||
|
||||
export type MultisigAccountInfo = StructType<typeof MultisigAccountInfo>;
|
||||
export const MultisigAccountInfo = object({
|
||||
export const MultisigAccountInfo = pick({
|
||||
numRequiredSigners: number(),
|
||||
numValidSigners: number(),
|
||||
isInitialized: boolean(),
|
||||
|
@ -47,7 +55,7 @@ export const MultisigAccountInfo = object({
|
|||
});
|
||||
|
||||
export type TokenAccount = StructType<typeof TokenAccount>;
|
||||
export const TokenAccount = object({
|
||||
export const TokenAccount = pick({
|
||||
type: TokenAccountType,
|
||||
info: any(),
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue