Explorer: Batch account fetching (#28415)

* Bump @solana/web3.js to 1.66.0

* Explorer: Add batched account fetcher to reduce RPC rate limiting
This commit is contained in:
Justin Starry 2022-10-16 21:18:49 +08:00 committed by GitHub
parent 831ed96730
commit d42e5725fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 506 additions and 394 deletions

View File

@ -20,7 +20,7 @@
"@solana/buffer-layout": "^3.0.0",
"@solana/spl-token": "^0.0.13",
"@solana/spl-token-registry": "^0.2.3736",
"@solana/web3.js": "^1.63.1",
"@solana/web3.js": "^1.66.0",
"axios": "^0.27.2",
"bignumber.js": "^9.0.2",
"bn.js": "^5.2.0",
@ -4490,6 +4490,14 @@
"node": ">= 10"
}
},
"node_modules/@metaplex/js/node_modules/@types/bn.js": {
"version": "4.11.6",
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz",
"integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@metaplex/js/node_modules/axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
@ -4498,6 +4506,17 @@
"follow-redirects": "^1.14.7"
}
},
"node_modules/@metaplex/js/node_modules/borsh": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/borsh/-/borsh-0.4.0.tgz",
"integrity": "sha512-aX6qtLya3K0AkT66CmYWCCDr77qsE9arV05OmdFpmat9qu8Pg9J5tBUPDztAW5fNh/d/MyVG/OYziP52Ndzx1g==",
"dependencies": {
"@types/bn.js": "^4.11.5",
"bn.js": "^5.0.0",
"bs58": "^4.0.0",
"text-encoding-utf-8": "^1.0.2"
}
},
"node_modules/@metaplex/js/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@ -5154,9 +5173,9 @@
}
},
"node_modules/@solana/web3.js": {
"version": "1.63.1",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.63.1.tgz",
"integrity": "sha512-wgEdGVK5FTS2zENxbcGSvKpGZ0jDS6BUdGu8Gn6ns0CzgJkK83u4ip3THSnBPEQ5i/jrqukg998BwV1H67+qiQ==",
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.66.0.tgz",
"integrity": "sha512-hQCzWd9u100Ba3da52u7GeDRqSRwyFZtZkUj4j08GKSK3c3+ZQ6CQoN3HBXzfyjVKMTyRGKT0FlPA+hOX3kmOQ==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@noble/ed25519": "^1.7.0",
@ -5220,16 +5239,6 @@
"ieee754": "^1.2.1"
}
},
"node_modules/@solana/web3.js/node_modules/borsh": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz",
"integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==",
"dependencies": {
"bn.js": "^5.2.0",
"bs58": "^4.0.0",
"text-encoding-utf-8": "^1.0.2"
}
},
"node_modules/@solana/web3.js/node_modules/superstruct": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
@ -8145,24 +8154,15 @@
}
},
"node_modules/borsh": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/borsh/-/borsh-0.4.0.tgz",
"integrity": "sha512-aX6qtLya3K0AkT66CmYWCCDr77qsE9arV05OmdFpmat9qu8Pg9J5tBUPDztAW5fNh/d/MyVG/OYziP52Ndzx1g==",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz",
"integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==",
"dependencies": {
"@types/bn.js": "^4.11.5",
"bn.js": "^5.0.0",
"bn.js": "^5.2.0",
"bs58": "^4.0.0",
"text-encoding-utf-8": "^1.0.2"
}
},
"node_modules/borsh/node_modules/@types/bn.js": {
"version": "4.11.6",
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz",
"integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -30706,6 +30706,14 @@
"dotenv": "10.0.0"
}
},
"@types/bn.js": {
"version": "4.11.6",
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz",
"integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==",
"requires": {
"@types/node": "*"
}
},
"axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
@ -30714,6 +30722,17 @@
"follow-redirects": "^1.14.7"
}
},
"borsh": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/borsh/-/borsh-0.4.0.tgz",
"integrity": "sha512-aX6qtLya3K0AkT66CmYWCCDr77qsE9arV05OmdFpmat9qu8Pg9J5tBUPDztAW5fNh/d/MyVG/OYziP52Ndzx1g==",
"requires": {
"@types/bn.js": "^4.11.5",
"bn.js": "^5.0.0",
"bs58": "^4.0.0",
"text-encoding-utf-8": "^1.0.2"
}
},
"buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@ -31160,9 +31179,9 @@
}
},
"@solana/web3.js": {
"version": "1.63.1",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.63.1.tgz",
"integrity": "sha512-wgEdGVK5FTS2zENxbcGSvKpGZ0jDS6BUdGu8Gn6ns0CzgJkK83u4ip3THSnBPEQ5i/jrqukg998BwV1H67+qiQ==",
"version": "1.66.0",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.66.0.tgz",
"integrity": "sha512-hQCzWd9u100Ba3da52u7GeDRqSRwyFZtZkUj4j08GKSK3c3+ZQ6CQoN3HBXzfyjVKMTyRGKT0FlPA+hOX3kmOQ==",
"requires": {
"@babel/runtime": "^7.12.5",
"@noble/ed25519": "^1.7.0",
@ -31208,16 +31227,6 @@
}
}
},
"borsh": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz",
"integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==",
"requires": {
"bn.js": "^5.2.0",
"bs58": "^4.0.0",
"text-encoding-utf-8": "^1.0.2"
}
},
"superstruct": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
@ -33506,24 +33515,13 @@
"requires": {}
},
"borsh": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/borsh/-/borsh-0.4.0.tgz",
"integrity": "sha512-aX6qtLya3K0AkT66CmYWCCDr77qsE9arV05OmdFpmat9qu8Pg9J5tBUPDztAW5fNh/d/MyVG/OYziP52Ndzx1g==",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz",
"integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==",
"requires": {
"@types/bn.js": "^4.11.5",
"bn.js": "^5.0.0",
"bn.js": "^5.2.0",
"bs58": "^4.0.0",
"text-encoding-utf-8": "^1.0.2"
},
"dependencies": {
"@types/bn.js": {
"version": "4.11.6",
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz",
"integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==",
"requires": {
"@types/node": "*"
}
}
}
},
"brace-expansion": {

View File

@ -22,7 +22,7 @@
"@solana/buffer-layout": "^3.0.0",
"@solana/spl-token": "^0.0.13",
"@solana/spl-token-registry": "^0.2.3736",
"@solana/web3.js": "^1.63.1",
"@solana/web3.js": "^1.66.0",
"axios": "^0.27.2",
"bignumber.js": "^9.0.2",
"bn.js": "^5.2.0",

View File

@ -58,7 +58,7 @@ function StakeConfigCard({
<div className="card">
<AccountHeader
title="Stake Config"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
@ -91,7 +91,7 @@ function ValidatorInfoCard({
<div className="card">
<AccountHeader
title="Validator Info"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>

View File

@ -25,7 +25,7 @@ export function MetaplexNFTHeader({
React.useEffect(() => {
if (collectionAddress && !collectionMintInfo) {
fetchAccountInfo(new PublicKey(collectionAddress));
fetchAccountInfo(new PublicKey(collectionAddress), "parsed");
}
}, [fetchAccountInfo, collectionAddress]); // eslint-disable-line react-hooks/exhaustive-deps

View File

@ -21,7 +21,7 @@ export function NonceAccountSection({
<div className="card">
<AccountHeader
title="Nonce Account"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>

View File

@ -108,7 +108,7 @@ function OverviewCard({
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey)}
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh

View File

@ -107,7 +107,7 @@ function SysvarAccountRecentBlockhashesCard({
<div className="card">
<AccountHeader
title="Sysvar: Recent Blockhashes"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
@ -129,7 +129,7 @@ function SysvarAccountSlotHashes({
<div className="card">
<AccountHeader
title="Sysvar: Slot Hashes"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
@ -158,7 +158,7 @@ function SysvarAccountSlotHistory({
<div className="card">
<AccountHeader
title="Sysvar: Slot History"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
@ -194,7 +194,7 @@ function SysvarAccountStakeHistory({
<div className="card">
<AccountHeader
title="Sysvar: Stake History"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
@ -217,7 +217,7 @@ function SysvarAccountFeesCard({
<div className="card">
<AccountHeader
title="Sysvar: Fees"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
@ -247,7 +247,7 @@ function SysvarAccountEpochScheduleCard({
<div className="card">
<AccountHeader
title="Sysvar: Epoch Schedule"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
@ -301,7 +301,7 @@ function SysvarAccountClockCard({
<div className="card">
<AccountHeader
title="Sysvar: Clock"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
@ -352,7 +352,7 @@ function SysvarAccountRentCard({
<div className="card">
<AccountHeader
title="Sysvar: Rent"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>
@ -401,7 +401,7 @@ function SysvarAccountRewardsCard({
<div className="card">
<AccountHeader
title="Sysvar: Rewards"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>

View File

@ -95,7 +95,7 @@ function FungibleTokenMintAccountCard({
const { tokenRegistry } = useTokenRegistry();
const mintAddress = account.pubkey.toBase58();
const fetchInfo = useFetchAccountInfo();
const refresh = () => fetchInfo(account.pubkey);
const refresh = () => fetchInfo(account.pubkey, "parsed");
const tokenInfo = tokenRegistry.get(mintAddress);
const bridgeContractAddress = getEthAddress(
@ -303,7 +303,7 @@ function NonFungibleTokenMintAccountCard({
mintInfo: MintAccountInfo;
}) {
const fetchInfo = useFetchAccountInfo();
const refresh = () => fetchInfo(account.pubkey);
const refresh = () => fetchInfo(account.pubkey, "parsed");
return (
<div className="card">
@ -437,7 +437,7 @@ function TokenAccountCard({
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey)}
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
@ -516,7 +516,7 @@ function MultisigAccountCard({
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey)}
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh

View File

@ -38,10 +38,12 @@ export function UnknownAccountCard({ account }: { account: Account }) {
</td>
</tr>
<tr>
<td>Allocated Data Size</td>
<td className="text-lg-end">{account.space} byte(s)</td>
</tr>
{account.space !== undefined && (
<tr>
<td>Allocated Data Size</td>
<td className="text-lg-end">{account.space} byte(s)</td>
</tr>
)}
<tr>
<td>Assigned Program Id</td>

View File

@ -81,7 +81,7 @@ export function UpgradeableProgramSection({
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey)}
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
@ -218,7 +218,7 @@ export function UpgradeableProgramDataSection({
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey)}
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
@ -238,17 +238,19 @@ export function UpgradeableProgramDataSection({
<SolBalance lamports={account.lamports} />
</td>
</tr>
<tr>
<td>Data Size (Bytes)</td>
<td className="text-lg-end">
<Downloadable
data={programData.data[0]}
filename={`${account.pubkey.toString()}.bin`}
>
<span className="me-2">{account.space}</span>
</Downloadable>
</td>
</tr>
{account.space !== undefined && (
<tr>
<td>Data Size (Bytes)</td>
<td className="text-lg-end">
<Downloadable
data={programData.data[0]}
filename={`${account.pubkey.toString()}.bin`}
>
<span className="me-2">{account.space}</span>
</Downloadable>
</td>
</tr>
)}
<tr>
<td>Upgradeable</td>
<td className="text-lg-end">
@ -290,7 +292,7 @@ export function UpgradeableProgramBufferSection({
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey)}
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh
@ -310,10 +312,12 @@ export function UpgradeableProgramBufferSection({
<SolBalance lamports={account.lamports} />
</td>
</tr>
<tr>
<td>Data Size (Bytes)</td>
<td className="text-lg-end">{account.space}</td>
</tr>
{account.space !== undefined && (
<tr>
<td>Data Size (Bytes)</td>
<td className="text-lg-end">{account.space}</td>
</tr>
)}
{programBuffer.authority !== null && (
<tr>
<td>Deploy Authority</td>

View File

@ -24,7 +24,7 @@ export function VoteAccountSection({
<div className="card">
<AccountHeader
title="Vote Account"
refresh={() => refresh(account.pubkey)}
refresh={() => refresh(account.pubkey, "parsed")}
/>
<TableCardBody>

View File

@ -39,7 +39,7 @@ export function AddressLookupTableAccountSection(
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey)}
onClick={() => refresh(account.pubkey, "parsed")}
>
<span className="fe fe-refresh-cw me-2"></span>
Refresh

View File

@ -28,7 +28,7 @@ export function NFTokenAccountSection({ account }: { account: Account }) {
const NFTCard = ({ nft }: { nft: NftokenTypes.NftAccount }) => {
const fetchInfo = useFetchAccountInfo();
const refresh = () => fetchInfo(new PublicKey(nft.address));
const refresh = () => fetchInfo(new PublicKey(nft.address), "parsed");
return (
<div className="card">
@ -163,7 +163,7 @@ const CollectionCard = ({
collection: NftokenTypes.CollectionAccount;
}) => {
const fetchInfo = useFetchAccountInfo();
const refresh = () => fetchInfo(new PublicKey(collection.address));
const refresh = () => fetchInfo(new PublicKey(collection.address), "parsed");
return (
<div className="card">

View File

@ -96,13 +96,13 @@ function TokenInstruction(props: InfoProps) {
React.useEffect(() => {
if (tokenAddress && !tokenInfo) {
fetchAccountInfo(new PublicKey(tokenAddress));
fetchAccountInfo(new PublicKey(tokenAddress), "parsed");
}
}, [fetchAccountInfo, tokenAddress]); // eslint-disable-line react-hooks/exhaustive-deps
React.useEffect(() => {
if (mintAddress && !mintInfo) {
fetchAccountInfo(new PublicKey(mintAddress));
fetchAccountInfo(new PublicKey(mintAddress), "parsed");
}
}, [fetchAccountInfo, mintAddress]); // eslint-disable-line react-hooks/exhaustive-deps

View File

@ -173,7 +173,7 @@ export function AccountDetailsPage({ address, tab }: Props) {
// Fetch account on load
React.useEffect(() => {
if (!info && status === ClusterStatus.Connected && pubkey) {
fetchAccount(pubkey);
fetchAccount(pubkey, "parsed");
}
}, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps
@ -303,7 +303,12 @@ function DetailsSections({
info.status === FetchStatus.FetchFailed ||
info.data?.lamports === undefined
) {
return <ErrorCard retry={() => fetchAccount(pubkey)} text="Fetch Failed" />;
return (
<ErrorCard
retry={() => fetchAccount(pubkey, "parsed")}
text="Fetch Failed"
/>
);
}
const account = info.data;

View File

@ -1,7 +1,8 @@
import React from "react";
import { PublicKey, VersionedMessage } from "@solana/web3.js";
import { Address } from "components/common/Address";
import { useAddressLookupTable, useFetchAccountInfo } from "providers/accounts";
import { useAddressLookupTable } from "providers/accounts";
import { FetchStatus } from "providers/cache";
export function AddressTableLookupsCard({
message,
@ -80,31 +81,38 @@ function LookupRow({
lookupTableIndex: number;
readOnly: boolean;
}) {
const lookupTable = useAddressLookupTable(lookupTableKey.toBase58());
const fetchAccountInfo = useFetchAccountInfo();
React.useEffect(() => {
if (!lookupTable) fetchAccountInfo(lookupTableKey);
}, [lookupTableKey, lookupTable, fetchAccountInfo]);
const lookupTableInfo = useAddressLookupTable(lookupTableKey.toBase58());
const loadingComponent = (
<span className="text-muted">
<span className="spinner-grow spinner-grow-sm me-2"></span>
Loading
</span>
);
let resolvedKeyComponent;
if (!lookupTable) {
resolvedKeyComponent = (
<span className="text-muted">
<span className="spinner-grow spinner-grow-sm me-2"></span>
Loading
</span>
);
} else if (typeof lookupTable === "string") {
resolvedKeyComponent = (
<span className="text-muted">Invalid Lookup Table</span>
);
} else if (lookupTableIndex < lookupTable.state.addresses.length) {
const resolvedKey = lookupTable.state.addresses[lookupTableIndex];
resolvedKeyComponent = <Address pubkey={resolvedKey} link />;
if (!lookupTableInfo) {
resolvedKeyComponent = loadingComponent;
} else {
resolvedKeyComponent = (
<span className="text-muted">Invalid Lookup Table Index</span>
);
const [lookupTable, status] = lookupTableInfo;
if (status === FetchStatus.Fetching) {
resolvedKeyComponent = loadingComponent;
} else if (status === FetchStatus.FetchFailed || !lookupTable) {
resolvedKeyComponent = (
<span className="text-muted">Failed to fetch Lookup Table</span>
);
} else if (typeof lookupTable === "string") {
resolvedKeyComponent = (
<span className="text-muted">Invalid Lookup Table</span>
);
} else if (lookupTableIndex >= lookupTable.state.addresses.length) {
resolvedKeyComponent = (
<span className="text-muted">Invalid Lookup Table Index</span>
);
} else {
const resolvedKey = lookupTable.state.addresses[lookupTableIndex];
resolvedKeyComponent = <Address pubkey={resolvedKey} link />;
}
}
return (

View File

@ -20,8 +20,6 @@ export const createFeePayerValidator = (
if (account.lamports === 0) return "Account doesn't exist";
if (!account.owner.equals(SystemProgram.programId))
return "Only system-owned accounts can pay fees";
// TODO: Actually nonce accounts can pay fees too
if (account.space > 0) return "Only unallocated accounts can pay fees";
if (account.lamports < feeLamports) {
return "Insufficient funds for fees";
}
@ -42,13 +40,8 @@ export function AddressFromLookupTableWithContext({
lookupTableKey: PublicKey;
lookupTableIndex: number;
}) {
const lookupTable = useAddressLookupTable(lookupTableKey.toBase58());
const fetchAccountInfo = useFetchAccountInfo();
React.useEffect(() => {
if (!lookupTable) fetchAccountInfo(lookupTableKey);
}, [lookupTableKey, lookupTable, fetchAccountInfo]);
let pubkey;
const lookupTableInfo = useAddressLookupTable(lookupTableKey.toBase58());
const lookupTable = lookupTableInfo && lookupTableInfo[0];
if (!lookupTable) {
return (
<span className="text-muted">
@ -58,18 +51,17 @@ export function AddressFromLookupTableWithContext({
);
} else if (typeof lookupTable === "string") {
return <div>Invalid Lookup Table</div>;
} else if (lookupTableIndex < lookupTable.state.addresses.length) {
pubkey = lookupTable.state.addresses[lookupTableIndex];
} else {
} else if (lookupTableIndex >= lookupTable.state.addresses.length) {
return <div>Invalid Lookup Table Index</div>;
} else {
const pubkey = lookupTable.state.addresses[lookupTableIndex];
return (
<div className="d-flex align-items-end flex-column">
<Address pubkey={pubkey} link />
<AccountInfo pubkey={pubkey} />
</div>
);
}
return (
<div className="d-flex align-items-end flex-column">
<Address pubkey={pubkey} link />
<AccountInfo pubkey={pubkey} />
</div>
);
}
export function AddressWithContext({
@ -102,7 +94,7 @@ function AccountInfo({
// Fetch account on load
React.useEffect(() => {
if (!info && status === ClusterStatus.Connected && pubkey) {
fetchAccount(pubkey);
fetchAccount(pubkey, "skip");
}
}, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps
@ -129,9 +121,10 @@ function AccountInfo({
<span className="text-muted">
{`Owned by ${ownerLabel || ownerAddress}.`}
{` Balance is ${lamportsToSolString(account.lamports)} SOL.`}
{` Size is ${new Intl.NumberFormat("en-US").format(
account.space
)} byte(s).`}
{account.space !== undefined &&
` Size is ${new Intl.NumberFormat("en-US").format(
account.space
)} byte(s).`}
</span>
);
}

View File

@ -23,6 +23,7 @@ import { SimulatorCard } from "./SimulatorCard";
import { MIN_MESSAGE_LENGTH, RawInput } from "./RawInputCard";
import { InstructionsSection } from "./InstructionsSection";
import base58 from "bs58";
import { useFetchAccountInfo } from "providers/accounts";
export type TransactionData = {
rawMessage: Uint8Array;
@ -269,6 +270,13 @@ function LoadedView({
}) {
const { message, rawMessage, signatures } = transaction;
const fetchAccountInfo = useFetchAccountInfo();
React.useEffect(() => {
for (let lookup of message.addressTableLookups) {
fetchAccountInfo(lookup.accountKey, "parsed");
}
}, [message, fetchAccountInfo]);
return (
<>
<OverviewCard message={message} raw={rawMessage} onClear={onClear} />

View File

@ -7,6 +7,7 @@ import {
AddressLookupTableAccount,
AddressLookupTableProgram,
SystemProgram,
ParsedAccountData,
} from "@solana/web3.js";
import { useCluster, Cluster } from "../cluster";
import { HistoryProvider } from "./history";
@ -109,242 +110,324 @@ export interface Account {
lamports: number;
executable: boolean;
owner: PublicKey;
space: number;
space?: number;
data: AccountData;
}
type State = Cache.State<Account>;
type Dispatch = Cache.Dispatch<Account>;
type Fetchers = { [mode in FetchAccountDataMode]: MultipleAccountFetcher };
const FetchersContext = React.createContext<Fetchers | undefined>(undefined);
const StateContext = React.createContext<State | undefined>(undefined);
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
class MultipleAccountFetcher {
pubkeys: PublicKey[] = [];
fetchTimeout?: NodeJS.Timeout;
constructor(
private dispatch: Dispatch,
private cluster: Cluster,
private url: string,
private dataMode: FetchAccountDataMode
) {}
fetch = (pubkey: PublicKey) => {
if (this.pubkeys !== undefined) this.pubkeys.push(pubkey);
if (this.fetchTimeout === undefined) {
this.fetchTimeout = setTimeout(() => {
this.fetchTimeout = undefined;
if (this.pubkeys !== undefined) {
const pubkeys = this.pubkeys;
this.pubkeys = [];
const { dispatch, cluster, url, dataMode } = this;
fetchMultipleAccounts({ dispatch, pubkeys, cluster, url, dataMode });
}
}, 100);
}
};
}
export type FetchAccountDataMode = "parsed" | "raw" | "skip";
type AccountsProviderProps = { children: React.ReactNode };
export function AccountsProvider({ children }: AccountsProviderProps) {
const { url } = useCluster();
const { cluster, url } = useCluster();
const [state, dispatch] = Cache.useReducer<Account>(url);
const [fetchers, setFetchers] = React.useState<Fetchers>(() => ({
skip: new MultipleAccountFetcher(dispatch, cluster, url, "skip"),
raw: new MultipleAccountFetcher(dispatch, cluster, url, "raw"),
parsed: new MultipleAccountFetcher(dispatch, cluster, url, "parsed"),
}));
// Clear accounts cache whenever cluster is changed
React.useEffect(() => {
dispatch({ type: ActionType.Clear, url });
}, [dispatch, url]);
setFetchers({
skip: new MultipleAccountFetcher(dispatch, cluster, url, "skip"),
raw: new MultipleAccountFetcher(dispatch, cluster, url, "raw"),
parsed: new MultipleAccountFetcher(dispatch, cluster, url, "parsed"),
});
}, [dispatch, cluster, url]);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
<TokensProvider>
<HistoryProvider>
<RewardsProvider>
<FlaggedAccountsProvider>{children}</FlaggedAccountsProvider>
</RewardsProvider>
</HistoryProvider>
</TokensProvider>
<FetchersContext.Provider value={fetchers}>
<TokensProvider>
<HistoryProvider>
<RewardsProvider>
<FlaggedAccountsProvider>{children}</FlaggedAccountsProvider>
</RewardsProvider>
</HistoryProvider>
</TokensProvider>
</FetchersContext.Provider>
</DispatchContext.Provider>
</StateContext.Provider>
);
}
async function fetchAccountInfo(
dispatch: Dispatch,
pubkey: PublicKey,
cluster: Cluster,
url: string
) {
dispatch({
type: ActionType.Update,
key: pubkey.toBase58(),
status: Cache.FetchStatus.Fetching,
url,
});
async function fetchMultipleAccounts({
dispatch,
pubkeys,
dataMode,
cluster,
url,
}: {
dispatch: Dispatch;
pubkeys: PublicKey[];
dataMode: FetchAccountDataMode;
cluster: Cluster;
url: string;
}) {
for (let pubkey of pubkeys) {
dispatch({
type: ActionType.Update,
key: pubkey.toBase58(),
status: Cache.FetchStatus.Fetching,
url,
});
}
let data;
let fetchStatus;
try {
const connection = new Connection(url, "confirmed");
const result = (await connection.getParsedAccountInfo(pubkey)).value;
const BATCH_SIZE = 100;
const connection = new Connection(url, "confirmed");
let account: Account;
if (result === null) {
account = {
pubkey,
lamports: 0,
owner: SystemProgram.programId,
space: 0,
executable: false,
data: { raw: Buffer.alloc(0) },
};
} else {
// Only save data in memory if we can decode it
let space: number;
if (!("parsed" in result.data)) {
space = result.data.length;
let nextBatchStart = 0;
while (nextBatchStart < pubkeys.length) {
const batch = pubkeys.slice(nextBatchStart, nextBatchStart + BATCH_SIZE);
nextBatchStart += BATCH_SIZE;
try {
let results;
if (dataMode === "parsed") {
results = (await connection.getMultipleParsedAccounts(batch)).value;
} else if (dataMode === "raw") {
results = await connection.getMultipleAccountsInfo(batch);
} else {
space = result.data.space;
results = await connection.getMultipleAccountsInfo(batch, {
dataSlice: { length: 0, offset: 0 },
});
}
let parsedData: ParsedData | undefined;
if ("parsed" in result.data) {
try {
const info = create(result.data.parsed, ParsedInfo);
switch (result.data.program) {
case "bpf-upgradeable-loader": {
const parsed = create(info, UpgradeableLoaderAccount);
for (let i = 0; i < batch.length; i++) {
const pubkey = batch[i];
const result = results[i];
// Fetch program data to get program upgradeability info
let programData: ProgramDataAccountInfo | undefined;
if (parsed.type === "program") {
const result = (
await connection.getParsedAccountInfo(parsed.info.programData)
).value;
if (
result &&
"parsed" in result.data &&
result.data.program === "bpf-upgradeable-loader"
) {
const info = create(result.data.parsed, ParsedInfo);
programData = create(info, ProgramDataAccount).info;
}
}
parsedData = {
program: result.data.program,
parsed,
programData,
};
break;
let account: Account;
if (result === null) {
account = {
pubkey,
lamports: 0,
owner: SystemProgram.programId,
space: 0,
executable: false,
data: { raw: Buffer.alloc(0) },
};
} else {
let space: number | undefined = undefined;
let parsedData: ParsedData | undefined;
if ("parsed" in result.data) {
const accountData: ParsedAccountData = result.data;
space = result.data.space;
try {
parsedData = await handleParsedAccountData(
connection,
pubkey,
accountData
);
} catch (error) {
reportError(error, { url, address: pubkey.toBase58() });
}
case "stake": {
const parsed = create(info, StakeAccount);
const isDelegated = parsed.type === "delegated";
const activation = isDelegated
? await connection.getStakeActivation(pubkey)
: undefined;
parsedData = {
program: result.data.program,
parsed,
activation,
};
break;
}
case "vote":
parsedData = {
program: result.data.program,
parsed: create(info, VoteAccount),
};
break;
case "nonce":
parsedData = {
program: result.data.program,
parsed: create(info, NonceAccount),
};
break;
case "sysvar":
parsedData = {
program: result.data.program,
parsed: create(info, SysvarAccount),
};
break;
case "config":
parsedData = {
program: result.data.program,
parsed: create(info, ConfigAccount),
};
break;
case "address-lookup-table": {
const parsed = create(info, ParsedAddressLookupTableAccount);
parsedData = {
program: result.data.program,
parsed,
};
break;
}
case "spl-token":
const parsed = create(info, TokenAccount);
let nftData;
try {
// Generate a PDA and check for a Metadata Account
if (parsed.type === "mint") {
const metadata = await Metadata.load(
connection,
await Metadata.getPDA(pubkey)
);
if (metadata) {
// We have a valid Metadata account. Try and pull edition data.
const editionInfo = await getEditionInfo(
metadata,
connection
);
const id = pubkeyToString(pubkey);
const metadataJSON = await getMetaDataJSON(
id,
metadata.data
);
nftData = {
metadata: metadata.data,
json: metadataJSON,
editionInfo,
};
}
}
} catch (error) {
// unable to find NFT metadata account
}
parsedData = {
program: result.data.program,
parsed,
nftData,
};
break;
default:
parsedData = undefined;
}
} catch (error) {
reportError(error, { url, address: pubkey.toBase58() });
// If we cannot parse account layout as native spl account
// then keep raw data for other components to decode
let rawData: Buffer | undefined;
if (
!parsedData &&
!("parsed" in result.data) &&
dataMode !== "skip"
) {
space = result.data.length;
rawData = result.data;
}
account = {
pubkey,
lamports: result.lamports,
executable: result.executable,
owner: result.owner,
space,
data: {
parsed: parsedData,
raw: rawData,
},
};
}
dispatch({
type: ActionType.Update,
status: FetchStatus.Fetched,
data: account,
key: pubkey.toBase58(),
url,
});
}
} catch (error) {
if (cluster !== Cluster.Custom) {
reportError(error, { url });
}
for (let pubkey of batch) {
dispatch({
type: ActionType.Update,
status: FetchStatus.FetchFailed,
key: pubkey.toBase58(),
url,
});
}
}
}
}
async function handleParsedAccountData(
connection: Connection,
accountKey: PublicKey,
accountData: ParsedAccountData
): Promise<ParsedData | undefined> {
const info = create(accountData.parsed, ParsedInfo);
switch (accountData.program) {
case "bpf-upgradeable-loader": {
const parsed = create(info, UpgradeableLoaderAccount);
// Fetch program data to get program upgradeability info
let programData: ProgramDataAccountInfo | undefined;
if (parsed.type === "program") {
const result = (
await connection.getParsedAccountInfo(parsed.info.programData)
).value;
if (
result &&
"parsed" in result.data &&
result.data.program === "bpf-upgradeable-loader"
) {
const info = create(result.data.parsed, ParsedInfo);
programData = create(info, ProgramDataAccount).info;
}
}
// If we cannot parse account layout as native spl account
// then keep raw data for other components to decode
let rawData: Buffer | undefined;
if (!parsedData && !("parsed" in result.data)) {
rawData = result.data;
}
account = {
pubkey,
lamports: result.lamports,
space,
executable: result.executable,
owner: result.owner,
data: {
parsed: parsedData,
raw: rawData,
},
return {
program: accountData.program,
parsed,
programData,
};
}
data = account;
fetchStatus = FetchStatus.Fetched;
} catch (error) {
if (cluster !== Cluster.Custom) {
reportError(error, { url });
case "stake": {
const parsed = create(info, StakeAccount);
const isDelegated = parsed.type === "delegated";
const activation = isDelegated
? await connection.getStakeActivation(accountKey)
: undefined;
return {
program: accountData.program,
parsed,
activation,
};
}
case "vote": {
return {
program: accountData.program,
parsed: create(info, VoteAccount),
};
}
case "nonce": {
return {
program: accountData.program,
parsed: create(info, NonceAccount),
};
}
case "sysvar": {
return {
program: accountData.program,
parsed: create(info, SysvarAccount),
};
}
case "config": {
return {
program: accountData.program,
parsed: create(info, ConfigAccount),
};
}
case "address-lookup-table": {
const parsed = create(info, ParsedAddressLookupTableAccount);
return {
program: accountData.program,
parsed,
};
}
case "spl-token": {
const parsed = create(info, TokenAccount);
let nftData;
try {
// Generate a PDA and check for a Metadata Account
if (parsed.type === "mint") {
const metadata = await Metadata.load(
connection,
await Metadata.getPDA(accountKey)
);
if (metadata) {
// We have a valid Metadata account. Try and pull edition data.
const editionInfo = await getEditionInfo(metadata, connection);
const id = pubkeyToString(accountKey);
const metadataJSON = await getMetaDataJSON(id, metadata.data);
nftData = {
metadata: metadata.data,
json: metadataJSON,
editionInfo,
};
}
}
} catch (error) {
// unable to find NFT metadata account
}
return {
program: accountData.program,
parsed,
nftData,
};
}
fetchStatus = FetchStatus.FetchFailed;
}
dispatch({
type: ActionType.Update,
status: fetchStatus,
data,
key: pubkey.toBase58(),
url,
});
}
const IMAGE_MIME_TYPE_REGEX = /data:image\/(svg\+xml|png|jpeg|gif)/g;
@ -447,72 +530,83 @@ export function useTokenAccountInfo(
address: string | undefined
): TokenAccountInfo | undefined {
const accountInfo = useAccountInfo(address);
if (address === undefined || accountInfo?.data === undefined) return;
const account = accountInfo.data;
return React.useMemo(() => {
if (address === undefined || accountInfo?.data === undefined) return;
const account = accountInfo.data;
try {
const parsedData = account.data.parsed;
if (!parsedData) return;
if (
parsedData.program !== "spl-token" ||
parsedData.parsed.type !== "account"
) {
return;
try {
const parsedData = account.data.parsed;
if (!parsedData) return;
if (
parsedData.program !== "spl-token" ||
parsedData.parsed.type !== "account"
) {
return;
}
return create(parsedData.parsed.info, TokenAccountInfo);
} catch (err) {
reportError(err, { address });
}
return create(parsedData.parsed.info, TokenAccountInfo);
} catch (err) {
reportError(err, { address });
}
}, [address, accountInfo]);
}
export function useAddressLookupTable(
address: string | undefined
): AddressLookupTableAccount | undefined | string {
address: string
): [AddressLookupTableAccount | string | undefined, FetchStatus] | undefined {
const accountInfo = useAccountInfo(address);
if (address === undefined || accountInfo?.data === undefined) return;
const account = accountInfo.data;
if (account.lamports === 0) return "Lookup Table Not Found";
const { parsed: parsedData, raw: rawData } = account.data;
return React.useMemo(() => {
if (accountInfo === undefined) return;
const account = accountInfo.data;
if (account === undefined) return [account, accountInfo.status];
if (account.lamports === 0)
return ["Lookup Table Not Found", accountInfo.status];
const { parsed: parsedData, raw: rawData } = account.data;
const key = new PublicKey(address);
if (parsedData && parsedData.program === "address-lookup-table") {
if (parsedData.parsed.type === "lookupTable") {
return new AddressLookupTableAccount({
key,
state: parsedData.parsed.info,
});
} else if (parsedData.parsed.type === "uninitialized") {
return "Lookup Table Uninitialized";
const key = new PublicKey(address);
if (parsedData && parsedData.program === "address-lookup-table") {
if (parsedData.parsed.type === "lookupTable") {
return [
new AddressLookupTableAccount({
key,
state: parsedData.parsed.info,
}),
accountInfo.status,
];
} else if (parsedData.parsed.type === "uninitialized") {
return ["Lookup Table Uninitialized", accountInfo.status];
}
} else if (
rawData &&
account.owner.equals(AddressLookupTableProgram.programId)
) {
try {
return [
new AddressLookupTableAccount({
key,
state: AddressLookupTableAccount.deserialize(rawData),
}),
accountInfo.status,
];
} catch {}
}
} else if (
rawData &&
account.owner.equals(AddressLookupTableProgram.programId)
) {
try {
return new AddressLookupTableAccount({
key,
state: AddressLookupTableAccount.deserialize(rawData),
});
} catch {}
}
return "Invalid Lookup Table";
return ["Invalid Lookup Table", accountInfo.status];
}, [address, accountInfo]);
}
export function useFetchAccountInfo() {
const dispatch = React.useContext(DispatchContext);
if (!dispatch) {
const fetchers = React.useContext(FetchersContext);
if (!fetchers) {
throw new Error(
`useFetchAccountInfo must be used within a AccountsProvider`
);
}
const { cluster, url } = useCluster();
return React.useCallback(
(pubkey: PublicKey) => {
fetchAccountInfo(dispatch, pubkey, cluster, url);
(pubkey: PublicKey, dataMode: FetchAccountDataMode) => {
fetchers[dataMode].fetch(pubkey);
},
[dispatch, cluster, url]
[fetchers]
);
}