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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -96,13 +96,13 @@ function TokenInstruction(props: InfoProps) {
React.useEffect(() => { React.useEffect(() => {
if (tokenAddress && !tokenInfo) { if (tokenAddress && !tokenInfo) {
fetchAccountInfo(new PublicKey(tokenAddress)); fetchAccountInfo(new PublicKey(tokenAddress), "parsed");
} }
}, [fetchAccountInfo, tokenAddress]); // eslint-disable-line react-hooks/exhaustive-deps }, [fetchAccountInfo, tokenAddress]); // eslint-disable-line react-hooks/exhaustive-deps
React.useEffect(() => { React.useEffect(() => {
if (mintAddress && !mintInfo) { if (mintAddress && !mintInfo) {
fetchAccountInfo(new PublicKey(mintAddress)); fetchAccountInfo(new PublicKey(mintAddress), "parsed");
} }
}, [fetchAccountInfo, mintAddress]); // eslint-disable-line react-hooks/exhaustive-deps }, [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 // Fetch account on load
React.useEffect(() => { React.useEffect(() => {
if (!info && status === ClusterStatus.Connected && pubkey) { if (!info && status === ClusterStatus.Connected && pubkey) {
fetchAccount(pubkey); fetchAccount(pubkey, "parsed");
} }
}, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps }, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps
@ -303,7 +303,12 @@ function DetailsSections({
info.status === FetchStatus.FetchFailed || info.status === FetchStatus.FetchFailed ||
info.data?.lamports === undefined 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; const account = info.data;

View File

@ -1,7 +1,8 @@
import React from "react"; import React from "react";
import { PublicKey, VersionedMessage } from "@solana/web3.js"; import { PublicKey, VersionedMessage } from "@solana/web3.js";
import { Address } from "components/common/Address"; 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({ export function AddressTableLookupsCard({
message, message,
@ -80,31 +81,38 @@ function LookupRow({
lookupTableIndex: number; lookupTableIndex: number;
readOnly: boolean; readOnly: boolean;
}) { }) {
const lookupTable = useAddressLookupTable(lookupTableKey.toBase58()); const lookupTableInfo = useAddressLookupTable(lookupTableKey.toBase58());
const fetchAccountInfo = useFetchAccountInfo();
React.useEffect(() => { const loadingComponent = (
if (!lookupTable) fetchAccountInfo(lookupTableKey); <span className="text-muted">
}, [lookupTableKey, lookupTable, fetchAccountInfo]); <span className="spinner-grow spinner-grow-sm me-2"></span>
Loading
</span>
);
let resolvedKeyComponent; let resolvedKeyComponent;
if (!lookupTable) { if (!lookupTableInfo) {
resolvedKeyComponent = ( resolvedKeyComponent = loadingComponent;
<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 />;
} else { } else {
resolvedKeyComponent = ( const [lookupTable, status] = lookupTableInfo;
<span className="text-muted">Invalid Lookup Table Index</span> 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 ( return (

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import {
AddressLookupTableAccount, AddressLookupTableAccount,
AddressLookupTableProgram, AddressLookupTableProgram,
SystemProgram, SystemProgram,
ParsedAccountData,
} from "@solana/web3.js"; } from "@solana/web3.js";
import { useCluster, Cluster } from "../cluster"; import { useCluster, Cluster } from "../cluster";
import { HistoryProvider } from "./history"; import { HistoryProvider } from "./history";
@ -109,242 +110,324 @@ export interface Account {
lamports: number; lamports: number;
executable: boolean; executable: boolean;
owner: PublicKey; owner: PublicKey;
space: number; space?: number;
data: AccountData; data: AccountData;
} }
type State = Cache.State<Account>; type State = Cache.State<Account>;
type Dispatch = Cache.Dispatch<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 StateContext = React.createContext<State | undefined>(undefined);
const DispatchContext = React.createContext<Dispatch | 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 }; type AccountsProviderProps = { children: React.ReactNode };
export function AccountsProvider({ children }: AccountsProviderProps) { export function AccountsProvider({ children }: AccountsProviderProps) {
const { url } = useCluster(); const { cluster, url } = useCluster();
const [state, dispatch] = Cache.useReducer<Account>(url); 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 // Clear accounts cache whenever cluster is changed
React.useEffect(() => { React.useEffect(() => {
dispatch({ type: ActionType.Clear, url }); 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 ( return (
<StateContext.Provider value={state}> <StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}> <DispatchContext.Provider value={dispatch}>
<TokensProvider> <FetchersContext.Provider value={fetchers}>
<HistoryProvider> <TokensProvider>
<RewardsProvider> <HistoryProvider>
<FlaggedAccountsProvider>{children}</FlaggedAccountsProvider> <RewardsProvider>
</RewardsProvider> <FlaggedAccountsProvider>{children}</FlaggedAccountsProvider>
</HistoryProvider> </RewardsProvider>
</TokensProvider> </HistoryProvider>
</TokensProvider>
</FetchersContext.Provider>
</DispatchContext.Provider> </DispatchContext.Provider>
</StateContext.Provider> </StateContext.Provider>
); );
} }
async function fetchAccountInfo( async function fetchMultipleAccounts({
dispatch: Dispatch, dispatch,
pubkey: PublicKey, pubkeys,
cluster: Cluster, dataMode,
url: string cluster,
) { url,
dispatch({ }: {
type: ActionType.Update, dispatch: Dispatch;
key: pubkey.toBase58(), pubkeys: PublicKey[];
status: Cache.FetchStatus.Fetching, dataMode: FetchAccountDataMode;
url, cluster: Cluster;
}); url: string;
}) {
for (let pubkey of pubkeys) {
dispatch({
type: ActionType.Update,
key: pubkey.toBase58(),
status: Cache.FetchStatus.Fetching,
url,
});
}
let data; const BATCH_SIZE = 100;
let fetchStatus; const connection = new Connection(url, "confirmed");
try {
const connection = new Connection(url, "confirmed");
const result = (await connection.getParsedAccountInfo(pubkey)).value;
let account: Account; let nextBatchStart = 0;
if (result === null) { while (nextBatchStart < pubkeys.length) {
account = { const batch = pubkeys.slice(nextBatchStart, nextBatchStart + BATCH_SIZE);
pubkey, nextBatchStart += BATCH_SIZE;
lamports: 0,
owner: SystemProgram.programId, try {
space: 0, let results;
executable: false, if (dataMode === "parsed") {
data: { raw: Buffer.alloc(0) }, results = (await connection.getMultipleParsedAccounts(batch)).value;
}; } else if (dataMode === "raw") {
} else { results = await connection.getMultipleAccountsInfo(batch);
// Only save data in memory if we can decode it
let space: number;
if (!("parsed" in result.data)) {
space = result.data.length;
} else { } else {
space = result.data.space; results = await connection.getMultipleAccountsInfo(batch, {
dataSlice: { length: 0, offset: 0 },
});
} }
let parsedData: ParsedData | undefined; for (let i = 0; i < batch.length; i++) {
if ("parsed" in result.data) { const pubkey = batch[i];
try { const result = results[i];
const info = create(result.data.parsed, ParsedInfo);
switch (result.data.program) {
case "bpf-upgradeable-loader": {
const parsed = create(info, UpgradeableLoaderAccount);
// Fetch program data to get program upgradeability info let account: Account;
let programData: ProgramDataAccountInfo | undefined; if (result === null) {
if (parsed.type === "program") { account = {
const result = ( pubkey,
await connection.getParsedAccountInfo(parsed.info.programData) lamports: 0,
).value; owner: SystemProgram.programId,
if ( space: 0,
result && executable: false,
"parsed" in result.data && data: { raw: Buffer.alloc(0) },
result.data.program === "bpf-upgradeable-loader" };
) { } else {
const info = create(result.data.parsed, ParsedInfo); let space: number | undefined = undefined;
programData = create(info, ProgramDataAccount).info; let parsedData: ParsedData | undefined;
} if ("parsed" in result.data) {
} const accountData: ParsedAccountData = result.data;
space = result.data.space;
parsedData = { try {
program: result.data.program, parsedData = await handleParsedAccountData(
parsed, connection,
programData, pubkey,
}; accountData
);
break; } 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 return {
// then keep raw data for other components to decode program: accountData.program,
let rawData: Buffer | undefined; parsed,
if (!parsedData && !("parsed" in result.data)) { programData,
rawData = result.data;
}
account = {
pubkey,
lamports: result.lamports,
space,
executable: result.executable,
owner: result.owner,
data: {
parsed: parsedData,
raw: rawData,
},
}; };
} }
data = account;
fetchStatus = FetchStatus.Fetched; case "stake": {
} catch (error) { const parsed = create(info, StakeAccount);
if (cluster !== Cluster.Custom) { const isDelegated = parsed.type === "delegated";
reportError(error, { url }); 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; const IMAGE_MIME_TYPE_REGEX = /data:image\/(svg\+xml|png|jpeg|gif)/g;
@ -447,72 +530,83 @@ export function useTokenAccountInfo(
address: string | undefined address: string | undefined
): TokenAccountInfo | undefined { ): TokenAccountInfo | undefined {
const accountInfo = useAccountInfo(address); const accountInfo = useAccountInfo(address);
if (address === undefined || accountInfo?.data === undefined) return; return React.useMemo(() => {
const account = accountInfo.data; if (address === undefined || accountInfo?.data === undefined) return;
const account = accountInfo.data;
try { try {
const parsedData = account.data.parsed; const parsedData = account.data.parsed;
if (!parsedData) return; if (!parsedData) return;
if ( if (
parsedData.program !== "spl-token" || parsedData.program !== "spl-token" ||
parsedData.parsed.type !== "account" parsedData.parsed.type !== "account"
) { ) {
return; return;
}
return create(parsedData.parsed.info, TokenAccountInfo);
} catch (err) {
reportError(err, { address });
} }
}, [address, accountInfo]);
return create(parsedData.parsed.info, TokenAccountInfo);
} catch (err) {
reportError(err, { address });
}
} }
export function useAddressLookupTable( export function useAddressLookupTable(
address: string | undefined address: string
): AddressLookupTableAccount | undefined | string { ): [AddressLookupTableAccount | string | undefined, FetchStatus] | undefined {
const accountInfo = useAccountInfo(address); const accountInfo = useAccountInfo(address);
if (address === undefined || accountInfo?.data === undefined) return; return React.useMemo(() => {
const account = accountInfo.data; if (accountInfo === undefined) return;
if (account.lamports === 0) return "Lookup Table Not Found"; const account = accountInfo.data;
const { parsed: parsedData, raw: rawData } = account.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); const key = new PublicKey(address);
if (parsedData && parsedData.program === "address-lookup-table") { if (parsedData && parsedData.program === "address-lookup-table") {
if (parsedData.parsed.type === "lookupTable") { if (parsedData.parsed.type === "lookupTable") {
return new AddressLookupTableAccount({ return [
key, new AddressLookupTableAccount({
state: parsedData.parsed.info, key,
}); state: parsedData.parsed.info,
} else if (parsedData.parsed.type === "uninitialized") { }),
return "Lookup Table Uninitialized"; 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() { export function useFetchAccountInfo() {
const dispatch = React.useContext(DispatchContext); const fetchers = React.useContext(FetchersContext);
if (!dispatch) { if (!fetchers) {
throw new Error( throw new Error(
`useFetchAccountInfo must be used within a AccountsProvider` `useFetchAccountInfo must be used within a AccountsProvider`
); );
} }
const { cluster, url } = useCluster();
return React.useCallback( return React.useCallback(
(pubkey: PublicKey) => { (pubkey: PublicKey, dataMode: FetchAccountDataMode) => {
fetchAccountInfo(dispatch, pubkey, cluster, url); fetchers[dataMode].fetch(pubkey);
}, },
[dispatch, cluster, url] [fetchers]
); );
} }