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": ">=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": {
|
"node_modules/@blockworks-foundation/mango-client/node_modules/string-width": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||||
@ -4593,11 +4588,6 @@
|
|||||||
"text-encoding-utf-8": "^1.0.2"
|
"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": {
|
"node_modules/@project-serum/anchor/node_modules/superstruct": {
|
||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
|
||||||
@ -4704,11 +4694,6 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/@project-serum/sol-wallet-adapter": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@project-serum/sol-wallet-adapter/-/sol-wallet-adapter-0.1.8.tgz",
|
"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"
|
"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": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.16.6",
|
"version": "4.16.6",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
|
||||||
@ -19555,9 +19545,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pako": {
|
"node_modules/pako": {
|
||||||
"version": "1.0.11",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
|
||||||
},
|
},
|
||||||
"node_modules/parallel-transform": {
|
"node_modules/parallel-transform": {
|
||||||
"version": "1.2.0",
|
"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",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
"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": {
|
"string-width": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||||
@ -30800,11 +30785,6 @@
|
|||||||
"text-encoding-utf-8": "^1.0.2"
|
"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": {
|
"superstruct": {
|
||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
|
||||||
@ -30880,11 +30860,6 @@
|
|||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
|
"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==",
|
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"pako": "~1.0.5"
|
"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": {
|
"browserslist": {
|
||||||
@ -42391,9 +42373,9 @@
|
|||||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
|
||||||
},
|
},
|
||||||
"pako": {
|
"pako": {
|
||||||
"version": "1.0.11",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
|
||||||
},
|
},
|
||||||
"parallel-transform": {
|
"parallel-transform": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
@ -3,6 +3,7 @@ import { Cluster } from "providers/cluster";
|
|||||||
import { TableCardBody } from "components/common/TableCardBody";
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
import { InstructionLogs } from "utils/program-logs";
|
import { InstructionLogs } from "utils/program-logs";
|
||||||
import { ProgramName } from "utils/anchor";
|
import { ProgramName } from "utils/anchor";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
export function ProgramLogsCardBody({
|
export function ProgramLogsCardBody({
|
||||||
message,
|
message,
|
||||||
|
@ -1,63 +1,62 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
import { Account } from "providers/accounts";
|
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 { 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";
|
import { useAnchorProgram } from "providers/anchor";
|
||||||
|
|
||||||
export function AnchorAccountCard({ account }: { account: Account }) {
|
export function AnchorAccountCard({ account }: { account: Account }) {
|
||||||
|
const { lamports } = account;
|
||||||
const { url } = useCluster();
|
const { url } = useCluster();
|
||||||
const program = useAnchorProgram(
|
const anchorProgram = useAnchorProgram(
|
||||||
account.details?.owner.toString() ?? "",
|
account.details?.owner.toString() || "",
|
||||||
url
|
url
|
||||||
);
|
);
|
||||||
|
const rawData = account?.details?.rawData;
|
||||||
|
const programName = getProgramName(anchorProgram) || "Unknown Program";
|
||||||
|
|
||||||
const { foundAccountLayoutName, decodedAnchorAccountData } = useMemo(() => {
|
const { decodedAccountData, accountDef } = useMemo(() => {
|
||||||
let foundAccountLayoutName: string | undefined;
|
let decodedAccountData: any | null = null;
|
||||||
let decodedAnchorAccountData: { [key: string]: any } | undefined;
|
let accountDef: IdlTypeDef | undefined = undefined;
|
||||||
if (program && account.details && account.details.rawData) {
|
if (anchorProgram && rawData) {
|
||||||
const accountBuffer = account.details.rawData;
|
const coder = new BorshAccountsCoder(anchorProgram.idl);
|
||||||
const discriminator = accountBuffer.slice(0, 8);
|
const accountDefTmp = anchorProgram.idl.accounts?.find(
|
||||||
|
(accountType: any) =>
|
||||||
// Iterate all the structs, see if any of the name-hashes match
|
(rawData as Buffer)
|
||||||
Object.keys(program.account).forEach((accountType) => {
|
.slice(0, 8)
|
||||||
const layoutName = capitalizeFirstLetter(accountType);
|
.equals(BorshAccountsCoder.accountDiscriminator(accountType.name))
|
||||||
const discriminatorToCheck =
|
);
|
||||||
BorshAccountsCoder.accountDiscriminator(layoutName);
|
if (accountDefTmp) {
|
||||||
|
accountDef = accountDefTmp;
|
||||||
if (discriminatorToCheck.equals(discriminator)) {
|
decodedAccountData = coder.decode(accountDef.name, rawData);
|
||||||
foundAccountLayoutName = layoutName;
|
}
|
||||||
const accountDecoder = program.account[accountType];
|
|
||||||
decodedAnchorAccountData = accountDecoder.coder.accounts.decode(
|
|
||||||
layoutName,
|
|
||||||
accountBuffer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
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 (
|
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 (
|
return (
|
||||||
<>
|
<div>
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<h3 className="card-header-title">{foundAccountLayoutName}</h3>
|
<h3 className="card-header-title">
|
||||||
|
{programName}: {accountDef.name}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -66,92 +65,21 @@ export function AnchorAccountCard({ account }: { account: Account }) {
|
|||||||
<table className="table table-sm table-nowrap card-table">
|
<table className="table table-sm table-nowrap card-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="w-1 text-muted">Key</th>
|
<th className="w-1">Field</th>
|
||||||
<th className="text-muted">Value</th>
|
<th className="w-1">Type</th>
|
||||||
|
<th className="w-1">Value</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="list">
|
<tbody>
|
||||||
{decodedAnchorAccountData &&
|
{mapAccountToRows(
|
||||||
Object.keys(decodedAnchorAccountData).map((key) => (
|
decodedAccountData,
|
||||||
<AccountRow
|
accountDef as IdlTypeDef,
|
||||||
key={key}
|
anchorProgram.idl
|
||||||
valueName={key}
|
)}
|
||||||
value={decodedAnchorAccountData[key]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ type Props = {
|
|||||||
truncateUnknown?: boolean;
|
truncateUnknown?: boolean;
|
||||||
truncateChars?: number;
|
truncateChars?: number;
|
||||||
useMetadata?: boolean;
|
useMetadata?: boolean;
|
||||||
|
overrideText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Address({
|
export function Address({
|
||||||
@ -29,6 +30,7 @@ export function Address({
|
|||||||
truncateUnknown,
|
truncateUnknown,
|
||||||
truncateChars,
|
truncateChars,
|
||||||
useMetadata,
|
useMetadata,
|
||||||
|
overrideText,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const address = pubkey.toBase58();
|
const address = pubkey.toBase58();
|
||||||
const { tokenRegistry } = useTokenRegistry();
|
const { tokenRegistry } = useTokenRegistry();
|
||||||
@ -52,6 +54,10 @@ export function Address({
|
|||||||
addressLabel = addressLabel.slice(0, truncateChars) + "…";
|
addressLabel = addressLabel.slice(0, truncateChars) + "…";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (overrideText) {
|
||||||
|
addressLabel = overrideText;
|
||||||
|
}
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<Copyable text={address} replaceText={!alignRight}>
|
<Copyable text={address} replaceText={!alignRight}>
|
||||||
<span className="font-monospace">
|
<span className="font-monospace">
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
|
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
|
||||||
import { InstructionCard } from "./InstructionCard";
|
import { InstructionCard } from "./InstructionCard";
|
||||||
import { Idl, Program, BorshInstructionCoder } from "@project-serum/anchor";
|
import {
|
||||||
|
Idl,
|
||||||
|
Program,
|
||||||
|
BorshInstructionCoder,
|
||||||
|
Instruction,
|
||||||
|
} from "@project-serum/anchor";
|
||||||
import {
|
import {
|
||||||
getAnchorNameForInstruction,
|
getAnchorNameForInstruction,
|
||||||
getProgramName,
|
getProgramName,
|
||||||
capitalizeFirstLetter,
|
|
||||||
getAnchorAccountsFromInstruction,
|
getAnchorAccountsFromInstruction,
|
||||||
|
mapIxArgsToRows,
|
||||||
} from "utils/anchor";
|
} from "utils/anchor";
|
||||||
import { HexData } from "components/common/HexData";
|
|
||||||
import { Address } from "components/common/Address";
|
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: {
|
export default function AnchorDetailsCard(props: {
|
||||||
key: string;
|
key: string;
|
||||||
@ -26,46 +32,99 @@ export default function AnchorDetailsCard(props: {
|
|||||||
|
|
||||||
const ixName =
|
const ixName =
|
||||||
getAnchorNameForInstruction(ix, anchorProgram) ?? "Unknown Instruction";
|
getAnchorNameForInstruction(ix, anchorProgram) ?? "Unknown Instruction";
|
||||||
const cardTitle = `${programName}: ${ixName}`;
|
const cardTitle = `${camelToTitleCase(programName)}: ${camelToTitleCase(
|
||||||
|
ixName
|
||||||
|
)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InstructionCard title={cardTitle} {...props}>
|
<InstructionCard title={cardTitle} {...props}>
|
||||||
<RawAnchorDetails ix={ix} anchorProgram={anchorProgram} />
|
<AnchorDetails ix={ix} anchorProgram={anchorProgram} />
|
||||||
</InstructionCard>
|
</InstructionCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RawAnchorDetails({
|
function AnchorDetails({
|
||||||
ix,
|
ix,
|
||||||
anchorProgram,
|
anchorProgram,
|
||||||
}: {
|
}: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
anchorProgram: Program;
|
anchorProgram: Program;
|
||||||
}) {
|
}) {
|
||||||
let ixAccounts:
|
const { ixAccounts, decodedIxData, ixDef } = useMemo(() => {
|
||||||
| {
|
let ixAccounts:
|
||||||
name: string;
|
| {
|
||||||
isMut: boolean;
|
name: string;
|
||||||
isSigner: boolean;
|
isMut: boolean;
|
||||||
pda?: Object;
|
isSigner: boolean;
|
||||||
}[]
|
pda?: Object;
|
||||||
| null = null;
|
}[]
|
||||||
var decodedIxData = null;
|
| null = null;
|
||||||
if (anchorProgram) {
|
let decodedIxData: Instruction | null = null;
|
||||||
const decoder = new BorshInstructionCoder(anchorProgram.idl);
|
let ixDef: IdlInstruction | undefined;
|
||||||
decodedIxData = decoder.decode(ix.data);
|
if (anchorProgram) {
|
||||||
ixAccounts = getAnchorAccountsFromInstruction(decodedIxData, 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 (
|
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) => {
|
{ix.keys.map(({ pubkey, isSigner, isWritable }, keyIndex) => {
|
||||||
return (
|
return (
|
||||||
<tr key={keyIndex}>
|
<tr key={keyIndex}>
|
||||||
<td>
|
<td>
|
||||||
<div className="me-2 d-md-inline">
|
<div className="me-2 d-md-inline">
|
||||||
{ixAccounts && keyIndex < ixAccounts.length
|
{ixAccounts
|
||||||
? `${capitalizeFirstLetter(ixAccounts[keyIndex].name)}`
|
? keyIndex < ixAccounts.length
|
||||||
|
? `${camelToTitleCase(ixAccounts[keyIndex].name)}`
|
||||||
|
: `Remaining Account #${keyIndex + 1 - ixAccounts.length}`
|
||||||
: `Account #${keyIndex + 1}`}
|
: `Account #${keyIndex + 1}`}
|
||||||
</div>
|
</div>
|
||||||
{isWritable && (
|
{isWritable && (
|
||||||
@ -75,27 +134,23 @@ function RawAnchorDetails({
|
|||||||
<span className="badge bg-info-soft me-1">Signer</span>
|
<span className="badge bg-info-soft me-1">Signer</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-lg-end">
|
<td className="text-lg-end" colSpan={2}>
|
||||||
<Address pubkey={pubkey} alignRight link />
|
<Address pubkey={pubkey} alignRight link />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<tr>
|
{decodedIxData && ixDef && ixDef.args.length > 0 && (
|
||||||
<td>
|
<>
|
||||||
Instruction Data <span className="text-muted">(Hex)</span>
|
<tr className="table-sep">
|
||||||
</td>
|
<td>Argument Name</td>
|
||||||
{decodedIxData ? (
|
<td>Type</td>
|
||||||
<td className="metadata-json-viewer m-4">
|
<td className="text-lg-end">Value</td>
|
||||||
<ReactJson src={decodedIxData} theme="solarized" />
|
</tr>
|
||||||
</td>
|
{mapIxArgsToRows(decodedIxData.data, ixDef, anchorProgram.idl)}
|
||||||
) : (
|
</>
|
||||||
<td className="text-lg-end">
|
)}
|
||||||
<HexData raw={ix.data} />
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -100,12 +100,16 @@ export function InstructionCard({
|
|||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
{innerCards && innerCards.length > 0 && (
|
{innerCards && innerCards.length > 0 && (
|
||||||
<tr>
|
<>
|
||||||
<td colSpan={2}>
|
<tr className="table-sep">
|
||||||
Inner Instructions
|
<td colSpan={3}>Inner Instructions</td>
|
||||||
<div className="inner-cards">{innerCards}</div>
|
</tr>
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<td colSpan={3}>
|
||||||
|
<div className="inner-cards">{innerCards}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -271,7 +271,7 @@ function DetailsSections({
|
|||||||
account. Please be cautious sending SOL to this account.
|
account. Please be cautious sending SOL to this account.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{<InfoSection account={account} />}
|
<InfoSection account={account} />
|
||||||
<MoreSection
|
<MoreSection
|
||||||
account={account}
|
account={account}
|
||||||
tab={moreTab}
|
tab={moreTab}
|
||||||
@ -517,17 +517,17 @@ function getAnchorTabs(pubkey: PublicKey, account: Account) {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const anchorAccountTab: Tab = {
|
const accountDataTab: Tab = {
|
||||||
slug: "anchor-account",
|
slug: "anchor-account",
|
||||||
title: "Anchor Account",
|
title: "Anchor Data",
|
||||||
path: "/anchor-account",
|
path: "/anchor-account",
|
||||||
};
|
};
|
||||||
tabComponents.push({
|
tabComponents.push({
|
||||||
tab: anchorAccountTab,
|
tab: accountDataTab,
|
||||||
component: (
|
component: (
|
||||||
<React.Suspense key={anchorAccountTab.slug} fallback={<></>}>
|
<React.Suspense key={accountDataTab.slug} fallback={<></>}>
|
||||||
<AnchorAccountLink
|
<AccountDataLink
|
||||||
tab={anchorAccountTab}
|
tab={accountDataTab}
|
||||||
address={pubkey.toString()}
|
address={pubkey.toString()}
|
||||||
programId={account.details?.owner}
|
programId={account.details?.owner}
|
||||||
/>
|
/>
|
||||||
@ -567,7 +567,7 @@ function AnchorProgramLink({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AnchorAccountLink({
|
function AccountDataLink({
|
||||||
address,
|
address,
|
||||||
tab,
|
tab,
|
||||||
programId,
|
programId,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
//
|
//
|
||||||
// tables.scss
|
// tables.scss
|
||||||
// Extended from Bootstrap
|
// Extended from Bootstrap
|
||||||
//
|
//
|
||||||
|
|
||||||
//
|
//
|
||||||
// Bootstrap Overrides =====================================
|
// Bootstrap Overrides =====================================
|
||||||
//
|
//
|
||||||
|
|
||||||
@ -25,6 +25,15 @@
|
|||||||
border-bottom: 0;
|
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
|
// Sizing
|
||||||
|
|
||||||
|
@ -1,43 +1,33 @@
|
|||||||
import React from "react";
|
import React, { Fragment, ReactNode, useState } from "react";
|
||||||
import { Cluster } from "providers/cluster";
|
import { Cluster } from "providers/cluster";
|
||||||
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
|
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 { useAnchorProgram } from "providers/anchor";
|
||||||
import { programLabel } from "utils/tx";
|
import { programLabel } from "utils/tx";
|
||||||
import { ErrorBoundary } from "@sentry/react";
|
import { snakeToTitleCase, camelToTitleCase, numberWithSeparator } from "utils";
|
||||||
|
import {
|
||||||
function snakeToPascal(string: string) {
|
IdlInstruction,
|
||||||
return string
|
IdlType,
|
||||||
.split("/")
|
IdlTypeDef,
|
||||||
.map((snake) =>
|
} from "@project-serum/anchor/dist/cjs/idl";
|
||||||
snake
|
import { Address } from "components/common/Address";
|
||||||
.split("_")
|
import ReactJson from "react-json-view";
|
||||||
.map((substr) => substr.charAt(0).toUpperCase() + substr.slice(1))
|
|
||||||
.join("")
|
|
||||||
)
|
|
||||||
.join("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProgramName(program: Program | null): string | undefined {
|
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) {
|
export function AnchorProgramName({
|
||||||
return input.charAt(0).toUpperCase() + input.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AnchorProgramName({
|
|
||||||
programId,
|
programId,
|
||||||
url,
|
url,
|
||||||
|
defaultName = "Unknown Program",
|
||||||
}: {
|
}: {
|
||||||
programId: PublicKey;
|
programId: PublicKey;
|
||||||
url: string;
|
url: string;
|
||||||
|
defaultName?: string;
|
||||||
}) {
|
}) {
|
||||||
const program = useAnchorProgram(programId.toString(), url);
|
const program = useAnchorProgram(programId.toString(), url);
|
||||||
if (!program) {
|
const programName = getProgramName(program) || defaultName;
|
||||||
throw new Error("No anchor program name found for given programId");
|
|
||||||
}
|
|
||||||
const programName = getProgramName(program);
|
|
||||||
return <>{programName}</>;
|
return <>{programName}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,12 +42,13 @@ export function ProgramName({
|
|||||||
}) {
|
}) {
|
||||||
const defaultProgramName =
|
const defaultProgramName =
|
||||||
programLabel(programId.toBase58(), cluster) || "Unknown Program";
|
programLabel(programId.toBase58(), cluster) || "Unknown Program";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Suspense fallback={defaultProgramName}>
|
<React.Suspense fallback={<>{defaultProgramName}</>}>
|
||||||
<ErrorBoundary fallback={<>{defaultProgramName}</>}>
|
<AnchorProgramName
|
||||||
<AnchorProgramName programId={programId} url={url} />
|
programId={programId}
|
||||||
</ErrorBoundary>
|
url={url}
|
||||||
|
defaultName={defaultProgramName}
|
||||||
|
/>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -107,3 +98,387 @@ export function getAnchorAccountsFromInstruction(
|
|||||||
}
|
}
|
||||||
return null;
|
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);
|
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({
|
export function SolBalance({
|
||||||
lamports,
|
lamports,
|
||||||
maximumFractionDigits = 9,
|
maximumFractionDigits = 9,
|
||||||
@ -126,6 +130,27 @@ export function camelToTitleCase(str: string): string {
|
|||||||
return result.charAt(0).toUpperCase() + result.slice(1);
|
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) {
|
export function abbreviatedNumber(value: number, fixed = 1) {
|
||||||
if (value < 1e3) return value;
|
if (value < 1e3) return value;
|
||||||
if (value >= 1e3 && value < 1e6) return +(value / 1e3).toFixed(fixed) + "K";
|
if (value >= 1e3 && value < 1e6) return +(value / 1e3).toFixed(fixed) + "K";
|
||||||
|
Reference in New Issue
Block a user