From b4b26894cd80d9957475be3a36d024b61f00dcb1 Mon Sep 17 00:00:00 2001 From: man0s <95379755+losman0s@users.noreply.github.com> Date: Thu, 14 Apr 2022 03:38:59 +0800 Subject: [PATCH] Iterate on IDL account/instruction decoding (#24239) * Switch to more integrated Anchor data decoding * Revert anchor account data tab and better error handling --- explorer/package-lock.json | 54 +-- .../src/components/ProgramLogsCardBody.tsx | 1 + .../components/account/AnchorAccountCard.tsx | 166 ++----- explorer/src/components/common/Address.tsx | 6 + .../instruction/AnchorDetailsCard.tsx | 129 ++++-- .../instruction/InstructionCard.tsx | 16 +- explorer/src/pages/AccountDetailsPage.tsx | 16 +- explorer/src/scss/dashkit/_tables.scss | 13 +- explorer/src/utils/anchor.tsx | 435 ++++++++++++++++-- explorer/src/utils/index.tsx | 25 + 10 files changed, 623 insertions(+), 238 deletions(-) diff --git a/explorer/package-lock.json b/explorer/package-lock.json index f24499ffc955e4..b4324db726c208 100644 --- a/explorer/package-lock.json +++ b/explorer/package-lock.json @@ -1800,11 +1800,6 @@ "node": ">=8" } }, - "node_modules/@blockworks-foundation/mango-client/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/@blockworks-foundation/mango-client/node_modules/string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -4593,11 +4588,6 @@ "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", @@ -4704,11 +4694,6 @@ "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", @@ -8087,6 +8072,11 @@ "pako": "~1.0.5" } }, + "node_modules/browserify-zlib/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/browserslist": { "version": "4.16.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", @@ -19555,9 +19545,9 @@ } }, "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" }, "node_modules/parallel-transform": { "version": "1.2.0", @@ -28715,11 +28705,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, - "pako": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", - "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" - }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -30800,11 +30785,6 @@ "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", @@ -30880,11 +30860,6 @@ "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==" } } }, @@ -33543,6 +33518,13 @@ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", "requires": { "pako": "~1.0.5" + }, + "dependencies": { + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + } } }, "browserslist": { @@ -42391,9 +42373,9 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" }, "parallel-transform": { "version": "1.2.0", diff --git a/explorer/src/components/ProgramLogsCardBody.tsx b/explorer/src/components/ProgramLogsCardBody.tsx index 69653ea8cfacb0..7839ccf69d9d04 100644 --- a/explorer/src/components/ProgramLogsCardBody.tsx +++ b/explorer/src/components/ProgramLogsCardBody.tsx @@ -3,6 +3,7 @@ import { Cluster } from "providers/cluster"; import { TableCardBody } from "components/common/TableCardBody"; import { InstructionLogs } from "utils/program-logs"; import { ProgramName } from "utils/anchor"; +import React from "react"; export function ProgramLogsCardBody({ message, diff --git a/explorer/src/components/account/AnchorAccountCard.tsx b/explorer/src/components/account/AnchorAccountCard.tsx index 74db69709696ea..95ba337c25d2aa 100644 --- a/explorer/src/components/account/AnchorAccountCard.tsx +++ b/explorer/src/components/account/AnchorAccountCard.tsx @@ -1,63 +1,62 @@ import React, { useMemo } from "react"; - import { Account } from "providers/accounts"; -import { Address } from "components/common/Address"; +import { useCluster } from "providers/cluster"; import { BorshAccountsCoder } from "@project-serum/anchor"; -import { capitalizeFirstLetter } from "utils/anchor"; +import { IdlTypeDef } from "@project-serum/anchor/dist/cjs/idl"; +import { getProgramName, mapAccountToRows } 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 { lamports } = account; const { url } = useCluster(); - const program = useAnchorProgram( - account.details?.owner.toString() ?? "", + const anchorProgram = useAnchorProgram( + account.details?.owner.toString() || "", url ); + const rawData = account?.details?.rawData; + const programName = getProgramName(anchorProgram) || "Unknown Program"; - 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 - ); - } - }); + const { decodedAccountData, accountDef } = useMemo(() => { + let decodedAccountData: any | null = null; + let accountDef: IdlTypeDef | undefined = undefined; + if (anchorProgram && rawData) { + const coder = new BorshAccountsCoder(anchorProgram.idl); + const accountDefTmp = anchorProgram.idl.accounts?.find( + (accountType: any) => + (rawData as Buffer) + .slice(0, 8) + .equals(BorshAccountsCoder.accountDiscriminator(accountType.name)) + ); + if (accountDefTmp) { + accountDef = accountDefTmp; + decodedAccountData = coder.decode(accountDef.name, rawData); + } } - return { foundAccountLayoutName, decodedAnchorAccountData }; - }, [program, account.details]); - if (!foundAccountLayoutName || !decodedAnchorAccountData) { + return { + decodedAccountData, + accountDef, + }; + }, [anchorProgram, rawData]); + + if (lamports === undefined) return null; + if (!anchorProgram) return ; + if (!decodedAccountData || !accountDef) { return ( - + ); } return ( - <> +
-

{foundAccountLayoutName}

+

+ {programName}: {accountDef.name} +

@@ -66,92 +65,21 @@ export function AnchorAccountCard({ account }: { account: Account }) { - - + + + - - {decodedAnchorAccountData && - Object.keys(decodedAnchorAccountData).map((key) => ( - - ))} + + {mapAccountToRows( + decodedAccountData, + accountDef as IdlTypeDef, + anchorProgram.idl + )}
KeyValueFieldTypeValue
-
-
- {decodedAnchorAccountData && - Object.keys(decodedAnchorAccountData).length > 0 - ? `Decoded ${Object.keys(decodedAnchorAccountData).length} Items` - : "No decoded data"} -
-
- - ); -} - -function AccountRow({ valueName, value }: { valueName: string; value: any }) { - let displayValue: JSX.Element; - if (value instanceof PublicKey) { - displayValue =
; - } else if (value instanceof BN) { - displayValue = <>{value.toString()}; - } else if (!(value instanceof Object)) { - displayValue = <>{String(value)}; - } else if (value) { - const displayObject = stringifyPubkeyAndBigNums(value); - displayValue = ( - - ); - } else { - displayValue = <>null; - } - return ( - - {camelToUnderscore(valueName)} - {displayValue} - - ); -} - -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 + ); } diff --git a/explorer/src/components/common/Address.tsx b/explorer/src/components/common/Address.tsx index 6674e877feff70..5858e27090ee89 100644 --- a/explorer/src/components/common/Address.tsx +++ b/explorer/src/components/common/Address.tsx @@ -18,6 +18,7 @@ type Props = { truncateUnknown?: boolean; truncateChars?: number; useMetadata?: boolean; + overrideText?: string; }; export function Address({ @@ -29,6 +30,7 @@ export function Address({ truncateUnknown, truncateChars, useMetadata, + overrideText, }: Props) { const address = pubkey.toBase58(); const { tokenRegistry } = useTokenRegistry(); @@ -52,6 +54,10 @@ export function Address({ addressLabel = addressLabel.slice(0, truncateChars) + "…"; } + if (overrideText) { + addressLabel = overrideText; + } + const content = ( diff --git a/explorer/src/components/instruction/AnchorDetailsCard.tsx b/explorer/src/components/instruction/AnchorDetailsCard.tsx index 596f0fefc6f791..2de9fdb7b5a45c 100644 --- a/explorer/src/components/instruction/AnchorDetailsCard.tsx +++ b/explorer/src/components/instruction/AnchorDetailsCard.tsx @@ -1,15 +1,21 @@ import { SignatureResult, TransactionInstruction } from "@solana/web3.js"; import { InstructionCard } from "./InstructionCard"; -import { Idl, Program, BorshInstructionCoder } from "@project-serum/anchor"; +import { + Idl, + Program, + BorshInstructionCoder, + Instruction, +} from "@project-serum/anchor"; import { getAnchorNameForInstruction, getProgramName, - capitalizeFirstLetter, getAnchorAccountsFromInstruction, + mapIxArgsToRows, } from "utils/anchor"; -import { HexData } from "components/common/HexData"; import { Address } from "components/common/Address"; -import ReactJson from "react-json-view"; +import { camelToTitleCase } from "utils"; +import { IdlInstruction } from "@project-serum/anchor/dist/cjs/idl"; +import { useMemo } from "react"; export default function AnchorDetailsCard(props: { key: string; @@ -26,46 +32,99 @@ export default function AnchorDetailsCard(props: { const ixName = getAnchorNameForInstruction(ix, anchorProgram) ?? "Unknown Instruction"; - const cardTitle = `${programName}: ${ixName}`; + const cardTitle = `${camelToTitleCase(programName)}: ${camelToTitleCase( + ixName + )}`; return ( - + ); } -function RawAnchorDetails({ +function AnchorDetails({ 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); + const { ixAccounts, decodedIxData, ixDef } = useMemo(() => { + let ixAccounts: + | { + name: string; + isMut: boolean; + isSigner: boolean; + pda?: Object; + }[] + | null = null; + let decodedIxData: Instruction | null = null; + let ixDef: IdlInstruction | undefined; + if (anchorProgram) { + const coder = new BorshInstructionCoder(anchorProgram.idl); + decodedIxData = coder.decode(ix.data); + if (decodedIxData) { + ixDef = anchorProgram.idl.instructions.find( + (ixDef) => ixDef.name === decodedIxData?.name + ); + if (ixDef) { + ixAccounts = getAnchorAccountsFromInstruction( + decodedIxData, + anchorProgram + ); + } + } + } + + return { + ixAccounts, + decodedIxData, + ixDef, + }; + }, [anchorProgram, ix.data]); + + if (!ixAccounts || !decodedIxData || !ixDef) { + return ( + + + Failed to decode account data according to the public Anchor interface + + + ); } + const programName = getProgramName(anchorProgram) ?? "Unknown Program"; + return ( <> + + Program + +
+ + + + Account Name + + Address + + {ix.keys.map(({ pubkey, isSigner, isWritable }, keyIndex) => { return (
- {ixAccounts && keyIndex < ixAccounts.length - ? `${capitalizeFirstLetter(ixAccounts[keyIndex].name)}` + {ixAccounts + ? keyIndex < ixAccounts.length + ? `${camelToTitleCase(ixAccounts[keyIndex].name)}` + : `Remaining Account #${keyIndex + 1 - ixAccounts.length}` : `Account #${keyIndex + 1}`}
{isWritable && ( @@ -75,27 +134,23 @@ function RawAnchorDetails({ Signer )} - +
); })} - - - Instruction Data (Hex) - - {decodedIxData ? ( - - - - ) : ( - - - - )} - + {decodedIxData && ixDef && ixDef.args.length > 0 && ( + <> + + Argument Name + Type + Value + + {mapIxArgsToRows(decodedIxData.data, ixDef, anchorProgram.idl)} + + )} ); } diff --git a/explorer/src/components/instruction/InstructionCard.tsx b/explorer/src/components/instruction/InstructionCard.tsx index c51d398e99a2de..3ed6d5fcf09841 100644 --- a/explorer/src/components/instruction/InstructionCard.tsx +++ b/explorer/src/components/instruction/InstructionCard.tsx @@ -100,12 +100,16 @@ export function InstructionCard({ children )} {innerCards && innerCards.length > 0 && ( - - - Inner Instructions -
{innerCards}
- - + <> + + Inner Instructions + + + +
{innerCards}
+ + + )} diff --git a/explorer/src/pages/AccountDetailsPage.tsx b/explorer/src/pages/AccountDetailsPage.tsx index 6301abc3688cfa..2b625ecdcd8984 100644 --- a/explorer/src/pages/AccountDetailsPage.tsx +++ b/explorer/src/pages/AccountDetailsPage.tsx @@ -271,7 +271,7 @@ function DetailsSections({ account. Please be cautious sending SOL to this account. )} - {} + }> - }> + @@ -567,7 +567,7 @@ function AnchorProgramLink({ ); } -function AnchorAccountLink({ +function AccountDataLink({ address, tab, programId, diff --git a/explorer/src/scss/dashkit/_tables.scss b/explorer/src/scss/dashkit/_tables.scss index e198a97ed1f1ee..2e2435bf92a3c7 100644 --- a/explorer/src/scss/dashkit/_tables.scss +++ b/explorer/src/scss/dashkit/_tables.scss @@ -1,9 +1,9 @@ -// +// // tables.scss // Extended from Bootstrap // -// +// // Bootstrap Overrides ===================================== // @@ -25,6 +25,15 @@ border-bottom: 0; } +.table-sep { + background-color: $table-head-bg; + text-transform: uppercase; + font-size: $font-size-xs; + font-weight: $font-weight-bold; + letter-spacing: .08em; + color: $table-head-color; +} + // Sizing diff --git a/explorer/src/utils/anchor.tsx b/explorer/src/utils/anchor.tsx index 096fb9a0f05f0a..8c5d7cf0473688 100644 --- a/explorer/src/utils/anchor.tsx +++ b/explorer/src/utils/anchor.tsx @@ -1,43 +1,33 @@ -import React from "react"; +import React, { Fragment, ReactNode, useState } from "react"; import { Cluster } from "providers/cluster"; import { PublicKey, TransactionInstruction } from "@solana/web3.js"; -import { BorshInstructionCoder, Program } from "@project-serum/anchor"; +import { BorshInstructionCoder, Program, Idl } 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("/"); -} +import { snakeToTitleCase, camelToTitleCase, numberWithSeparator } from "utils"; +import { + IdlInstruction, + IdlType, + IdlTypeDef, +} from "@project-serum/anchor/dist/cjs/idl"; +import { Address } from "components/common/Address"; +import ReactJson from "react-json-view"; 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); + return program ? snakeToTitleCase(program.idl.name) : undefined; } -function AnchorProgramName({ +export function AnchorProgramName({ programId, url, + defaultName = "Unknown Program", }: { programId: PublicKey; url: string; + defaultName?: string; }) { const program = useAnchorProgram(programId.toString(), url); - if (!program) { - throw new Error("No anchor program name found for given programId"); - } - const programName = getProgramName(program); + const programName = getProgramName(program) || defaultName; return <>{programName}; } @@ -52,12 +42,13 @@ export function ProgramName({ }) { const defaultProgramName = programLabel(programId.toBase58(), cluster) || "Unknown Program"; - return ( - - {defaultProgramName}}> - - + {defaultProgramName}}> + ); } @@ -107,3 +98,387 @@ export function getAnchorAccountsFromInstruction( } return null; } + +export function mapIxArgsToRows(ixArgs: any, ixType: IdlInstruction, idl: Idl) { + return Object.entries(ixArgs).map(([key, value]) => { + try { + const fieldDef = ixType.args.find((ixDefArg) => ixDefArg.name === key); + if (!fieldDef) { + throw Error( + `Could not find expected ${key} field on account type definition for ${ixType.name}` + ); + } + return mapField(key, value, fieldDef.type, idl); + } catch (error: any) { + console.log("Error while displaying IDL-based account data", error); + return ( + + {key} + + + + + + + ); + } + }); +} + +export function mapAccountToRows( + accountData: any, + accountType: IdlTypeDef, + idl: Idl +) { + return Object.entries(accountData).map(([key, value]) => { + try { + if (accountType.type.kind !== "struct") { + throw Error( + `Account ${accountType.name} is of type ${accountType.type.kind} (expected: 'struct')` + ); + } + const fieldDef = accountType.type.fields.find( + (ixDefArg) => ixDefArg.name === key + ); + if (!fieldDef) { + throw Error( + `Could not find expected ${key} field on account type definition for ${accountType.name}` + ); + } + return mapField(key, value as any, fieldDef.type, idl); + } catch (error: any) { + console.log("Error while displaying IDL-based account data", error); + return ( + + {key} + + + + + + + ); + } + }); +} + +function mapField( + key: string, + value: any, + type: IdlType, + idl: Idl, + keySuffix?: any, + nestingLevel: number = 0 +): ReactNode { + let itemKey = key; + if (/^-?\d+$/.test(keySuffix)) { + itemKey = `#${keySuffix}`; + } + itemKey = camelToTitleCase(itemKey); + + if (value === undefined) { + return ( + +
null
+
+ ); + } + + if ( + type === "u8" || + type === "i8" || + type === "u16" || + type === "i16" || + type === "u32" || + type === "i32" || + type === "f32" || + type === "u64" || + type === "i64" || + type === "f64" || + type === "u128" || + type === "i128" + ) { + return ( + +
{numberWithSeparator(value.toString())}
+
+ ); + } else if (type === "bool" || type === "bytes" || type === "string") { + return ( + +
{value.toString()}
+
+ ); + } else if (type === "publicKey") { + return ( + +
+ + ); + } else if ("defined" in type) { + const fieldType = idl.types?.find((t) => t.name === type.defined); + if (!fieldType) { + throw Error(`Could not type definition for ${type.defined} field in IDL`); + } + if (fieldType.type.kind === "struct") { + const structFields = fieldType.type.fields; + return ( + + + {Object.entries(value).map( + ([innerKey, innerValue]: [string, any]) => { + const innerFieldType = structFields.find( + (t) => t.name === innerKey + ); + if (!innerFieldType) { + throw Error( + `Could not type definition for ${innerKey} field in user-defined struct ${fieldType.name}` + ); + } + return mapField( + innerKey, + innerValue, + innerFieldType?.type, + idl, + key, + nestingLevel + 1 + ); + } + )} + + + ); + } else { + const enumValue = Object.keys(value)[0]; + return ( + + {camelToTitleCase(enumValue)} + + ); + } + } else if ("option" in type) { + if (value === null) { + return ( + + Not provided + + ); + } + return mapField(key, value, type.option, idl, key, nestingLevel); + } else if ("vec" in type) { + const itemType = type.vec; + return ( + + + {(value as any[]).map((item, i) => + mapField(key, item, itemType, idl, i, nestingLevel + 1) + )} + + + ); + } else if ("array" in type) { + const [itemType] = type.array; + return ( + + + {(value as any[]).map((item, i) => + mapField(key, item, itemType, idl, i, nestingLevel + 1) + )} + + + ); + } else { + console.log("Impossible type:", type); + return ( + + {camelToTitleCase(key)} + + ??? + + ); + } +} + +function SimpleRow({ + rawKey, + type, + keySuffix, + nestingLevel = 0, + children, +}: { + rawKey: string; + type: IdlType | { enum: string }; + keySuffix?: any; + nestingLevel: number; + children?: ReactNode; +}) { + let itemKey = rawKey; + if (/^-?\d+$/.test(keySuffix)) { + itemKey = `#${keySuffix}`; + } + itemKey = camelToTitleCase(itemKey); + return ( + + + {nestingLevel > 0 && ( + + )} +
{itemKey}
+ + {typeDisplayName(type)} + {children} + + ); +} + +export function ExpandableRow({ + fieldName, + fieldType, + nestingLevel, + children, +}: { + fieldName: string; + fieldType: string; + nestingLevel: number; + children: React.ReactNode; +}) { + const [expanded, setExpanded] = useState(false); + return ( + <> + + + {nestingLevel > 0 && ( +
+ )} +
{fieldName}
+ + {fieldType} + setExpanded((current) => !current)} + > +
+ {expanded ? ( + <> + Collapse + + + ) : ( + <> + Expand + + + )} +
+ + + {expanded && <>{children}} + + ); +} + +function typeDisplayName( + type: + | IdlType + | { + enum: string; + } +): string { + switch (type) { + case "bool": + case "u8": + case "i8": + case "u16": + case "i16": + case "u32": + case "i32": + case "f32": + case "u64": + case "i64": + case "f64": + case "u128": + case "i128": + case "bytes": + case "string": + return type.toString(); + case "publicKey": + return "PublicKey"; + default: + if ("enum" in type) return `${type.enum} (enum)`; + if ("defined" in type) return type.defined; + if ("option" in type) return `${typeDisplayName(type.option)} (optional)`; + if ("vec" in type) return `${typeDisplayName(type.vec)}[]`; + if ("array" in type) + return `${typeDisplayName(type.array[0])}[${type.array[1]}]`; + return "unkonwn"; + } +} diff --git a/explorer/src/utils/index.tsx b/explorer/src/utils/index.tsx index a9097fe573677c..ab87413af8c51f 100644 --- a/explorer/src/utils/index.tsx +++ b/explorer/src/utils/index.tsx @@ -56,6 +56,10 @@ export function lamportsToSolString( return new Intl.NumberFormat("en-US", { maximumFractionDigits }).format(sol); } +export function numberWithSeparator(s: string) { + return s.replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} + export function SolBalance({ lamports, maximumFractionDigits = 9, @@ -126,6 +130,27 @@ export function camelToTitleCase(str: string): string { return result.charAt(0).toUpperCase() + result.slice(1); } +export function snakeToTitleCase(str: string): string { + const result = str.replace(/([-_]\w)/g, (g) => ` ${g[1].toUpperCase()}`); + return result.charAt(0).toUpperCase() + result.slice(1); +} + +export function snakeToPascal(string: string) { + return string + .split("/") + .map((snake) => + snake + .split("_") + .map((substr) => substr.charAt(0).toUpperCase() + substr.slice(1)) + .join("") + ) + .join("/"); +} + +export function capitalizeFirstLetter(input: string) { + return input.charAt(0).toUpperCase() + input.slice(1); +} + export function abbreviatedNumber(value: number, fixed = 1) { if (value < 1e3) return value; if (value >= 1e3 && value < 1e6) return +(value / 1e3).toFixed(fixed) + "K";