Iterate on IDL account/instruction decoding (#24239)

* Switch to more integrated Anchor data decoding

* Revert anchor account data tab and better error handling
This commit is contained in:
man0s 2022-04-14 03:38:59 +08:00 committed by GitHub
parent 96e3555e93
commit b4b26894cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 624 additions and 239 deletions

View File

@ -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",

View File

@ -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,

View File

@ -1,63 +1,62 @@
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 { BorshAccountsCoder } from "@project-serum/anchor";
import { IdlTypeDef } from "@project-serum/anchor/dist/cjs/idl";
import { getProgramName, mapAccountToRows } from "utils/anchor";
import { ErrorCard } from "components/common/ErrorCard";
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 <ErrorCard text="No Anchor IDL found" />;
if (!decodedAccountData || !accountDef) {
return (
<ErrorCard text="Failed to decode account data according to its public anchor interface" />
<ErrorCard text="Failed to decode account data according to the public Anchor interface" />
);
}
return (
<>
<div>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">{foundAccountLayoutName}</h3>
<h3 className="card-header-title">
{programName}: {accountDef.name}
</h3>
</div>
</div>
</div>
@ -66,92 +65,21 @@ export function AnchorAccountCard({ account }: { account: Account }) {
<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>
<th className="w-1">Field</th>
<th className="w-1">Type</th>
<th className="w-1">Value</th>
</tr>
</thead>
<tbody className="list">
{decodedAnchorAccountData &&
Object.keys(decodedAnchorAccountData).map((key) => (
<AccountRow
key={key}
valueName={key}
value={decodedAnchorAccountData[key]}
/>
))}
<tbody>
{mapAccountToRows(
decodedAccountData,
accountDef as IdlTypeDef,
anchorProgram.idl
)}
</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
</div>
);
}

View File

@ -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 = (
<Copyable text={address} replaceText={!alignRight}>
<span className="font-monospace">

View File

@ -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 (
<InstructionCard title={cardTitle} {...props}>
<RawAnchorDetails ix={ix} anchorProgram={anchorProgram} />
<AnchorDetails ix={ix} anchorProgram={anchorProgram} />
</InstructionCard>
);
}
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 (
<tr>
<td colSpan={3} className="text-lg-center">
Failed to decode account data according to the public Anchor interface
</td>
</tr>
);
}
const programName = getProgramName(anchorProgram) ?? "Unknown Program";
return (
<>
<tr>
<td>Program</td>
<td className="text-lg-end" colSpan={2}>
<Address
pubkey={ix.programId}
alignRight
link
raw
overrideText={programName}
/>
</td>
</tr>
<tr className="table-sep">
<td>Account Name</td>
<td className="text-lg-end" colSpan={2}>
Address
</td>
</tr>
{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)}`
{ixAccounts
? keyIndex < ixAccounts.length
? `${camelToTitleCase(ixAccounts[keyIndex].name)}`
: `Remaining Account #${keyIndex + 1 - ixAccounts.length}`
: `Account #${keyIndex + 1}`}
</div>
{isWritable && (
@ -75,27 +134,23 @@ function RawAnchorDetails({
<span className="badge bg-info-soft me-1">Signer</span>
)}
</td>
<td className="text-lg-end">
<td className="text-lg-end" colSpan={2}>
<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>
{decodedIxData && ixDef && ixDef.args.length > 0 && (
<>
<tr className="table-sep">
<td>Argument Name</td>
<td>Type</td>
<td className="text-lg-end">Value</td>
</tr>
{mapIxArgsToRows(decodedIxData.data, ixDef, anchorProgram.idl)}
</>
)}
</>
);
}

View File

@ -100,12 +100,16 @@ export function InstructionCard({
children
)}
{innerCards && innerCards.length > 0 && (
<tr>
<td colSpan={2}>
Inner Instructions
<div className="inner-cards">{innerCards}</div>
</td>
</tr>
<>
<tr className="table-sep">
<td colSpan={3}>Inner Instructions</td>
</tr>
<tr>
<td colSpan={3}>
<div className="inner-cards">{innerCards}</div>
</td>
</tr>
</>
)}
</tbody>
</table>

View File

@ -271,7 +271,7 @@ function DetailsSections({
account. Please be cautious sending SOL to this account.
</div>
)}
{<InfoSection account={account} />}
<InfoSection account={account} />
<MoreSection
account={account}
tab={moreTab}
@ -517,17 +517,17 @@ function getAnchorTabs(pubkey: PublicKey, account: Account) {
),
});
const anchorAccountTab: Tab = {
const accountDataTab: Tab = {
slug: "anchor-account",
title: "Anchor Account",
title: "Anchor Data",
path: "/anchor-account",
};
tabComponents.push({
tab: anchorAccountTab,
tab: accountDataTab,
component: (
<React.Suspense key={anchorAccountTab.slug} fallback={<></>}>
<AnchorAccountLink
tab={anchorAccountTab}
<React.Suspense key={accountDataTab.slug} fallback={<></>}>
<AccountDataLink
tab={accountDataTab}
address={pubkey.toString()}
programId={account.details?.owner}
/>
@ -567,7 +567,7 @@ function AnchorProgramLink({
);
}
function AnchorAccountLink({
function AccountDataLink({
address,
tab,
programId,

View File

@ -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

View File

@ -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;
return program ? snakeToTitleCase(program.idl.name) : undefined;
}
export function capitalizeFirstLetter(input: string) {
return input.charAt(0).toUpperCase() + input.slice(1);
}
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 (
<React.Suspense fallback={defaultProgramName}>
<ErrorBoundary fallback={<>{defaultProgramName}</>}>
<AnchorProgramName programId={programId} url={url} />
</ErrorBoundary>
<React.Suspense fallback={<>{defaultProgramName}</>}>
<AnchorProgramName
programId={programId}
url={url}
defaultName={defaultProgramName}
/>
</React.Suspense>
);
}
@ -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 (
<tr key={key}>
<td>{key}</td>
<td className="text-lg-end">
<td className="metadata-json-viewer m-4">
<ReactJson src={ixArgs} theme="solarized" />
</td>
</td>
</tr>
);
}
});
}
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 (
<tr key={key}>
<td>{key}</td>
<td className="text-lg-end">
<td className="metadata-json-viewer m-4">
<ReactJson src={accountData} theme="solarized" />
</td>
</td>
</tr>
);
}
});
}
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 (
<SimpleRow
key={keySuffix ? `${key}-${keySuffix}` : key}
rawKey={key}
type={type}
keySuffix={keySuffix}
nestingLevel={nestingLevel}
>
<div>null</div>
</SimpleRow>
);
}
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 (
<SimpleRow
key={keySuffix ? `${key}-${keySuffix}` : key}
rawKey={key}
type={type}
keySuffix={keySuffix}
nestingLevel={nestingLevel}
>
<div>{numberWithSeparator(value.toString())}</div>
</SimpleRow>
);
} else if (type === "bool" || type === "bytes" || type === "string") {
return (
<SimpleRow
key={keySuffix ? `${key}-${keySuffix}` : key}
rawKey={key}
type={type}
keySuffix={keySuffix}
nestingLevel={nestingLevel}
>
<div>{value.toString()}</div>
</SimpleRow>
);
} else if (type === "publicKey") {
return (
<SimpleRow
key={keySuffix ? `${key}-${keySuffix}` : key}
rawKey={key}
type={type}
keySuffix={keySuffix}
nestingLevel={nestingLevel}
>
<Address pubkey={value} link alignRight />
</SimpleRow>
);
} 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 (
<ExpandableRow
fieldName={itemKey}
fieldType={typeDisplayName(type)}
nestingLevel={nestingLevel}
key={keySuffix ? `${key}-${keySuffix}` : key}
>
<Fragment key={keySuffix ? `${key}-${keySuffix}` : key}>
{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
);
}
)}
</Fragment>
</ExpandableRow>
);
} else {
const enumValue = Object.keys(value)[0];
return (
<SimpleRow
key={keySuffix ? `${key}-${keySuffix}` : key}
rawKey={key}
type={{ enum: type.defined }}
keySuffix={keySuffix}
nestingLevel={nestingLevel}
>
{camelToTitleCase(enumValue)}
</SimpleRow>
);
}
} else if ("option" in type) {
if (value === null) {
return (
<SimpleRow
key={keySuffix ? `${key}-${keySuffix}` : key}
rawKey={key}
type={type}
keySuffix={keySuffix}
nestingLevel={nestingLevel}
>
Not provided
</SimpleRow>
);
}
return mapField(key, value, type.option, idl, key, nestingLevel);
} else if ("vec" in type) {
const itemType = type.vec;
return (
<ExpandableRow
fieldName={itemKey}
fieldType={typeDisplayName(type)}
nestingLevel={nestingLevel}
key={keySuffix ? `${key}-${keySuffix}` : key}
>
<Fragment key={keySuffix ? `${key}-${keySuffix}` : key}>
{(value as any[]).map((item, i) =>
mapField(key, item, itemType, idl, i, nestingLevel + 1)
)}
</Fragment>
</ExpandableRow>
);
} else if ("array" in type) {
const [itemType] = type.array;
return (
<ExpandableRow
fieldName={itemKey}
fieldType={typeDisplayName(type)}
nestingLevel={nestingLevel}
key={keySuffix ? `${key}-${keySuffix}` : key}
>
<Fragment key={keySuffix ? `${key}-${keySuffix}` : key}>
{(value as any[]).map((item, i) =>
mapField(key, item, itemType, idl, i, nestingLevel + 1)
)}
</Fragment>
</ExpandableRow>
);
} else {
console.log("Impossible type:", type);
return (
<tr key={keySuffix ? `${key}-${keySuffix}` : key}>
<td>{camelToTitleCase(key)}</td>
<td></td>
<td className="text-lg-end">???</td>
</tr>
);
}
}
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 (
<tr
style={{
...(nestingLevel === 0 ? {} : { backgroundColor: "#141816" }),
}}
>
<td className="d-flex flex-row">
{nestingLevel > 0 && (
<span
className="text-info fe fe-corner-down-right me-2"
style={{
paddingLeft: `${15 * nestingLevel}px`,
}}
/>
)}
<div>{itemKey}</div>
</td>
<td>{typeDisplayName(type)}</td>
<td className="text-lg-end">{children}</td>
</tr>
);
}
export function ExpandableRow({
fieldName,
fieldType,
nestingLevel,
children,
}: {
fieldName: string;
fieldType: string;
nestingLevel: number;
children: React.ReactNode;
}) {
const [expanded, setExpanded] = useState(false);
return (
<>
<tr
style={{
...(nestingLevel === 0 ? {} : { backgroundColor: "#141816" }),
}}
>
<td className="d-flex flex-row">
{nestingLevel > 0 && (
<div
className="text-info fe fe-corner-down-right me-2"
style={{
paddingLeft: `${15 * nestingLevel}px`,
}}
/>
)}
<div>{fieldName}</div>
</td>
<td>{fieldType}</td>
<td
className="text-lg-end"
onClick={() => setExpanded((current) => !current)}
>
<div className="c-pointer">
{expanded ? (
<>
<span className="text-info me-2">Collapse</span>
<span className="fe fe-chevron-up" />
</>
) : (
<>
<span className="text-info me-2">Expand</span>
<span className="fe fe-chevron-down" />
</>
)}
</div>
</td>
</tr>
{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";
}
}

View File

@ -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";