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