Explorer: Add Anchor Decoding to Programs/Accounts/Transactions (#23972)
* Add program idl to the Program page * Add instruction decoding to the Tx page * Add account decoding to the Account page
This commit is contained in:
165
explorer/package-lock.json
generated
165
explorer/package-lock.json
generated
@ -14,7 +14,7 @@
|
|||||||
"@cloudflare/stream-react": "^1.2.0",
|
"@cloudflare/stream-react": "^1.2.0",
|
||||||
"@metamask/jazzicon": "^2.0.0",
|
"@metamask/jazzicon": "^2.0.0",
|
||||||
"@metaplex/js": "4.12.0",
|
"@metaplex/js": "4.12.0",
|
||||||
"@project-serum/anchor": "^0.22.1",
|
"@project-serum/anchor": "0.23.0",
|
||||||
"@project-serum/serum": "^0.13.61",
|
"@project-serum/serum": "^0.13.61",
|
||||||
"@react-hook/debounce": "^4.0.0",
|
"@react-hook/debounce": "^4.0.0",
|
||||||
"@sentry/react": "^6.16.1",
|
"@sentry/react": "^6.16.1",
|
||||||
@ -4490,12 +4490,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@project-serum/anchor": {
|
"node_modules/@project-serum/anchor": {
|
||||||
"version": "0.22.1",
|
"version": "0.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.23.0.tgz",
|
||||||
"integrity": "sha512-5pHeyvQhzLahIQ8aZymmDMZJAJFklN0joZdI+YIqFkK2uU/mlKr6rBLQjxysf/j1mLLiNG00tdyLfUtTAdQz7w==",
|
"integrity": "sha512-LV2/ifZOJVFTZ4GbEloXln3iVfCvO1YM8i7BBCrUm4tehP7irMx4nr4/IabHWOzrQcQElsxSP/lb1tBp+2ff8A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@project-serum/borsh": "^0.2.5",
|
"@project-serum/borsh": "^0.2.5",
|
||||||
"@solana/web3.js": "^1.17.0",
|
"@solana/web3.js": "^1.36.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"bn.js": "^5.1.2",
|
"bn.js": "^5.1.2",
|
||||||
"bs58": "^4.0.1",
|
"bs58": "^4.0.1",
|
||||||
@ -4514,11 +4514,95 @@
|
|||||||
"node": ">=11"
|
"node": ">=11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@project-serum/anchor/node_modules/@babel/runtime": {
|
||||||
|
"version": "7.17.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz",
|
||||||
|
"integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==",
|
||||||
|
"dependencies": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@project-serum/anchor/node_modules/@solana/buffer-layout": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-lR0EMP2HC3+Mxwd4YcnZb0smnaDw7Bl2IQWZiTevRH5ZZBZn6VRWn3/92E3qdU4SSImJkA6IDHawOHAnx/qUvQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "~6.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=5.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@project-serum/anchor/node_modules/@solana/buffer-layout/node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@project-serum/anchor/node_modules/@solana/web3.js": {
|
||||||
|
"version": "1.37.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.37.0.tgz",
|
||||||
|
"integrity": "sha512-O2iCcgkGdi2FXwVLztPIZHcBuZXdhbVLavMsG+RdEyFGzFD0tQN1rOJ+Xb5eaexjqtgcqRN+Fyg3wAhLcHJbiA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@ethersproject/sha2": "^5.5.0",
|
||||||
|
"@solana/buffer-layout": "^4.0.0",
|
||||||
|
"bn.js": "^5.0.0",
|
||||||
|
"borsh": "^0.7.0",
|
||||||
|
"bs58": "^4.0.1",
|
||||||
|
"buffer": "6.0.1",
|
||||||
|
"cross-fetch": "^3.1.4",
|
||||||
|
"jayson": "^3.4.4",
|
||||||
|
"js-sha3": "^0.8.0",
|
||||||
|
"rpc-websockets": "^7.4.2",
|
||||||
|
"secp256k1": "^4.0.2",
|
||||||
|
"superstruct": "^0.14.2",
|
||||||
|
"tweetnacl": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@project-serum/anchor/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/@project-serum/anchor/node_modules/pako": {
|
"node_modules/@project-serum/anchor/node_modules/pako": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.3.tgz",
|
||||||
"integrity": "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw=="
|
"integrity": "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@project-serum/anchor/node_modules/superstruct": {
|
||||||
|
"version": "0.14.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
|
||||||
|
"integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ=="
|
||||||
|
},
|
||||||
"node_modules/@project-serum/borsh": {
|
"node_modules/@project-serum/borsh": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@project-serum/borsh/-/borsh-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@project-serum/borsh/-/borsh-0.2.5.tgz",
|
||||||
@ -30637,12 +30721,12 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"@project-serum/anchor": {
|
"@project-serum/anchor": {
|
||||||
"version": "0.22.1",
|
"version": "0.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.23.0.tgz",
|
||||||
"integrity": "sha512-5pHeyvQhzLahIQ8aZymmDMZJAJFklN0joZdI+YIqFkK2uU/mlKr6rBLQjxysf/j1mLLiNG00tdyLfUtTAdQz7w==",
|
"integrity": "sha512-LV2/ifZOJVFTZ4GbEloXln3iVfCvO1YM8i7BBCrUm4tehP7irMx4nr4/IabHWOzrQcQElsxSP/lb1tBp+2ff8A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@project-serum/borsh": "^0.2.5",
|
"@project-serum/borsh": "^0.2.5",
|
||||||
"@solana/web3.js": "^1.17.0",
|
"@solana/web3.js": "^1.36.0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"bn.js": "^5.1.2",
|
"bn.js": "^5.1.2",
|
||||||
"bs58": "^4.0.1",
|
"bs58": "^4.0.1",
|
||||||
@ -30658,10 +30742,73 @@
|
|||||||
"toml": "^3.0.0"
|
"toml": "^3.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/runtime": {
|
||||||
|
"version": "7.17.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz",
|
||||||
|
"integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==",
|
||||||
|
"requires": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@solana/buffer-layout": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-lR0EMP2HC3+Mxwd4YcnZb0smnaDw7Bl2IQWZiTevRH5ZZBZn6VRWn3/92E3qdU4SSImJkA6IDHawOHAnx/qUvQ==",
|
||||||
|
"requires": {
|
||||||
|
"buffer": "~6.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"requires": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@solana/web3.js": {
|
||||||
|
"version": "1.37.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.37.0.tgz",
|
||||||
|
"integrity": "sha512-O2iCcgkGdi2FXwVLztPIZHcBuZXdhbVLavMsG+RdEyFGzFD0tQN1rOJ+Xb5eaexjqtgcqRN+Fyg3wAhLcHJbiA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@ethersproject/sha2": "^5.5.0",
|
||||||
|
"@solana/buffer-layout": "^4.0.0",
|
||||||
|
"bn.js": "^5.0.0",
|
||||||
|
"borsh": "^0.7.0",
|
||||||
|
"bs58": "^4.0.1",
|
||||||
|
"buffer": "6.0.1",
|
||||||
|
"cross-fetch": "^3.1.4",
|
||||||
|
"jayson": "^3.4.4",
|
||||||
|
"js-sha3": "^0.8.0",
|
||||||
|
"rpc-websockets": "^7.4.2",
|
||||||
|
"secp256k1": "^4.0.2",
|
||||||
|
"superstruct": "^0.14.2",
|
||||||
|
"tweetnacl": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
"pako": {
|
"pako": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.3.tgz",
|
||||||
"integrity": "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw=="
|
"integrity": "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw=="
|
||||||
|
},
|
||||||
|
"superstruct": {
|
||||||
|
"version": "0.14.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
|
||||||
|
"integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"@cloudflare/stream-react": "^1.2.0",
|
"@cloudflare/stream-react": "^1.2.0",
|
||||||
"@metamask/jazzicon": "^2.0.0",
|
"@metamask/jazzicon": "^2.0.0",
|
||||||
"@metaplex/js": "4.12.0",
|
"@metaplex/js": "4.12.0",
|
||||||
"@project-serum/anchor": "^0.22.1",
|
"@project-serum/anchor": "0.23.0",
|
||||||
"@project-serum/serum": "^0.13.61",
|
"@project-serum/serum": "^0.13.61",
|
||||||
"@react-hook/debounce": "^4.0.0",
|
"@react-hook/debounce": "^4.0.0",
|
||||||
"@sentry/react": "^6.16.1",
|
"@sentry/react": "^6.16.1",
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import React from "react";
|
|
||||||
import { Message, ParsedMessage } from "@solana/web3.js";
|
import { Message, ParsedMessage } from "@solana/web3.js";
|
||||||
import { Cluster } from "providers/cluster";
|
import { Cluster } from "providers/cluster";
|
||||||
import { TableCardBody } from "components/common/TableCardBody";
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
import { programLabel } from "utils/tx";
|
|
||||||
import { InstructionLogs } from "utils/program-logs";
|
import { InstructionLogs } from "utils/program-logs";
|
||||||
|
import { ProgramName } from "utils/anchor";
|
||||||
|
|
||||||
export function ProgramLogsCardBody({
|
export function ProgramLogsCardBody({
|
||||||
message,
|
message,
|
||||||
logs,
|
logs,
|
||||||
cluster,
|
cluster,
|
||||||
|
url,
|
||||||
}: {
|
}: {
|
||||||
message: Message | ParsedMessage;
|
message: Message | ParsedMessage;
|
||||||
logs: InstructionLogs[];
|
logs: InstructionLogs[];
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
|
url: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TableCardBody>
|
<TableCardBody>
|
||||||
@ -28,9 +29,6 @@ export function ProgramLogsCardBody({
|
|||||||
} else {
|
} else {
|
||||||
programId = ix.programId;
|
programId = ix.programId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const programName =
|
|
||||||
programLabel(programId.toBase58(), cluster) || "Unknown Program";
|
|
||||||
const programLogs: InstructionLogs | undefined = logs[index];
|
const programLogs: InstructionLogs | undefined = logs[index];
|
||||||
|
|
||||||
let badgeColor = "white";
|
let badgeColor = "white";
|
||||||
@ -45,7 +43,12 @@ export function ProgramLogsCardBody({
|
|||||||
<span className={`badge bg-${badgeColor}-soft me-2`}>
|
<span className={`badge bg-${badgeColor}-soft me-2`}>
|
||||||
#{index + 1}
|
#{index + 1}
|
||||||
</span>
|
</span>
|
||||||
{programName} Instruction
|
<ProgramName
|
||||||
|
programId={programId}
|
||||||
|
cluster={cluster}
|
||||||
|
url={url}
|
||||||
|
/>{" "}
|
||||||
|
Instruction
|
||||||
</div>
|
</div>
|
||||||
{programLogs && (
|
{programLogs && (
|
||||||
<div className="d-flex align-items-start flex-column font-monospace p-2 font-size-sm">
|
<div className="d-flex align-items-start flex-column font-monospace p-2 font-size-sm">
|
||||||
|
157
explorer/src/components/account/AnchorAccountCard.tsx
Normal file
157
explorer/src/components/account/AnchorAccountCard.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
|
import { Account } from "providers/accounts";
|
||||||
|
import { Address } from "components/common/Address";
|
||||||
|
import { BorshAccountsCoder } from "@project-serum/anchor";
|
||||||
|
import { capitalizeFirstLetter } from "utils/anchor";
|
||||||
|
import { ErrorCard } from "components/common/ErrorCard";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import BN from "bn.js";
|
||||||
|
|
||||||
|
import ReactJson from "react-json-view";
|
||||||
|
import { useCluster } from "providers/cluster";
|
||||||
|
import { useAnchorProgram } from "providers/anchor";
|
||||||
|
|
||||||
|
export function AnchorAccountCard({ account }: { account: Account }) {
|
||||||
|
const { url } = useCluster();
|
||||||
|
const program = useAnchorProgram(
|
||||||
|
account.details?.owner.toString() ?? "",
|
||||||
|
url
|
||||||
|
);
|
||||||
|
|
||||||
|
const { foundAccountLayoutName, decodedAnchorAccountData } = useMemo(() => {
|
||||||
|
let foundAccountLayoutName: string | undefined;
|
||||||
|
let decodedAnchorAccountData: { [key: string]: any } | undefined;
|
||||||
|
if (program && account.details && account.details.rawData) {
|
||||||
|
const accountBuffer = account.details.rawData;
|
||||||
|
const discriminator = accountBuffer.slice(0, 8);
|
||||||
|
|
||||||
|
// Iterate all the structs, see if any of the name-hashes match
|
||||||
|
Object.keys(program.account).forEach((accountType) => {
|
||||||
|
const layoutName = capitalizeFirstLetter(accountType);
|
||||||
|
const discriminatorToCheck =
|
||||||
|
BorshAccountsCoder.accountDiscriminator(layoutName);
|
||||||
|
|
||||||
|
if (discriminatorToCheck.equals(discriminator)) {
|
||||||
|
foundAccountLayoutName = layoutName;
|
||||||
|
const accountDecoder = program.account[accountType];
|
||||||
|
decodedAnchorAccountData = accountDecoder.coder.accounts.decode(
|
||||||
|
layoutName,
|
||||||
|
accountBuffer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { foundAccountLayoutName, decodedAnchorAccountData };
|
||||||
|
}, [program, account.details]);
|
||||||
|
|
||||||
|
if (!foundAccountLayoutName || !decodedAnchorAccountData) {
|
||||||
|
return (
|
||||||
|
<ErrorCard text="Failed to decode account data according to its public anchor interface" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<h3 className="card-header-title">{foundAccountLayoutName}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="table-responsive mb-0">
|
||||||
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="w-1 text-muted">Key</th>
|
||||||
|
<th className="text-muted">Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="list">
|
||||||
|
{decodedAnchorAccountData &&
|
||||||
|
Object.keys(decodedAnchorAccountData).map((key) => (
|
||||||
|
<AccountRow
|
||||||
|
key={key}
|
||||||
|
valueName={key}
|
||||||
|
value={decodedAnchorAccountData[key]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="card-footer">
|
||||||
|
<div className="text-muted text-center">
|
||||||
|
{decodedAnchorAccountData &&
|
||||||
|
Object.keys(decodedAnchorAccountData).length > 0
|
||||||
|
? `Decoded ${Object.keys(decodedAnchorAccountData).length} Items`
|
||||||
|
: "No decoded data"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccountRow({ valueName, value }: { valueName: string; value: any }) {
|
||||||
|
let displayValue: JSX.Element;
|
||||||
|
if (value instanceof PublicKey) {
|
||||||
|
displayValue = <Address pubkey={value} link />;
|
||||||
|
} else if (value instanceof BN) {
|
||||||
|
displayValue = <>{value.toString()}</>;
|
||||||
|
} else if (!(value instanceof Object)) {
|
||||||
|
displayValue = <>{String(value)}</>;
|
||||||
|
} else if (value) {
|
||||||
|
const displayObject = stringifyPubkeyAndBigNums(value);
|
||||||
|
displayValue = (
|
||||||
|
<ReactJson
|
||||||
|
src={JSON.parse(JSON.stringify(displayObject))}
|
||||||
|
collapsed={1}
|
||||||
|
theme="solarized"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
displayValue = <>null</>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td className="w-1 text-monospace">{camelToUnderscore(valueName)}</td>
|
||||||
|
<td className="text-monospace">{displayValue}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function camelToUnderscore(key: string) {
|
||||||
|
var result = key.replace(/([A-Z])/g, " $1");
|
||||||
|
return result.split(" ").join("_").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyPubkeyAndBigNums(object: Object): Object {
|
||||||
|
if (!Array.isArray(object)) {
|
||||||
|
if (object instanceof PublicKey) {
|
||||||
|
return object.toString();
|
||||||
|
} else if (object instanceof BN) {
|
||||||
|
return object.toString();
|
||||||
|
} else if (!(object instanceof Object)) {
|
||||||
|
return object;
|
||||||
|
} else {
|
||||||
|
const parsedObject: { [key: string]: Object } = {};
|
||||||
|
Object.keys(object).map((key) => {
|
||||||
|
let value = (object as { [key: string]: any })[key];
|
||||||
|
if (value instanceof Object) {
|
||||||
|
value = stringifyPubkeyAndBigNums(value);
|
||||||
|
}
|
||||||
|
parsedObject[key] = value;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return parsedObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object.map((innerObject) =>
|
||||||
|
innerObject instanceof Object
|
||||||
|
? stringifyPubkeyAndBigNums(innerObject)
|
||||||
|
: innerObject
|
||||||
|
);
|
||||||
|
}
|
36
explorer/src/components/account/AnchorProgramCard.tsx
Normal file
36
explorer/src/components/account/AnchorProgramCard.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { useAnchorProgram } from "providers/anchor";
|
||||||
|
import { useCluster } from "providers/cluster";
|
||||||
|
import ReactJson from "react-json-view";
|
||||||
|
|
||||||
|
export function AnchorProgramCard({ programId }: { programId: PublicKey }) {
|
||||||
|
const { url } = useCluster();
|
||||||
|
const program = useAnchorProgram(programId.toString(), url);
|
||||||
|
|
||||||
|
if (!program) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<h3 className="card-header-title">Anchor IDL</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card metadata-json-viewer m-4">
|
||||||
|
<ReactJson
|
||||||
|
src={program.idl}
|
||||||
|
theme={"solarized"}
|
||||||
|
style={{ padding: 25 }}
|
||||||
|
collapsed={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
101
explorer/src/components/instruction/AnchorDetailsCard.tsx
Normal file
101
explorer/src/components/instruction/AnchorDetailsCard.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
|
||||||
|
import { InstructionCard } from "./InstructionCard";
|
||||||
|
import { Idl, Program, BorshInstructionCoder } from "@project-serum/anchor";
|
||||||
|
import {
|
||||||
|
getAnchorNameForInstruction,
|
||||||
|
getProgramName,
|
||||||
|
capitalizeFirstLetter,
|
||||||
|
getAnchorAccountsFromInstruction,
|
||||||
|
} from "utils/anchor";
|
||||||
|
import { HexData } from "components/common/HexData";
|
||||||
|
import { Address } from "components/common/Address";
|
||||||
|
import ReactJson from "react-json-view";
|
||||||
|
|
||||||
|
export default function AnchorDetailsCard(props: {
|
||||||
|
key: string;
|
||||||
|
ix: TransactionInstruction;
|
||||||
|
index: number;
|
||||||
|
result: SignatureResult;
|
||||||
|
signature: string;
|
||||||
|
innerCards?: JSX.Element[];
|
||||||
|
childIndex?: number;
|
||||||
|
anchorProgram: Program<Idl>;
|
||||||
|
}) {
|
||||||
|
const { ix, anchorProgram } = props;
|
||||||
|
const programName = getProgramName(anchorProgram) ?? "Unknown Program";
|
||||||
|
|
||||||
|
const ixName =
|
||||||
|
getAnchorNameForInstruction(ix, anchorProgram) ?? "Unknown Instruction";
|
||||||
|
const cardTitle = `${programName}: ${ixName}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InstructionCard title={cardTitle} {...props}>
|
||||||
|
<RawAnchorDetails ix={ix} anchorProgram={anchorProgram} />
|
||||||
|
</InstructionCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RawAnchorDetails({
|
||||||
|
ix,
|
||||||
|
anchorProgram,
|
||||||
|
}: {
|
||||||
|
ix: TransactionInstruction;
|
||||||
|
anchorProgram: Program;
|
||||||
|
}) {
|
||||||
|
let ixAccounts:
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
isMut: boolean;
|
||||||
|
isSigner: boolean;
|
||||||
|
pda?: Object;
|
||||||
|
}[]
|
||||||
|
| null = null;
|
||||||
|
var decodedIxData = null;
|
||||||
|
if (anchorProgram) {
|
||||||
|
const decoder = new BorshInstructionCoder(anchorProgram.idl);
|
||||||
|
decodedIxData = decoder.decode(ix.data);
|
||||||
|
ixAccounts = getAnchorAccountsFromInstruction(decodedIxData, anchorProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ix.keys.map(({ pubkey, isSigner, isWritable }, keyIndex) => {
|
||||||
|
return (
|
||||||
|
<tr key={keyIndex}>
|
||||||
|
<td>
|
||||||
|
<div className="me-2 d-md-inline">
|
||||||
|
{ixAccounts && keyIndex < ixAccounts.length
|
||||||
|
? `${capitalizeFirstLetter(ixAccounts[keyIndex].name)}`
|
||||||
|
: `Account #${keyIndex + 1}`}
|
||||||
|
</div>
|
||||||
|
{isWritable && (
|
||||||
|
<span className="badge bg-info-soft me-1">Writable</span>
|
||||||
|
)}
|
||||||
|
{isSigner && (
|
||||||
|
<span className="badge bg-info-soft me-1">Signer</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="text-lg-end">
|
||||||
|
<Address pubkey={pubkey} alignRight link />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Instruction Data <span className="text-muted">(Hex)</span>
|
||||||
|
</td>
|
||||||
|
{decodedIxData ? (
|
||||||
|
<td className="metadata-json-viewer m-4">
|
||||||
|
<ReactJson src={decodedIxData} theme="solarized" />
|
||||||
|
</td>
|
||||||
|
) : (
|
||||||
|
<td className="text-lg-end">
|
||||||
|
<HexData raw={ix.data} />
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,167 +0,0 @@
|
|||||||
import {
|
|
||||||
Connection,
|
|
||||||
SignatureResult,
|
|
||||||
TransactionInstruction,
|
|
||||||
} from "@solana/web3.js";
|
|
||||||
import { InstructionCard } from "./InstructionCard";
|
|
||||||
import {
|
|
||||||
BorshInstructionCoder,
|
|
||||||
Idl,
|
|
||||||
Program,
|
|
||||||
Provider,
|
|
||||||
} from "@project-serum/anchor";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useCluster } from "../../providers/cluster";
|
|
||||||
import { Address } from "../common/Address";
|
|
||||||
import { snakeCase } from "snake-case";
|
|
||||||
|
|
||||||
export function GenericAnchorDetailsCard(props: {
|
|
||||||
ix: TransactionInstruction;
|
|
||||||
index: number;
|
|
||||||
result: SignatureResult;
|
|
||||||
signature: string;
|
|
||||||
innerCards?: JSX.Element[];
|
|
||||||
childIndex?: number;
|
|
||||||
}) {
|
|
||||||
const { ix, index, result, innerCards, childIndex } = props;
|
|
||||||
|
|
||||||
const cluster = useCluster();
|
|
||||||
|
|
||||||
const [idl, setIdl] = useState<Idl | null>();
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchIdl() {
|
|
||||||
if (idl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch on chain idl
|
|
||||||
const idl_: Idl | null = await Program.fetchIdl(ix.programId, {
|
|
||||||
connection: new Connection(cluster.url),
|
|
||||||
} as Provider);
|
|
||||||
setIdl(idl_);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchIdl();
|
|
||||||
}, [ix.programId, cluster.url, idl]);
|
|
||||||
|
|
||||||
const [programName, setProgramName] = useState<string | null>(null);
|
|
||||||
const [ixTitle, setIxTitle] = useState<string | null>(null);
|
|
||||||
const [ixAccounts, setIxAccounts] = useState<
|
|
||||||
{ name: string; isMut: boolean; isSigner: boolean; pda?: Object }[] | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function parseIxDetailsUsingCoder() {
|
|
||||||
if (!idl || (programName && ixTitle && ixAccounts)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// e.g. voter_stake_registry -> voter stake registry
|
|
||||||
var _programName = idl.name.replaceAll("_", " ").trim();
|
|
||||||
// e.g. voter stake registry -> Voter Stake Registry
|
|
||||||
_programName = _programName
|
|
||||||
.toLowerCase()
|
|
||||||
.split(" ")
|
|
||||||
.map((word) => word.charAt(0).toUpperCase() + word.substring(1))
|
|
||||||
.join(" ");
|
|
||||||
setProgramName(_programName);
|
|
||||||
|
|
||||||
const coder = new BorshInstructionCoder(idl);
|
|
||||||
const decodedIx = coder.decode(ix.data);
|
|
||||||
if (!decodedIx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get ix title, pascal case it
|
|
||||||
var _ixTitle = decodedIx.name;
|
|
||||||
_ixTitle = _ixTitle.charAt(0).toUpperCase() + _ixTitle.slice(1);
|
|
||||||
setIxTitle(_ixTitle);
|
|
||||||
|
|
||||||
// get ix accounts
|
|
||||||
const idlInstructions = idl.instructions.filter(
|
|
||||||
(ix) => ix.name === decodedIx.name
|
|
||||||
);
|
|
||||||
if (idlInstructions.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIxAccounts(
|
|
||||||
idlInstructions[0].accounts as {
|
|
||||||
// type coercing since anchor doesn't export the underlying type
|
|
||||||
name: string;
|
|
||||||
isMut: boolean;
|
|
||||||
isSigner: boolean;
|
|
||||||
pda?: Object;
|
|
||||||
}[]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
parseIxDetailsUsingCoder();
|
|
||||||
}, [
|
|
||||||
ix.programId,
|
|
||||||
ix.keys,
|
|
||||||
ix.data,
|
|
||||||
idl,
|
|
||||||
cluster,
|
|
||||||
programName,
|
|
||||||
ixTitle,
|
|
||||||
ixAccounts,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{idl && (
|
|
||||||
<InstructionCard
|
|
||||||
ix={ix}
|
|
||||||
index={index}
|
|
||||||
result={result}
|
|
||||||
title={`${programName || "Unknown"}: ${ixTitle || "Unknown"}`}
|
|
||||||
innerCards={innerCards}
|
|
||||||
childIndex={childIndex}
|
|
||||||
>
|
|
||||||
<tr key={ix.programId.toBase58()}>
|
|
||||||
<td>Program</td>
|
|
||||||
<td className="text-lg-end">
|
|
||||||
<Address pubkey={ix.programId} alignRight link />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{ixAccounts != null &&
|
|
||||||
ix.keys.map((am, keyIndex) => (
|
|
||||||
<tr key={keyIndex}>
|
|
||||||
<td>
|
|
||||||
<div className="me-2 d-md-inline">
|
|
||||||
{/* remaining accounts would not have a name */}
|
|
||||||
{ixAccounts[keyIndex] &&
|
|
||||||
snakeCase(ixAccounts[keyIndex].name)}
|
|
||||||
{!ixAccounts[keyIndex] &&
|
|
||||||
"remaining account #" +
|
|
||||||
(keyIndex - ixAccounts.length + 1)}
|
|
||||||
</div>
|
|
||||||
{am.isWritable && (
|
|
||||||
<span className="badge bg-info-soft me-1">Writable</span>
|
|
||||||
)}
|
|
||||||
{am.isSigner && (
|
|
||||||
<span className="badge bg-info-soft me-1">Signer</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Address pubkey={am.pubkey} alignRight link />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</InstructionCard>
|
|
||||||
)}
|
|
||||||
{!idl && (
|
|
||||||
<InstructionCard
|
|
||||||
ix={ix}
|
|
||||||
index={index}
|
|
||||||
result={result}
|
|
||||||
title={`Unknown Program: Unknown Instruction`}
|
|
||||||
innerCards={innerCards}
|
|
||||||
childIndex={childIndex}
|
|
||||||
defaultRaw
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
import { TransactionInstruction } from "@solana/web3.js";
|
|
||||||
|
|
||||||
// list of programs written in anchor
|
|
||||||
// - should have idl on-chain for GenericAnchorDetailsCard to work out of the box
|
|
||||||
// - before adding another program to this list, please make sure that the ix
|
|
||||||
// are decoding without any errors
|
|
||||||
const knownAnchorPrograms = [
|
|
||||||
// https://github.com/blockworks-foundation/voter-stake-registry
|
|
||||||
"4Q6WW2ouZ6V3iaNm56MTd5n2tnTm4C5fiH8miFHnAFHo",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const isInstructionFromAnAnchorProgram = (
|
|
||||||
instruction: TransactionInstruction
|
|
||||||
) => {
|
|
||||||
return knownAnchorPrograms.includes(instruction.programId.toBase58());
|
|
||||||
};
|
|
@ -5,7 +5,6 @@ import {
|
|||||||
ParsedInstruction,
|
ParsedInstruction,
|
||||||
ParsedTransaction,
|
ParsedTransaction,
|
||||||
PartiallyDecodedInstruction,
|
PartiallyDecodedInstruction,
|
||||||
PublicKey,
|
|
||||||
SignatureResult,
|
SignatureResult,
|
||||||
TransactionSignature,
|
TransactionSignature,
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
@ -42,9 +41,11 @@ import { AssociatedTokenDetailsCard } from "components/instruction/AssociatedTok
|
|||||||
import { MangoDetailsCard } from "components/instruction/MangoDetails";
|
import { MangoDetailsCard } from "components/instruction/MangoDetails";
|
||||||
import { isPythInstruction } from "components/instruction/pyth/types";
|
import { isPythInstruction } from "components/instruction/pyth/types";
|
||||||
import { PythDetailsCard } from "components/instruction/pyth/PythDetailsCard";
|
import { PythDetailsCard } from "components/instruction/pyth/PythDetailsCard";
|
||||||
import { isInstructionFromAnAnchorProgram } from "../instruction/anchor/types";
|
import AnchorDetailsCard from "../instruction/AnchorDetailsCard";
|
||||||
import { GenericAnchorDetailsCard } from "../instruction/GenericAnchorDetails";
|
|
||||||
import { isMangoInstruction } from "../instruction/mango/types";
|
import { isMangoInstruction } from "../instruction/mango/types";
|
||||||
|
import { useAnchorProgram } from "providers/anchor";
|
||||||
|
import { LoadingCard } from "components/common/LoadingCard";
|
||||||
|
import { ErrorBoundary } from "@sentry/react";
|
||||||
|
|
||||||
export type InstructionDetailsProps = {
|
export type InstructionDetailsProps = {
|
||||||
tx: ParsedTransaction;
|
tx: ParsedTransaction;
|
||||||
@ -58,14 +59,16 @@ export type InstructionDetailsProps = {
|
|||||||
export function InstructionsSection({ signature }: SignatureProps) {
|
export function InstructionsSection({ signature }: SignatureProps) {
|
||||||
const status = useTransactionStatus(signature);
|
const status = useTransactionStatus(signature);
|
||||||
const details = useTransactionDetails(signature);
|
const details = useTransactionDetails(signature);
|
||||||
const { cluster } = useCluster();
|
const { cluster, url } = useCluster();
|
||||||
const fetchDetails = useFetchTransactionDetails();
|
const fetchDetails = useFetchTransactionDetails();
|
||||||
const refreshDetails = () => fetchDetails(signature);
|
const refreshDetails = () => fetchDetails(signature);
|
||||||
|
|
||||||
if (!status?.data?.info || !details?.data?.transaction) return null;
|
const result = status?.data?.info?.result;
|
||||||
|
if (!result || !details?.data?.transaction) {
|
||||||
const { transaction } = details.data.transaction;
|
return <ErrorCard retry={refreshDetails} text="No instructions found" />;
|
||||||
|
}
|
||||||
const { meta } = details.data.transaction;
|
const { meta } = details.data.transaction;
|
||||||
|
const { transaction } = details.data?.transaction;
|
||||||
|
|
||||||
if (transaction.message.instructions.length === 0) {
|
if (transaction.message.instructions.length === 0) {
|
||||||
return <ErrorCard retry={refreshDetails} text="No instructions found" />;
|
return <ErrorCard retry={refreshDetails} text="No instructions found" />;
|
||||||
@ -91,58 +94,60 @@ export function InstructionsSection({ signature }: SignatureProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = status.data.info.result;
|
|
||||||
const instructionDetails = transaction.message.instructions.map(
|
|
||||||
(instruction, index) => {
|
|
||||||
let innerCards: JSX.Element[] = [];
|
|
||||||
|
|
||||||
if (index in innerInstructions) {
|
|
||||||
innerInstructions[index].forEach((ix, childIndex) => {
|
|
||||||
if (typeof ix.programId === "string") {
|
|
||||||
ix.programId = new PublicKey(ix.programId);
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = renderInstructionCard({
|
|
||||||
index,
|
|
||||||
ix,
|
|
||||||
result,
|
|
||||||
signature,
|
|
||||||
tx: transaction,
|
|
||||||
childIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
innerCards.push(res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderInstructionCard({
|
|
||||||
index,
|
|
||||||
ix: instruction,
|
|
||||||
result,
|
|
||||||
signature,
|
|
||||||
tx: transaction,
|
|
||||||
innerCards,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="header-body">
|
<div className="header-body">
|
||||||
<h3 className="mb-0">
|
<h3 className="mb-0">
|
||||||
{instructionDetails.length > 1 ? "Instructions" : "Instruction"}
|
{transaction.message.instructions.length > 1
|
||||||
|
? "Instructions"
|
||||||
|
: "Instruction"}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{instructionDetails}
|
<React.Suspense fallback={<LoadingCard message="Loading Instructions" />}>
|
||||||
|
{transaction.message.instructions.map((instruction, index) => {
|
||||||
|
let innerCards: JSX.Element[] = [];
|
||||||
|
|
||||||
|
if (index in innerInstructions) {
|
||||||
|
innerInstructions[index].forEach((ix, childIndex) => {
|
||||||
|
let res = (
|
||||||
|
<InstructionCard
|
||||||
|
key={`${index}-${childIndex}`}
|
||||||
|
index={index}
|
||||||
|
ix={ix}
|
||||||
|
result={result}
|
||||||
|
signature={signature}
|
||||||
|
tx={transaction}
|
||||||
|
childIndex={childIndex}
|
||||||
|
url={url}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
innerCards.push(res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InstructionCard
|
||||||
|
key={`${index}`}
|
||||||
|
index={index}
|
||||||
|
ix={instruction}
|
||||||
|
result={result}
|
||||||
|
signature={signature}
|
||||||
|
tx={transaction}
|
||||||
|
innerCards={innerCards}
|
||||||
|
url={url}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Suspense>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderInstructionCard({
|
function InstructionCard({
|
||||||
ix,
|
ix,
|
||||||
tx,
|
tx,
|
||||||
result,
|
result,
|
||||||
@ -150,6 +155,7 @@ function renderInstructionCard({
|
|||||||
signature,
|
signature,
|
||||||
innerCards,
|
innerCards,
|
||||||
childIndex,
|
childIndex,
|
||||||
|
url,
|
||||||
}: {
|
}: {
|
||||||
ix: ParsedInstruction | PartiallyDecodedInstruction;
|
ix: ParsedInstruction | PartiallyDecodedInstruction;
|
||||||
tx: ParsedTransaction;
|
tx: ParsedTransaction;
|
||||||
@ -158,8 +164,10 @@ function renderInstructionCard({
|
|||||||
signature: TransactionSignature;
|
signature: TransactionSignature;
|
||||||
innerCards?: JSX.Element[];
|
innerCards?: JSX.Element[];
|
||||||
childIndex?: number;
|
childIndex?: number;
|
||||||
|
url: string;
|
||||||
}) {
|
}) {
|
||||||
const key = `${index}-${childIndex}`;
|
const key = `${index}-${childIndex}`;
|
||||||
|
const anchorProgram = useAnchorProgram(ix.programId.toString(), url);
|
||||||
|
|
||||||
if ("parsed" in ix) {
|
if ("parsed" in ix) {
|
||||||
const props = {
|
const props = {
|
||||||
@ -216,8 +224,6 @@ function renderInstructionCard({
|
|||||||
|
|
||||||
if (isBonfidaBotInstruction(transactionIx)) {
|
if (isBonfidaBotInstruction(transactionIx)) {
|
||||||
return <BonfidaBotDetailsCard key={key} {...props} />;
|
return <BonfidaBotDetailsCard key={key} {...props} />;
|
||||||
} else if (isInstructionFromAnAnchorProgram(transactionIx)) {
|
|
||||||
return <GenericAnchorDetailsCard key={key} {...props} />;
|
|
||||||
} else if (isMangoInstruction(transactionIx)) {
|
} else if (isMangoInstruction(transactionIx)) {
|
||||||
return <MangoDetailsCard key={key} {...props} />;
|
return <MangoDetailsCard key={key} {...props} />;
|
||||||
} else if (isSerumInstruction(transactionIx)) {
|
} else if (isSerumInstruction(transactionIx)) {
|
||||||
@ -230,6 +236,12 @@ function renderInstructionCard({
|
|||||||
return <WormholeDetailsCard key={key} {...props} />;
|
return <WormholeDetailsCard key={key} {...props} />;
|
||||||
} else if (isPythInstruction(transactionIx)) {
|
} else if (isPythInstruction(transactionIx)) {
|
||||||
return <PythDetailsCard key={key} {...props} />;
|
return <PythDetailsCard key={key} {...props} />;
|
||||||
|
} else if (anchorProgram) {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary fallback={<UnknownDetailsCard {...props} />}>
|
||||||
|
<AnchorDetailsCard key={key} anchorProgram={anchorProgram} {...props} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return <UnknownDetailsCard key={key} {...props} />;
|
return <UnknownDetailsCard key={key} {...props} />;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { prettyProgramLogs } from "utils/program-logs";
|
|||||||
import { useCluster } from "providers/cluster";
|
import { useCluster } from "providers/cluster";
|
||||||
|
|
||||||
export function ProgramLogSection({ signature }: SignatureProps) {
|
export function ProgramLogSection({ signature }: SignatureProps) {
|
||||||
const { cluster } = useCluster();
|
const { cluster, url } = useCluster();
|
||||||
const details = useTransactionDetails(signature);
|
const details = useTransactionDetails(signature);
|
||||||
|
|
||||||
const transaction = details?.data?.transaction;
|
const transaction = details?.data?.transaction;
|
||||||
@ -32,6 +32,7 @@ export function ProgramLogSection({ signature }: SignatureProps) {
|
|||||||
message={message}
|
message={message}
|
||||||
logs={prettyLogs}
|
logs={prettyLogs}
|
||||||
cluster={cluster}
|
cluster={cluster}
|
||||||
|
url={url}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
|
@ -5,7 +5,6 @@ import {
|
|||||||
useFetchAccountInfo,
|
useFetchAccountInfo,
|
||||||
useAccountInfo,
|
useAccountInfo,
|
||||||
Account,
|
Account,
|
||||||
ProgramData,
|
|
||||||
TokenProgramData,
|
TokenProgramData,
|
||||||
useMintAccountInfo,
|
useMintAccountInfo,
|
||||||
} from "providers/accounts";
|
} from "providers/accounts";
|
||||||
@ -41,6 +40,9 @@ import { NFTHeader } from "components/account/MetaplexNFTHeader";
|
|||||||
import { DomainsCard } from "components/account/DomainsCard";
|
import { DomainsCard } from "components/account/DomainsCard";
|
||||||
import isMetaplexNFT from "providers/accounts/utils/isMetaplexNFT";
|
import isMetaplexNFT from "providers/accounts/utils/isMetaplexNFT";
|
||||||
import { SecurityCard } from "components/account/SecurityCard";
|
import { SecurityCard } from "components/account/SecurityCard";
|
||||||
|
import { AnchorAccountCard } from "components/account/AnchorAccountCard";
|
||||||
|
import { AnchorProgramCard } from "components/account/AnchorProgramCard";
|
||||||
|
import { useAnchorProgram } from "providers/anchor";
|
||||||
|
|
||||||
const IDENTICON_WIDTH = 64;
|
const IDENTICON_WIDTH = 64;
|
||||||
|
|
||||||
@ -246,11 +248,16 @@ function DetailsSections({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const account = info.data;
|
const account = info.data;
|
||||||
const data = account?.details?.data;
|
const tabComponents = getTabs(pubkey, account).concat(
|
||||||
const tabs = getTabs(data);
|
getAnchorTabs(pubkey, account)
|
||||||
|
);
|
||||||
|
|
||||||
let moreTab: MoreTabs = "history";
|
let moreTab: MoreTabs = "history";
|
||||||
if (tab && tabs.filter(({ slug }) => slug === tab).length === 0) {
|
if (
|
||||||
|
tab &&
|
||||||
|
tabComponents.filter((tabComponent) => tabComponent.tab.slug === tab)
|
||||||
|
.length === 0
|
||||||
|
) {
|
||||||
return <Redirect to={{ ...location, pathname: `/address/${address}` }} />;
|
return <Redirect to={{ ...location, pathname: `/address/${address}` }} />;
|
||||||
} else if (tab) {
|
} else if (tab) {
|
||||||
moreTab = tab as MoreTabs;
|
moreTab = tab as MoreTabs;
|
||||||
@ -265,7 +272,11 @@ function DetailsSections({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{<InfoSection account={account} />}
|
{<InfoSection account={account} />}
|
||||||
{<MoreSection account={account} tab={moreTab} tabs={tabs} />}
|
<MoreSection
|
||||||
|
account={account}
|
||||||
|
tab={moreTab}
|
||||||
|
tabs={tabComponents.map(({ component }) => component)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -315,6 +326,11 @@ type Tab = {
|
|||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TabComponent = {
|
||||||
|
tab: Tab;
|
||||||
|
component: JSX.Element | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type MoreTabs =
|
export type MoreTabs =
|
||||||
| "history"
|
| "history"
|
||||||
| "tokens"
|
| "tokens"
|
||||||
@ -328,7 +344,9 @@ export type MoreTabs =
|
|||||||
| "rewards"
|
| "rewards"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
| "domains"
|
| "domains"
|
||||||
| "security";
|
| "security"
|
||||||
|
| "anchor-program"
|
||||||
|
| "anchor-account";
|
||||||
|
|
||||||
function MoreSection({
|
function MoreSection({
|
||||||
account,
|
account,
|
||||||
@ -337,29 +355,17 @@ function MoreSection({
|
|||||||
}: {
|
}: {
|
||||||
account: Account;
|
account: Account;
|
||||||
tab: MoreTabs;
|
tab: MoreTabs;
|
||||||
tabs: Tab[];
|
tabs: (JSX.Element | null)[];
|
||||||
}) {
|
}) {
|
||||||
const pubkey = account.pubkey;
|
const pubkey = account.pubkey;
|
||||||
const address = account.pubkey.toBase58();
|
|
||||||
const data = account?.details?.data;
|
const data = account?.details?.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="header-body pt-0">
|
<div className="header-body pt-0">
|
||||||
<ul className="nav nav-tabs nav-overflow header-tabs">
|
<ul className="nav nav-tabs nav-overflow header-tabs">{tabs}</ul>
|
||||||
{tabs.map(({ title, slug, path }) => (
|
|
||||||
<li key={slug} className="nav-item">
|
|
||||||
<NavLink
|
|
||||||
className="nav-link"
|
|
||||||
to={clusterPath(`/address/${address}${path}`)}
|
|
||||||
exact
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -401,11 +407,29 @@ function MoreSection({
|
|||||||
{tab === "security" && data?.program === "bpf-upgradeable-loader" && (
|
{tab === "security" && data?.program === "bpf-upgradeable-loader" && (
|
||||||
<SecurityCard data={data} />
|
<SecurityCard data={data} />
|
||||||
)}
|
)}
|
||||||
|
{tab === "anchor-program" && (
|
||||||
|
<React.Suspense
|
||||||
|
fallback={<LoadingCard message="Loading anchor program IDL" />}
|
||||||
|
>
|
||||||
|
<AnchorProgramCard programId={pubkey} />
|
||||||
|
</React.Suspense>
|
||||||
|
)}
|
||||||
|
{tab === "anchor-account" && (
|
||||||
|
<React.Suspense
|
||||||
|
fallback={
|
||||||
|
<LoadingCard message="Decoding account data using anchor interface" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AnchorAccountCard account={account} />
|
||||||
|
</React.Suspense>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTabs(data?: ProgramData): Tab[] {
|
function getTabs(pubkey: PublicKey, account: Account): TabComponent[] {
|
||||||
|
const address = pubkey.toBase58();
|
||||||
|
const data = account.details?.data;
|
||||||
const tabs: Tab[] = [
|
const tabs: Tab[] = [
|
||||||
{
|
{
|
||||||
slug: "history",
|
slug: "history",
|
||||||
@ -455,5 +479,122 @@ function getTabs(data?: ProgramData): Tab[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return tabs;
|
return tabs.map((tab) => {
|
||||||
|
return {
|
||||||
|
tab,
|
||||||
|
component: (
|
||||||
|
<li key={tab.slug} className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
className="nav-link"
|
||||||
|
to={clusterPath(`/address/${address}${tab.path}`)}
|
||||||
|
exact
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAnchorTabs(pubkey: PublicKey, account: Account) {
|
||||||
|
const tabComponents = [];
|
||||||
|
const anchorProgramTab: Tab = {
|
||||||
|
slug: "anchor-program",
|
||||||
|
title: "Anchor Program IDL",
|
||||||
|
path: "/anchor-program",
|
||||||
|
};
|
||||||
|
tabComponents.push({
|
||||||
|
tab: anchorProgramTab,
|
||||||
|
component: (
|
||||||
|
<React.Suspense key={anchorProgramTab.slug} fallback={<></>}>
|
||||||
|
<AnchorProgramLink
|
||||||
|
tab={anchorProgramTab}
|
||||||
|
address={pubkey.toString()}
|
||||||
|
pubkey={pubkey}
|
||||||
|
/>
|
||||||
|
</React.Suspense>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const anchorAccountTab: Tab = {
|
||||||
|
slug: "anchor-account",
|
||||||
|
title: "Anchor Account",
|
||||||
|
path: "/anchor-account",
|
||||||
|
};
|
||||||
|
tabComponents.push({
|
||||||
|
tab: anchorAccountTab,
|
||||||
|
component: (
|
||||||
|
<React.Suspense key={anchorAccountTab.slug} fallback={<></>}>
|
||||||
|
<AnchorAccountLink
|
||||||
|
tab={anchorAccountTab}
|
||||||
|
address={pubkey.toString()}
|
||||||
|
programId={account.details?.owner}
|
||||||
|
/>
|
||||||
|
</React.Suspense>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return tabComponents;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnchorProgramLink({
|
||||||
|
tab,
|
||||||
|
address,
|
||||||
|
pubkey,
|
||||||
|
}: {
|
||||||
|
tab: Tab;
|
||||||
|
address: string;
|
||||||
|
pubkey: PublicKey;
|
||||||
|
}) {
|
||||||
|
const { url } = useCluster();
|
||||||
|
const anchorProgram = useAnchorProgram(pubkey.toString() ?? "", url);
|
||||||
|
|
||||||
|
if (!anchorProgram) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={tab.slug} className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
className="nav-link"
|
||||||
|
to={clusterPath(`/address/${address}${tab.path}`)}
|
||||||
|
exact
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnchorAccountLink({
|
||||||
|
address,
|
||||||
|
tab,
|
||||||
|
programId,
|
||||||
|
}: {
|
||||||
|
address: string;
|
||||||
|
tab: Tab;
|
||||||
|
programId: PublicKey | undefined;
|
||||||
|
}) {
|
||||||
|
const { url } = useCluster();
|
||||||
|
const accountAnchorProgram = useAnchorProgram(
|
||||||
|
programId?.toString() ?? "",
|
||||||
|
url
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!accountAnchorProgram) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={tab.slug} className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
className="nav-link"
|
||||||
|
to={clusterPath(`/address/${address}${tab.path}`)}
|
||||||
|
exact
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -105,7 +105,11 @@ export function TransactionDetailsPage({ signature: raw }: SignatureProps) {
|
|||||||
) : (
|
) : (
|
||||||
<SignatureContext.Provider value={signature}>
|
<SignatureContext.Provider value={signature}>
|
||||||
<StatusCard signature={signature} autoRefresh={autoRefresh} />
|
<StatusCard signature={signature} autoRefresh={autoRefresh} />
|
||||||
<DetailsSection signature={signature} />
|
<React.Suspense
|
||||||
|
fallback={<LoadingCard message="Loading transaction details" />}
|
||||||
|
>
|
||||||
|
<DetailsSection signature={signature} />
|
||||||
|
</React.Suspense>
|
||||||
</SignatureContext.Provider>
|
</SignatureContext.Provider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,7 @@ import { ProgramLogsCardBody } from "components/ProgramLogsCardBody";
|
|||||||
const DEFAULT_SIGNATURE = bs58.encode(Buffer.alloc(64).fill(0));
|
const DEFAULT_SIGNATURE = bs58.encode(Buffer.alloc(64).fill(0));
|
||||||
|
|
||||||
export function SimulatorCard({ message }: { message: Message }) {
|
export function SimulatorCard({ message }: { message: Message }) {
|
||||||
const { cluster } = useCluster();
|
const { cluster, url } = useCluster();
|
||||||
const {
|
const {
|
||||||
simulate,
|
simulate,
|
||||||
simulating,
|
simulating,
|
||||||
@ -67,7 +67,12 @@ export function SimulatorCard({ message }: { message: Message }) {
|
|||||||
Retry
|
Retry
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ProgramLogsCardBody message={message} logs={logs} cluster={cluster} />
|
<ProgramLogsCardBody
|
||||||
|
message={message}
|
||||||
|
logs={logs}
|
||||||
|
cluster={cluster}
|
||||||
|
url={url}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -90,6 +90,7 @@ export interface Details {
|
|||||||
owner: PublicKey;
|
owner: PublicKey;
|
||||||
space: number;
|
space: number;
|
||||||
data?: ProgramData;
|
data?: ProgramData;
|
||||||
|
rawData?: Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
@ -284,11 +285,19 @@ async function fetchAccountInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we cannot parse account layout as native spl account
|
||||||
|
// then keep raw data for other components to decode
|
||||||
|
let rawData: Buffer | undefined;
|
||||||
|
if (!data && !("parsed" in result.data)) {
|
||||||
|
rawData = result.data;
|
||||||
|
}
|
||||||
|
|
||||||
details = {
|
details = {
|
||||||
space,
|
space,
|
||||||
executable: result.executable,
|
executable: result.executable,
|
||||||
owner: result.owner,
|
owner: result.owner,
|
||||||
data,
|
data,
|
||||||
|
rawData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
data = { pubkey, lamports, details };
|
data = { pubkey, lamports, details };
|
||||||
|
47
explorer/src/providers/anchor.tsx
Normal file
47
explorer/src/providers/anchor.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Idl, Program, Provider } from "@project-serum/anchor";
|
||||||
|
import { Connection, Keypair } from "@solana/web3.js";
|
||||||
|
import { NodeWallet } from "@metaplex/js";
|
||||||
|
|
||||||
|
const cachedAnchorProgramPromises: Record<
|
||||||
|
string,
|
||||||
|
| void
|
||||||
|
| { __type: "promise"; promise: Promise<void> }
|
||||||
|
| { __type: "result"; result: Program<Idl> | null }
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
export function useAnchorProgram(
|
||||||
|
programAddress: string,
|
||||||
|
url: string
|
||||||
|
): Program | null {
|
||||||
|
const key = `${programAddress}-${url}`;
|
||||||
|
const cacheEntry = cachedAnchorProgramPromises[key];
|
||||||
|
|
||||||
|
if (cacheEntry === undefined) {
|
||||||
|
const promise = Program.at(
|
||||||
|
programAddress,
|
||||||
|
new Provider(new Connection(url), new NodeWallet(Keypair.generate()), {})
|
||||||
|
)
|
||||||
|
.then((program) => {
|
||||||
|
cachedAnchorProgramPromises[key] = {
|
||||||
|
__type: "result",
|
||||||
|
result: program,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch((_) => {
|
||||||
|
cachedAnchorProgramPromises[key] = { __type: "result", result: null };
|
||||||
|
});
|
||||||
|
cachedAnchorProgramPromises[key] = {
|
||||||
|
__type: "promise",
|
||||||
|
promise,
|
||||||
|
};
|
||||||
|
throw promise;
|
||||||
|
} else if (cacheEntry.__type === "promise") {
|
||||||
|
throw cacheEntry.promise;
|
||||||
|
}
|
||||||
|
return cacheEntry.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnchorAccount = {
|
||||||
|
layout: string;
|
||||||
|
account: Object;
|
||||||
|
};
|
109
explorer/src/utils/anchor.tsx
Normal file
109
explorer/src/utils/anchor.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Cluster } from "providers/cluster";
|
||||||
|
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
|
||||||
|
import { BorshInstructionCoder, Program } from "@project-serum/anchor";
|
||||||
|
import { useAnchorProgram } from "providers/anchor";
|
||||||
|
import { programLabel } from "utils/tx";
|
||||||
|
import { ErrorBoundary } from "@sentry/react";
|
||||||
|
|
||||||
|
function snakeToPascal(string: string) {
|
||||||
|
return string
|
||||||
|
.split("/")
|
||||||
|
.map((snake) =>
|
||||||
|
snake
|
||||||
|
.split("_")
|
||||||
|
.map((substr) => substr.charAt(0).toUpperCase() + substr.slice(1))
|
||||||
|
.join("")
|
||||||
|
)
|
||||||
|
.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProgramName(program: Program | null): string | undefined {
|
||||||
|
return program ? snakeToPascal(program.idl.name) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function capitalizeFirstLetter(input: string) {
|
||||||
|
return input.charAt(0).toUpperCase() + input.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnchorProgramName({
|
||||||
|
programId,
|
||||||
|
url,
|
||||||
|
}: {
|
||||||
|
programId: PublicKey;
|
||||||
|
url: string;
|
||||||
|
}) {
|
||||||
|
const program = useAnchorProgram(programId.toString(), url);
|
||||||
|
if (!program) {
|
||||||
|
throw new Error("No anchor program name found for given programId");
|
||||||
|
}
|
||||||
|
const programName = getProgramName(program);
|
||||||
|
return <>{programName}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProgramName({
|
||||||
|
programId,
|
||||||
|
cluster,
|
||||||
|
url,
|
||||||
|
}: {
|
||||||
|
programId: PublicKey;
|
||||||
|
cluster: Cluster;
|
||||||
|
url: string;
|
||||||
|
}) {
|
||||||
|
const defaultProgramName =
|
||||||
|
programLabel(programId.toBase58(), cluster) || "Unknown Program";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Suspense fallback={defaultProgramName}>
|
||||||
|
<ErrorBoundary fallback={<>{defaultProgramName}</>}>
|
||||||
|
<AnchorProgramName programId={programId} url={url} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</React.Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAnchorNameForInstruction(
|
||||||
|
ix: TransactionInstruction,
|
||||||
|
program: Program
|
||||||
|
): string | null {
|
||||||
|
const coder = new BorshInstructionCoder(program.idl);
|
||||||
|
const decodedIx = coder.decode(ix.data);
|
||||||
|
|
||||||
|
if (!decodedIx) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ixTitle = decodedIx.name;
|
||||||
|
return _ixTitle.charAt(0).toUpperCase() + _ixTitle.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAnchorAccountsFromInstruction(
|
||||||
|
decodedIx: Object | null,
|
||||||
|
program: Program
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
isMut: boolean;
|
||||||
|
isSigner: boolean;
|
||||||
|
pda?: Object;
|
||||||
|
}[]
|
||||||
|
| null {
|
||||||
|
if (decodedIx) {
|
||||||
|
// get ix accounts
|
||||||
|
const idlInstructions = program.idl.instructions.filter(
|
||||||
|
// @ts-ignore
|
||||||
|
(ix) => ix.name === decodedIx.name
|
||||||
|
);
|
||||||
|
if (idlInstructions.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return idlInstructions[0].accounts as {
|
||||||
|
// type coercing since anchor doesn't export the underlying type
|
||||||
|
name: string;
|
||||||
|
isMut: boolean;
|
||||||
|
isSigner: boolean;
|
||||||
|
pda?: Object;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
Reference in New Issue
Block a user