feat(explorer): render program name, ix name, and account names from on chain idl for specific anchor programs (#23499)
* show titles of ix, from idl Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * remove unused Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * remaining accounts Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * fallback Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * fix from code review: remove default for the non fallback case Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * keep camelcase Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * formatting Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
dc3863ef14
commit
1b7b261460
|
@ -14,6 +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/serum": "^0.13.61",
|
||||
"@react-hook/debounce": "^4.0.0",
|
||||
"@sentry/react": "^6.16.1",
|
||||
|
@ -4489,17 +4490,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@project-serum/anchor": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.11.1.tgz",
|
||||
"integrity": "sha512-oIdm4vTJkUy6GmE6JgqDAuQPKI7XM4TPJkjtoIzp69RZe0iAD9JP2XHx7lV1jLdYXeYHqDXfBt3zcq7W91K6PA==",
|
||||
"version": "0.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.22.1.tgz",
|
||||
"integrity": "sha512-5pHeyvQhzLahIQ8aZymmDMZJAJFklN0joZdI+YIqFkK2uU/mlKr6rBLQjxysf/j1mLLiNG00tdyLfUtTAdQz7w==",
|
||||
"dependencies": {
|
||||
"@project-serum/borsh": "^0.2.2",
|
||||
"@project-serum/borsh": "^0.2.5",
|
||||
"@solana/web3.js": "^1.17.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"bn.js": "^5.1.2",
|
||||
"bs58": "^4.0.1",
|
||||
"buffer-layout": "^1.2.0",
|
||||
"buffer-layout": "^1.2.2",
|
||||
"camelcase": "^5.3.1",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"crypto-hash": "^1.3.0",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"find": "^0.3.0",
|
||||
|
@ -4547,6 +4549,30 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@project-serum/serum/node_modules/@project-serum/anchor": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.11.1.tgz",
|
||||
"integrity": "sha512-oIdm4vTJkUy6GmE6JgqDAuQPKI7XM4TPJkjtoIzp69RZe0iAD9JP2XHx7lV1jLdYXeYHqDXfBt3zcq7W91K6PA==",
|
||||
"dependencies": {
|
||||
"@project-serum/borsh": "^0.2.2",
|
||||
"@solana/web3.js": "^1.17.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"bn.js": "^5.1.2",
|
||||
"bs58": "^4.0.1",
|
||||
"buffer-layout": "^1.2.0",
|
||||
"camelcase": "^5.3.1",
|
||||
"crypto-hash": "^1.3.0",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"find": "^0.3.0",
|
||||
"js-sha256": "^0.9.0",
|
||||
"pako": "^2.0.3",
|
||||
"snake-case": "^3.0.4",
|
||||
"toml": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=11"
|
||||
}
|
||||
},
|
||||
"node_modules/@project-serum/serum/node_modules/@solana/spl-token": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.6.tgz",
|
||||
|
@ -4594,6 +4620,11 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@project-serum/serum/node_modules/pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
|
||||
},
|
||||
"node_modules/@project-serum/sol-wallet-adapter": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@project-serum/sol-wallet-adapter/-/sol-wallet-adapter-0.1.8.tgz",
|
||||
|
@ -30606,17 +30637,18 @@
|
|||
"peer": true
|
||||
},
|
||||
"@project-serum/anchor": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.11.1.tgz",
|
||||
"integrity": "sha512-oIdm4vTJkUy6GmE6JgqDAuQPKI7XM4TPJkjtoIzp69RZe0iAD9JP2XHx7lV1jLdYXeYHqDXfBt3zcq7W91K6PA==",
|
||||
"version": "0.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.22.1.tgz",
|
||||
"integrity": "sha512-5pHeyvQhzLahIQ8aZymmDMZJAJFklN0joZdI+YIqFkK2uU/mlKr6rBLQjxysf/j1mLLiNG00tdyLfUtTAdQz7w==",
|
||||
"requires": {
|
||||
"@project-serum/borsh": "^0.2.2",
|
||||
"@project-serum/borsh": "^0.2.5",
|
||||
"@solana/web3.js": "^1.17.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"bn.js": "^5.1.2",
|
||||
"bs58": "^4.0.1",
|
||||
"buffer-layout": "^1.2.0",
|
||||
"buffer-layout": "^1.2.2",
|
||||
"camelcase": "^5.3.1",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"crypto-hash": "^1.3.0",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"find": "^0.3.0",
|
||||
|
@ -30654,6 +30686,27 @@
|
|||
"buffer-layout": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@project-serum/anchor": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.11.1.tgz",
|
||||
"integrity": "sha512-oIdm4vTJkUy6GmE6JgqDAuQPKI7XM4TPJkjtoIzp69RZe0iAD9JP2XHx7lV1jLdYXeYHqDXfBt3zcq7W91K6PA==",
|
||||
"requires": {
|
||||
"@project-serum/borsh": "^0.2.2",
|
||||
"@solana/web3.js": "^1.17.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"bn.js": "^5.1.2",
|
||||
"bs58": "^4.0.1",
|
||||
"buffer-layout": "^1.2.0",
|
||||
"camelcase": "^5.3.1",
|
||||
"crypto-hash": "^1.3.0",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"find": "^0.3.0",
|
||||
"js-sha256": "^0.9.0",
|
||||
"pako": "^2.0.3",
|
||||
"snake-case": "^3.0.4",
|
||||
"toml": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"@solana/spl-token": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.6.tgz",
|
||||
|
@ -30680,6 +30733,11 @@
|
|||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
|
||||
},
|
||||
"pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -9,6 +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/serum": "^0.13.61",
|
||||
"@react-hook/debounce": "^4.0.0",
|
||||
"@sentry/react": "^6.16.1",
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
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());
|
||||
};
|
|
@ -21,8 +21,8 @@ import { WormholeDetailsCard } from "components/instruction/WormholeDetailsCard"
|
|||
import { UnknownDetailsCard } from "components/instruction/UnknownDetailsCard";
|
||||
import { BonfidaBotDetailsCard } from "components/instruction/BonfidaBotDetails";
|
||||
import {
|
||||
SignatureProps,
|
||||
INNER_INSTRUCTIONS_START_SLOT,
|
||||
SignatureProps,
|
||||
} from "pages/TransactionDetailsPage";
|
||||
import { intoTransactionInstruction } from "utils/tx";
|
||||
import { isSerumInstruction } from "components/instruction/serum/types";
|
||||
|
@ -39,10 +39,12 @@ import { BpfUpgradeableLoaderDetailsCard } from "components/instruction/bpf-upgr
|
|||
import { VoteDetailsCard } from "components/instruction/vote/VoteDetailsCard";
|
||||
import { isWormholeInstruction } from "components/instruction/wormhole/types";
|
||||
import { AssociatedTokenDetailsCard } from "components/instruction/AssociatedTokenDetailsCard";
|
||||
import { isMangoInstruction } from "components/instruction/mango/types";
|
||||
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 { isMangoInstruction } from "../instruction/mango/types";
|
||||
|
||||
export type InstructionDetailsProps = {
|
||||
tx: ParsedTransaction;
|
||||
|
@ -214,6 +216,8 @@ 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)) {
|
||||
|
|
Loading…
Reference in New Issue