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:
parent
cd09390367
commit
559ee5a843
|
@ -14,7 +14,7 @@
|
|||
"@cloudflare/stream-react": "^1.2.0",
|
||||
"@metamask/jazzicon": "^2.0.0",
|
||||
"@metaplex/js": "4.12.0",
|
||||
"@project-serum/anchor": "^0.22.1",
|
||||
"@project-serum/anchor": "0.23.0",
|
||||
"@project-serum/serum": "^0.13.61",
|
||||
"@react-hook/debounce": "^4.0.0",
|
||||
"@sentry/react": "^6.16.1",
|
||||
|
@ -4490,12 +4490,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@project-serum/anchor": {
|
||||
"version": "0.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.22.1.tgz",
|
||||
"integrity": "sha512-5pHeyvQhzLahIQ8aZymmDMZJAJFklN0joZdI+YIqFkK2uU/mlKr6rBLQjxysf/j1mLLiNG00tdyLfUtTAdQz7w==",
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.23.0.tgz",
|
||||
"integrity": "sha512-LV2/ifZOJVFTZ4GbEloXln3iVfCvO1YM8i7BBCrUm4tehP7irMx4nr4/IabHWOzrQcQElsxSP/lb1tBp+2ff8A==",
|
||||
"dependencies": {
|
||||
"@project-serum/borsh": "^0.2.5",
|
||||
"@solana/web3.js": "^1.17.0",
|
||||
"@solana/web3.js": "^1.36.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"bn.js": "^5.1.2",
|
||||
"bs58": "^4.0.1",
|
||||
|
@ -4514,11 +4514,95 @@
|
|||
"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": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.3.tgz",
|
||||
"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": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@project-serum/borsh/-/borsh-0.2.5.tgz",
|
||||
|
@ -30637,12 +30721,12 @@
|
|||
"peer": true
|
||||
},
|
||||
"@project-serum/anchor": {
|
||||
"version": "0.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.22.1.tgz",
|
||||
"integrity": "sha512-5pHeyvQhzLahIQ8aZymmDMZJAJFklN0joZdI+YIqFkK2uU/mlKr6rBLQjxysf/j1mLLiNG00tdyLfUtTAdQz7w==",
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.23.0.tgz",
|
||||
"integrity": "sha512-LV2/ifZOJVFTZ4GbEloXln3iVfCvO1YM8i7BBCrUm4tehP7irMx4nr4/IabHWOzrQcQElsxSP/lb1tBp+2ff8A==",
|
||||
"requires": {
|
||||
"@project-serum/borsh": "^0.2.5",
|
||||
"@solana/web3.js": "^1.17.0",
|
||||
"@solana/web3.js": "^1.36.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"bn.js": "^5.1.2",
|
||||
"bs58": "^4.0.1",
|
||||
|
@ -30658,10 +30742,73 @@
|
|||
"toml": "^3.0.0"
|
||||
},
|
||||
"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": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.3.tgz",
|
||||
"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",
|
||||
"@metamask/jazzicon": "^2.0.0",
|
||||
"@metaplex/js": "4.12.0",
|
||||
"@project-serum/anchor": "^0.22.1",
|
||||
"@project-serum/anchor": "0.23.0",
|
||||
"@project-serum/serum": "^0.13.61",
|
||||
"@react-hook/debounce": "^4.0.0",
|
||||
"@sentry/react": "^6.16.1",
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import React from "react";
|
||||
import { Message, ParsedMessage } from "@solana/web3.js";
|
||||
import { Cluster } from "providers/cluster";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { programLabel } from "utils/tx";
|
||||
import { InstructionLogs } from "utils/program-logs";
|
||||
import { ProgramName } from "utils/anchor";
|
||||
|
||||
export function ProgramLogsCardBody({
|
||||
message,
|
||||
logs,
|
||||
cluster,
|
||||
url,
|
||||
}: {
|
||||
message: Message | ParsedMessage;
|
||||
logs: InstructionLogs[];
|
||||
cluster: Cluster;
|
||||
url: string;
|
||||
}) {
|
||||
return (
|
||||
<TableCardBody>
|
||||
|
@ -28,9 +29,6 @@ export function ProgramLogsCardBody({
|
|||
} else {
|
||||
programId = ix.programId;
|
||||
}
|
||||
|
||||
const programName =
|
||||
programLabel(programId.toBase58(), cluster) || "Unknown Program";
|
||||
const programLogs: InstructionLogs | undefined = logs[index];
|
||||
|
||||
let badgeColor = "white";
|
||||
|
@ -45,7 +43,12 @@ export function ProgramLogsCardBody({
|
|||
<span className={`badge bg-${badgeColor}-soft me-2`}>
|
||||
#{index + 1}
|
||||
</span>
|
||||
{programName} Instruction
|
||||
<ProgramName
|
||||
programId={programId}
|
||||
cluster={cluster}
|
||||
url={url}
|
||||
/>{" "}
|
||||
Instruction
|
||||
</div>
|
||||
{programLogs && (
|
||||
<div className="d-flex align-items-start flex-column font-monospace p-2 font-size-sm">
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
ParsedTransaction,
|
||||
PartiallyDecodedInstruction,
|
||||
PublicKey,
|
||||
SignatureResult,
|
||||
TransactionSignature,
|
||||
} from "@solana/web3.js";
|
||||
|
@ -42,9 +41,11 @@ import { AssociatedTokenDetailsCard } from "components/instruction/AssociatedTok
|
|||
import { MangoDetailsCard } from "components/instruction/MangoDetails";
|
||||
import { isPythInstruction } from "components/instruction/pyth/types";
|
||||
import { PythDetailsCard } from "components/instruction/pyth/PythDetailsCard";
|
||||
import { isInstructionFromAnAnchorProgram } from "../instruction/anchor/types";
|
||||
import { GenericAnchorDetailsCard } from "../instruction/GenericAnchorDetails";
|
||||
import AnchorDetailsCard from "../instruction/AnchorDetailsCard";
|
||||
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 = {
|
||||
tx: ParsedTransaction;
|
||||
|
@ -58,14 +59,16 @@ export type InstructionDetailsProps = {
|
|||
export function InstructionsSection({ signature }: SignatureProps) {
|
||||
const status = useTransactionStatus(signature);
|
||||
const details = useTransactionDetails(signature);
|
||||
const { cluster } = useCluster();
|
||||
const { cluster, url } = useCluster();
|
||||
const fetchDetails = useFetchTransactionDetails();
|
||||
const refreshDetails = () => fetchDetails(signature);
|
||||
|
||||
if (!status?.data?.info || !details?.data?.transaction) return null;
|
||||
|
||||
const { transaction } = details.data.transaction;
|
||||
const result = status?.data?.info?.result;
|
||||
if (!result || !details?.data?.transaction) {
|
||||
return <ErrorCard retry={refreshDetails} text="No instructions found" />;
|
||||
}
|
||||
const { meta } = details.data.transaction;
|
||||
const { transaction } = details.data?.transaction;
|
||||
|
||||
if (transaction.message.instructions.length === 0) {
|
||||
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 (
|
||||
<>
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<div className="header-body">
|
||||
<h3 className="mb-0">
|
||||
{instructionDetails.length > 1 ? "Instructions" : "Instruction"}
|
||||
{transaction.message.instructions.length > 1
|
||||
? "Instructions"
|
||||
: "Instruction"}
|
||||
</h3>
|
||||
</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,
|
||||
tx,
|
||||
result,
|
||||
|
@ -150,6 +155,7 @@ function renderInstructionCard({
|
|||
signature,
|
||||
innerCards,
|
||||
childIndex,
|
||||
url,
|
||||
}: {
|
||||
ix: ParsedInstruction | PartiallyDecodedInstruction;
|
||||
tx: ParsedTransaction;
|
||||
|
@ -158,8 +164,10 @@ function renderInstructionCard({
|
|||
signature: TransactionSignature;
|
||||
innerCards?: JSX.Element[];
|
||||
childIndex?: number;
|
||||
url: string;
|
||||
}) {
|
||||
const key = `${index}-${childIndex}`;
|
||||
const anchorProgram = useAnchorProgram(ix.programId.toString(), url);
|
||||
|
||||
if ("parsed" in ix) {
|
||||
const props = {
|
||||
|
@ -216,8 +224,6 @@ function renderInstructionCard({
|
|||
|
||||
if (isBonfidaBotInstruction(transactionIx)) {
|
||||
return <BonfidaBotDetailsCard key={key} {...props} />;
|
||||
} else if (isInstructionFromAnAnchorProgram(transactionIx)) {
|
||||
return <GenericAnchorDetailsCard key={key} {...props} />;
|
||||
} else if (isMangoInstruction(transactionIx)) {
|
||||
return <MangoDetailsCard key={key} {...props} />;
|
||||
} else if (isSerumInstruction(transactionIx)) {
|
||||
|
@ -230,6 +236,12 @@ function renderInstructionCard({
|
|||
return <WormholeDetailsCard key={key} {...props} />;
|
||||
} else if (isPythInstruction(transactionIx)) {
|
||||
return <PythDetailsCard key={key} {...props} />;
|
||||
} else if (anchorProgram) {
|
||||
return (
|
||||
<ErrorBoundary fallback={<UnknownDetailsCard {...props} />}>
|
||||
<AnchorDetailsCard key={key} anchorProgram={anchorProgram} {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
} else {
|
||||
return <UnknownDetailsCard key={key} {...props} />;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { prettyProgramLogs } from "utils/program-logs";
|
|||
import { useCluster } from "providers/cluster";
|
||||
|
||||
export function ProgramLogSection({ signature }: SignatureProps) {
|
||||
const { cluster } = useCluster();
|
||||
const { cluster, url } = useCluster();
|
||||
const details = useTransactionDetails(signature);
|
||||
|
||||
const transaction = details?.data?.transaction;
|
||||
|
@ -32,6 +32,7 @@ export function ProgramLogSection({ signature }: SignatureProps) {
|
|||
message={message}
|
||||
logs={prettyLogs}
|
||||
cluster={cluster}
|
||||
url={url}
|
||||
/>
|
||||
) : (
|
||||
<div className="card-body">
|
||||
|
|
|
@ -5,7 +5,6 @@ import {
|
|||
useFetchAccountInfo,
|
||||
useAccountInfo,
|
||||
Account,
|
||||
ProgramData,
|
||||
TokenProgramData,
|
||||
useMintAccountInfo,
|
||||
} from "providers/accounts";
|
||||
|
@ -41,6 +40,9 @@ import { NFTHeader } from "components/account/MetaplexNFTHeader";
|
|||
import { DomainsCard } from "components/account/DomainsCard";
|
||||
import isMetaplexNFT from "providers/accounts/utils/isMetaplexNFT";
|
||||
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;
|
||||
|
||||
|
@ -246,11 +248,16 @@ function DetailsSections({
|
|||
}
|
||||
|
||||
const account = info.data;
|
||||
const data = account?.details?.data;
|
||||
const tabs = getTabs(data);
|
||||
const tabComponents = getTabs(pubkey, account).concat(
|
||||
getAnchorTabs(pubkey, account)
|
||||
);
|
||||
|
||||
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}` }} />;
|
||||
} else if (tab) {
|
||||
moreTab = tab as MoreTabs;
|
||||
|
@ -265,7 +272,11 @@ function DetailsSections({
|
|||
</div>
|
||||
)}
|
||||
{<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;
|
||||
};
|
||||
|
||||
type TabComponent = {
|
||||
tab: Tab;
|
||||
component: JSX.Element | null;
|
||||
};
|
||||
|
||||
export type MoreTabs =
|
||||
| "history"
|
||||
| "tokens"
|
||||
|
@ -328,7 +344,9 @@ export type MoreTabs =
|
|||
| "rewards"
|
||||
| "metadata"
|
||||
| "domains"
|
||||
| "security";
|
||||
| "security"
|
||||
| "anchor-program"
|
||||
| "anchor-account";
|
||||
|
||||
function MoreSection({
|
||||
account,
|
||||
|
@ -337,29 +355,17 @@ function MoreSection({
|
|||
}: {
|
||||
account: Account;
|
||||
tab: MoreTabs;
|
||||
tabs: Tab[];
|
||||
tabs: (JSX.Element | null)[];
|
||||
}) {
|
||||
const pubkey = account.pubkey;
|
||||
const address = account.pubkey.toBase58();
|
||||
const data = account?.details?.data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<div className="header-body pt-0">
|
||||
<ul className="nav nav-tabs nav-overflow header-tabs">
|
||||
{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>
|
||||
<ul className="nav nav-tabs nav-overflow header-tabs">{tabs}</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -401,11 +407,29 @@ function MoreSection({
|
|||
{tab === "security" && data?.program === "bpf-upgradeable-loader" && (
|
||||
<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[] = [
|
||||
{
|
||||
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}>
|
||||
<StatusCard signature={signature} autoRefresh={autoRefresh} />
|
||||
<React.Suspense
|
||||
fallback={<LoadingCard message="Loading transaction details" />}
|
||||
>
|
||||
<DetailsSection signature={signature} />
|
||||
</React.Suspense>
|
||||
</SignatureContext.Provider>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@ import { ProgramLogsCardBody } from "components/ProgramLogsCardBody";
|
|||
const DEFAULT_SIGNATURE = bs58.encode(Buffer.alloc(64).fill(0));
|
||||
|
||||
export function SimulatorCard({ message }: { message: Message }) {
|
||||
const { cluster } = useCluster();
|
||||
const { cluster, url } = useCluster();
|
||||
const {
|
||||
simulate,
|
||||
simulating,
|
||||
|
@ -67,7 +67,12 @@ export function SimulatorCard({ message }: { message: Message }) {
|
|||
Retry
|
||||
</button>
|
||||
</div>
|
||||
<ProgramLogsCardBody message={message} logs={logs} cluster={cluster} />
|
||||
<ProgramLogsCardBody
|
||||
message={message}
|
||||
logs={logs}
|
||||
cluster={cluster}
|
||||
url={url}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -90,6 +90,7 @@ export interface Details {
|
|||
owner: PublicKey;
|
||||
space: number;
|
||||
data?: ProgramData;
|
||||
rawData?: Buffer;
|
||||
}
|
||||
|
||||
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 = {
|
||||
space,
|
||||
executable: result.executable,
|
||||
owner: result.owner,
|
||||
data,
|
||||
rawData,
|
||||
};
|
||||
}
|
||||
data = { pubkey, lamports, details };
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue