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:
54
explorer/package-lock.json
generated
54
explorer/package-lock.json
generated
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
Reference in New Issue
Block a user