diff --git a/explorer/package-lock.json b/explorer/package-lock.json index b7f22fa74e..77b5152429 100644 --- a/explorer/package-lock.json +++ b/explorer/package-lock.json @@ -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==" } } }, diff --git a/explorer/package.json b/explorer/package.json index 3b00882c23..0dece6f4a3 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -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", diff --git a/explorer/src/components/instruction/GenericAnchorDetails.tsx b/explorer/src/components/instruction/GenericAnchorDetails.tsx new file mode 100644 index 0000000000..c5f383f0e5 --- /dev/null +++ b/explorer/src/components/instruction/GenericAnchorDetails.tsx @@ -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(); + 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(null); + const [ixTitle, setIxTitle] = useState(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 ( +
+ {idl && ( + + + Program + +
+ + + + {ixAccounts != null && + ix.keys.map((am, keyIndex) => ( + + +
+ {/* remaining accounts would not have a name */} + {ixAccounts[keyIndex] && + snakeCase(ixAccounts[keyIndex].name)} + {!ixAccounts[keyIndex] && + "remaining account #" + + (keyIndex - ixAccounts.length + 1)} +
+ {am.isWritable && ( + Writable + )} + {am.isSigner && ( + Signer + )} + + +
+ + + ))} + + )} + {!idl && ( + + )} +
+ ); +} diff --git a/explorer/src/components/instruction/anchor/types.ts b/explorer/src/components/instruction/anchor/types.ts new file mode 100644 index 0000000000..f1a39b2869 --- /dev/null +++ b/explorer/src/components/instruction/anchor/types.ts @@ -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()); +}; diff --git a/explorer/src/components/transaction/InstructionsSection.tsx b/explorer/src/components/transaction/InstructionsSection.tsx index ce813c8809..34577835b9 100644 --- a/explorer/src/components/transaction/InstructionsSection.tsx +++ b/explorer/src/components/transaction/InstructionsSection.tsx @@ -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 ; + } else if (isInstructionFromAnAnchorProgram(transactionIx)) { + return ; } else if (isMangoInstruction(transactionIx)) { return ; } else if (isSerumInstruction(transactionIx)) {