diff --git a/explorer/package-lock.json b/explorer/package-lock.json
index f24499ffc955e4..b4324db726c208 100644
--- a/explorer/package-lock.json
+++ b/explorer/package-lock.json
@@ -1800,11 +1800,6 @@
"node": ">=8"
}
},
- "node_modules/@blockworks-foundation/mango-client/node_modules/pako": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
- "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
- },
"node_modules/@blockworks-foundation/mango-client/node_modules/string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
@@ -4593,11 +4588,6 @@
"text-encoding-utf-8": "^1.0.2"
}
},
- "node_modules/@project-serum/anchor/node_modules/pako": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.3.tgz",
- "integrity": "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw=="
- },
"node_modules/@project-serum/anchor/node_modules/superstruct": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
@@ -4704,11 +4694,6 @@
"node": ">=10"
}
},
- "node_modules/@project-serum/serum/node_modules/pako": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
- "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
- },
"node_modules/@project-serum/sol-wallet-adapter": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/@project-serum/sol-wallet-adapter/-/sol-wallet-adapter-0.1.8.tgz",
@@ -8087,6 +8072,11 @@
"pako": "~1.0.5"
}
},
+ "node_modules/browserify-zlib/node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ },
"node_modules/browserslist": {
"version": "4.16.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
@@ -19555,9 +19545,9 @@
}
},
"node_modules/pako": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
- "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
+ "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
},
"node_modules/parallel-transform": {
"version": "1.2.0",
@@ -28715,11 +28705,6 @@
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
- "pako": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
- "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
- },
"string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
@@ -30800,11 +30785,6 @@
"text-encoding-utf-8": "^1.0.2"
}
},
- "pako": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.3.tgz",
- "integrity": "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw=="
- },
"superstruct": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
@@ -30880,11 +30860,6 @@
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
- },
- "pako": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
- "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
}
}
},
@@ -33543,6 +33518,13 @@
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
"requires": {
"pako": "~1.0.5"
+ },
+ "dependencies": {
+ "pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ }
}
},
"browserslist": {
@@ -42391,9 +42373,9 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"pako": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
- "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
+ "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
},
"parallel-transform": {
"version": "1.2.0",
diff --git a/explorer/src/components/ProgramLogsCardBody.tsx b/explorer/src/components/ProgramLogsCardBody.tsx
index 69653ea8cfacb0..7839ccf69d9d04 100644
--- a/explorer/src/components/ProgramLogsCardBody.tsx
+++ b/explorer/src/components/ProgramLogsCardBody.tsx
@@ -3,6 +3,7 @@ import { Cluster } from "providers/cluster";
import { TableCardBody } from "components/common/TableCardBody";
import { InstructionLogs } from "utils/program-logs";
import { ProgramName } from "utils/anchor";
+import React from "react";
export function ProgramLogsCardBody({
message,
diff --git a/explorer/src/components/account/AnchorAccountCard.tsx b/explorer/src/components/account/AnchorAccountCard.tsx
index 74db69709696ea..95ba337c25d2aa 100644
--- a/explorer/src/components/account/AnchorAccountCard.tsx
+++ b/explorer/src/components/account/AnchorAccountCard.tsx
@@ -1,63 +1,62 @@
import React, { useMemo } from "react";
-
import { Account } from "providers/accounts";
-import { Address } from "components/common/Address";
+import { useCluster } from "providers/cluster";
import { BorshAccountsCoder } from "@project-serum/anchor";
-import { capitalizeFirstLetter } from "utils/anchor";
+import { IdlTypeDef } from "@project-serum/anchor/dist/cjs/idl";
+import { getProgramName, mapAccountToRows } from "utils/anchor";
import { ErrorCard } from "components/common/ErrorCard";
-import { PublicKey } from "@solana/web3.js";
-import BN from "bn.js";
-
-import ReactJson from "react-json-view";
-import { useCluster } from "providers/cluster";
import { useAnchorProgram } from "providers/anchor";
export function AnchorAccountCard({ account }: { account: Account }) {
+ const { lamports } = account;
const { url } = useCluster();
- const program = useAnchorProgram(
- account.details?.owner.toString() ?? "",
+ const anchorProgram = useAnchorProgram(
+ account.details?.owner.toString() || "",
url
);
+ const rawData = account?.details?.rawData;
+ const programName = getProgramName(anchorProgram) || "Unknown Program";
- const { foundAccountLayoutName, decodedAnchorAccountData } = useMemo(() => {
- let foundAccountLayoutName: string | undefined;
- let decodedAnchorAccountData: { [key: string]: any } | undefined;
- if (program && account.details && account.details.rawData) {
- const accountBuffer = account.details.rawData;
- const discriminator = accountBuffer.slice(0, 8);
-
- // Iterate all the structs, see if any of the name-hashes match
- Object.keys(program.account).forEach((accountType) => {
- const layoutName = capitalizeFirstLetter(accountType);
- const discriminatorToCheck =
- BorshAccountsCoder.accountDiscriminator(layoutName);
-
- if (discriminatorToCheck.equals(discriminator)) {
- foundAccountLayoutName = layoutName;
- const accountDecoder = program.account[accountType];
- decodedAnchorAccountData = accountDecoder.coder.accounts.decode(
- layoutName,
- accountBuffer
- );
- }
- });
+ const { decodedAccountData, accountDef } = useMemo(() => {
+ let decodedAccountData: any | null = null;
+ let accountDef: IdlTypeDef | undefined = undefined;
+ if (anchorProgram && rawData) {
+ const coder = new BorshAccountsCoder(anchorProgram.idl);
+ const accountDefTmp = anchorProgram.idl.accounts?.find(
+ (accountType: any) =>
+ (rawData as Buffer)
+ .slice(0, 8)
+ .equals(BorshAccountsCoder.accountDiscriminator(accountType.name))
+ );
+ if (accountDefTmp) {
+ accountDef = accountDefTmp;
+ decodedAccountData = coder.decode(accountDef.name, rawData);
+ }
}
- return { foundAccountLayoutName, decodedAnchorAccountData };
- }, [program, account.details]);
- if (!foundAccountLayoutName || !decodedAnchorAccountData) {
+ return {
+ decodedAccountData,
+ accountDef,
+ };
+ }, [anchorProgram, rawData]);
+
+ if (lamports === undefined) return null;
+ if (!anchorProgram) return ;
+ if (!decodedAccountData || !accountDef) {
return (
-
+
);
}
return (
- <>
+
-
{foundAccountLayoutName}
+
+ {programName}: {accountDef.name}
+
@@ -66,92 +65,21 @@ export function AnchorAccountCard({ account }: { account: Account }) {
- Key |
- Value |
+ Field |
+ Type |
+ Value |
-
- {decodedAnchorAccountData &&
- Object.keys(decodedAnchorAccountData).map((key) => (
-
- ))}
+
+ {mapAccountToRows(
+ decodedAccountData,
+ accountDef as IdlTypeDef,
+ anchorProgram.idl
+ )}
-
-
- {decodedAnchorAccountData &&
- Object.keys(decodedAnchorAccountData).length > 0
- ? `Decoded ${Object.keys(decodedAnchorAccountData).length} Items`
- : "No decoded data"}
-
-
- >
- );
-}
-
-function AccountRow({ valueName, value }: { valueName: string; value: any }) {
- let displayValue: JSX.Element;
- if (value instanceof PublicKey) {
- displayValue = ;
- } else if (value instanceof BN) {
- displayValue = <>{value.toString()}>;
- } else if (!(value instanceof Object)) {
- displayValue = <>{String(value)}>;
- } else if (value) {
- const displayObject = stringifyPubkeyAndBigNums(value);
- displayValue = (
-
- );
- } else {
- displayValue = <>null>;
- }
- return (
-
- {camelToUnderscore(valueName)} |
- {displayValue} |
-
- );
-}
-
-function camelToUnderscore(key: string) {
- var result = key.replace(/([A-Z])/g, " $1");
- return result.split(" ").join("_").toLowerCase();
-}
-
-function stringifyPubkeyAndBigNums(object: Object): Object {
- if (!Array.isArray(object)) {
- if (object instanceof PublicKey) {
- return object.toString();
- } else if (object instanceof BN) {
- return object.toString();
- } else if (!(object instanceof Object)) {
- return object;
- } else {
- const parsedObject: { [key: string]: Object } = {};
- Object.keys(object).map((key) => {
- let value = (object as { [key: string]: any })[key];
- if (value instanceof Object) {
- value = stringifyPubkeyAndBigNums(value);
- }
- parsedObject[key] = value;
- return null;
- });
- return parsedObject;
- }
- }
- return object.map((innerObject) =>
- innerObject instanceof Object
- ? stringifyPubkeyAndBigNums(innerObject)
- : innerObject
+
);
}
diff --git a/explorer/src/components/common/Address.tsx b/explorer/src/components/common/Address.tsx
index 6674e877feff70..5858e27090ee89 100644
--- a/explorer/src/components/common/Address.tsx
+++ b/explorer/src/components/common/Address.tsx
@@ -18,6 +18,7 @@ type Props = {
truncateUnknown?: boolean;
truncateChars?: number;
useMetadata?: boolean;
+ overrideText?: string;
};
export function Address({
@@ -29,6 +30,7 @@ export function Address({
truncateUnknown,
truncateChars,
useMetadata,
+ overrideText,
}: Props) {
const address = pubkey.toBase58();
const { tokenRegistry } = useTokenRegistry();
@@ -52,6 +54,10 @@ export function Address({
addressLabel = addressLabel.slice(0, truncateChars) + "…";
}
+ if (overrideText) {
+ addressLabel = overrideText;
+ }
+
const content = (
diff --git a/explorer/src/components/instruction/AnchorDetailsCard.tsx b/explorer/src/components/instruction/AnchorDetailsCard.tsx
index 596f0fefc6f791..2de9fdb7b5a45c 100644
--- a/explorer/src/components/instruction/AnchorDetailsCard.tsx
+++ b/explorer/src/components/instruction/AnchorDetailsCard.tsx
@@ -1,15 +1,21 @@
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
import { InstructionCard } from "./InstructionCard";
-import { Idl, Program, BorshInstructionCoder } from "@project-serum/anchor";
+import {
+ Idl,
+ Program,
+ BorshInstructionCoder,
+ Instruction,
+} from "@project-serum/anchor";
import {
getAnchorNameForInstruction,
getProgramName,
- capitalizeFirstLetter,
getAnchorAccountsFromInstruction,
+ mapIxArgsToRows,
} from "utils/anchor";
-import { HexData } from "components/common/HexData";
import { Address } from "components/common/Address";
-import ReactJson from "react-json-view";
+import { camelToTitleCase } from "utils";
+import { IdlInstruction } from "@project-serum/anchor/dist/cjs/idl";
+import { useMemo } from "react";
export default function AnchorDetailsCard(props: {
key: string;
@@ -26,46 +32,99 @@ export default function AnchorDetailsCard(props: {
const ixName =
getAnchorNameForInstruction(ix, anchorProgram) ?? "Unknown Instruction";
- const cardTitle = `${programName}: ${ixName}`;
+ const cardTitle = `${camelToTitleCase(programName)}: ${camelToTitleCase(
+ ixName
+ )}`;
return (
-
+
);
}
-function RawAnchorDetails({
+function AnchorDetails({
ix,
anchorProgram,
}: {
ix: TransactionInstruction;
anchorProgram: Program;
}) {
- let ixAccounts:
- | {
- name: string;
- isMut: boolean;
- isSigner: boolean;
- pda?: Object;
- }[]
- | null = null;
- var decodedIxData = null;
- if (anchorProgram) {
- const decoder = new BorshInstructionCoder(anchorProgram.idl);
- decodedIxData = decoder.decode(ix.data);
- ixAccounts = getAnchorAccountsFromInstruction(decodedIxData, anchorProgram);
+ const { ixAccounts, decodedIxData, ixDef } = useMemo(() => {
+ let ixAccounts:
+ | {
+ name: string;
+ isMut: boolean;
+ isSigner: boolean;
+ pda?: Object;
+ }[]
+ | null = null;
+ let decodedIxData: Instruction | null = null;
+ let ixDef: IdlInstruction | undefined;
+ if (anchorProgram) {
+ const coder = new BorshInstructionCoder(anchorProgram.idl);
+ decodedIxData = coder.decode(ix.data);
+ if (decodedIxData) {
+ ixDef = anchorProgram.idl.instructions.find(
+ (ixDef) => ixDef.name === decodedIxData?.name
+ );
+ if (ixDef) {
+ ixAccounts = getAnchorAccountsFromInstruction(
+ decodedIxData,
+ anchorProgram
+ );
+ }
+ }
+ }
+
+ return {
+ ixAccounts,
+ decodedIxData,
+ ixDef,
+ };
+ }, [anchorProgram, ix.data]);
+
+ if (!ixAccounts || !decodedIxData || !ixDef) {
+ return (
+
+
+ Failed to decode account data according to the public Anchor interface
+ |
+
+ );
}
+ const programName = getProgramName(anchorProgram) ?? "Unknown Program";
+
return (
<>
+
+ Program |
+
+
+ |
+
+
+ Account Name |
+
+ Address
+ |
+
{ix.keys.map(({ pubkey, isSigner, isWritable }, keyIndex) => {
return (
- {ixAccounts && keyIndex < ixAccounts.length
- ? `${capitalizeFirstLetter(ixAccounts[keyIndex].name)}`
+ {ixAccounts
+ ? keyIndex < ixAccounts.length
+ ? `${camelToTitleCase(ixAccounts[keyIndex].name)}`
+ : `Remaining Account #${keyIndex + 1 - ixAccounts.length}`
: `Account #${keyIndex + 1}`}
{isWritable && (
@@ -75,27 +134,23 @@ function RawAnchorDetails({
Signer
)}
|
-
+ |
|
);
})}
-
-
- Instruction Data (Hex)
- |
- {decodedIxData ? (
-
-
- |
- ) : (
-
-
- |
- )}
-
+ {decodedIxData && ixDef && ixDef.args.length > 0 && (
+ <>
+
+ Argument Name |
+ Type |
+ Value |
+
+ {mapIxArgsToRows(decodedIxData.data, ixDef, anchorProgram.idl)}
+ >
+ )}
>
);
}
diff --git a/explorer/src/components/instruction/InstructionCard.tsx b/explorer/src/components/instruction/InstructionCard.tsx
index c51d398e99a2de..3ed6d5fcf09841 100644
--- a/explorer/src/components/instruction/InstructionCard.tsx
+++ b/explorer/src/components/instruction/InstructionCard.tsx
@@ -100,12 +100,16 @@ export function InstructionCard({
children
)}
{innerCards && innerCards.length > 0 && (
-
-
- Inner Instructions
- {innerCards}
- |
-
+ <>
+
+ Inner Instructions |
+
+
+
+ {innerCards}
+ |
+
+ >
)}
diff --git a/explorer/src/pages/AccountDetailsPage.tsx b/explorer/src/pages/AccountDetailsPage.tsx
index 6301abc3688cfa..2b625ecdcd8984 100644
--- a/explorer/src/pages/AccountDetailsPage.tsx
+++ b/explorer/src/pages/AccountDetailsPage.tsx
@@ -271,7 +271,7 @@ function DetailsSections({
account. Please be cautious sending SOL to this account.
)}
- {}
+
>}>
- >}>
+
@@ -567,7 +567,7 @@ function AnchorProgramLink({
);
}
-function AnchorAccountLink({
+function AccountDataLink({
address,
tab,
programId,
diff --git a/explorer/src/scss/dashkit/_tables.scss b/explorer/src/scss/dashkit/_tables.scss
index e198a97ed1f1ee..2e2435bf92a3c7 100644
--- a/explorer/src/scss/dashkit/_tables.scss
+++ b/explorer/src/scss/dashkit/_tables.scss
@@ -1,9 +1,9 @@
-//
+//
// tables.scss
// Extended from Bootstrap
//
-//
+//
// Bootstrap Overrides =====================================
//
@@ -25,6 +25,15 @@
border-bottom: 0;
}
+.table-sep {
+ background-color: $table-head-bg;
+ text-transform: uppercase;
+ font-size: $font-size-xs;
+ font-weight: $font-weight-bold;
+ letter-spacing: .08em;
+ color: $table-head-color;
+}
+
// Sizing
diff --git a/explorer/src/utils/anchor.tsx b/explorer/src/utils/anchor.tsx
index 096fb9a0f05f0a..8c5d7cf0473688 100644
--- a/explorer/src/utils/anchor.tsx
+++ b/explorer/src/utils/anchor.tsx
@@ -1,43 +1,33 @@
-import React from "react";
+import React, { Fragment, ReactNode, useState } from "react";
import { Cluster } from "providers/cluster";
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
-import { BorshInstructionCoder, Program } from "@project-serum/anchor";
+import { BorshInstructionCoder, Program, Idl } from "@project-serum/anchor";
import { useAnchorProgram } from "providers/anchor";
import { programLabel } from "utils/tx";
-import { ErrorBoundary } from "@sentry/react";
-
-function snakeToPascal(string: string) {
- return string
- .split("/")
- .map((snake) =>
- snake
- .split("_")
- .map((substr) => substr.charAt(0).toUpperCase() + substr.slice(1))
- .join("")
- )
- .join("/");
-}
+import { snakeToTitleCase, camelToTitleCase, numberWithSeparator } from "utils";
+import {
+ IdlInstruction,
+ IdlType,
+ IdlTypeDef,
+} from "@project-serum/anchor/dist/cjs/idl";
+import { Address } from "components/common/Address";
+import ReactJson from "react-json-view";
export function getProgramName(program: Program | null): string | undefined {
- return program ? snakeToPascal(program.idl.name) : undefined;
-}
-
-export function capitalizeFirstLetter(input: string) {
- return input.charAt(0).toUpperCase() + input.slice(1);
+ return program ? snakeToTitleCase(program.idl.name) : undefined;
}
-function AnchorProgramName({
+export function AnchorProgramName({
programId,
url,
+ defaultName = "Unknown Program",
}: {
programId: PublicKey;
url: string;
+ defaultName?: string;
}) {
const program = useAnchorProgram(programId.toString(), url);
- if (!program) {
- throw new Error("No anchor program name found for given programId");
- }
- const programName = getProgramName(program);
+ const programName = getProgramName(program) || defaultName;
return <>{programName}>;
}
@@ -52,12 +42,13 @@ export function ProgramName({
}) {
const defaultProgramName =
programLabel(programId.toBase58(), cluster) || "Unknown Program";
-
return (
-
- {defaultProgramName}>}>
-
-
+ {defaultProgramName}>}>
+
);
}
@@ -107,3 +98,387 @@ export function getAnchorAccountsFromInstruction(
}
return null;
}
+
+export function mapIxArgsToRows(ixArgs: any, ixType: IdlInstruction, idl: Idl) {
+ return Object.entries(ixArgs).map(([key, value]) => {
+ try {
+ const fieldDef = ixType.args.find((ixDefArg) => ixDefArg.name === key);
+ if (!fieldDef) {
+ throw Error(
+ `Could not find expected ${key} field on account type definition for ${ixType.name}`
+ );
+ }
+ return mapField(key, value, fieldDef.type, idl);
+ } catch (error: any) {
+ console.log("Error while displaying IDL-based account data", error);
+ return (
+
+ {key} |
+
+ |
+
+ |
+
+
+ );
+ }
+ });
+}
+
+export function mapAccountToRows(
+ accountData: any,
+ accountType: IdlTypeDef,
+ idl: Idl
+) {
+ return Object.entries(accountData).map(([key, value]) => {
+ try {
+ if (accountType.type.kind !== "struct") {
+ throw Error(
+ `Account ${accountType.name} is of type ${accountType.type.kind} (expected: 'struct')`
+ );
+ }
+ const fieldDef = accountType.type.fields.find(
+ (ixDefArg) => ixDefArg.name === key
+ );
+ if (!fieldDef) {
+ throw Error(
+ `Could not find expected ${key} field on account type definition for ${accountType.name}`
+ );
+ }
+ return mapField(key, value as any, fieldDef.type, idl);
+ } catch (error: any) {
+ console.log("Error while displaying IDL-based account data", error);
+ return (
+
+ {key} |
+
+ |
+
+ |
+
+
+ );
+ }
+ });
+}
+
+function mapField(
+ key: string,
+ value: any,
+ type: IdlType,
+ idl: Idl,
+ keySuffix?: any,
+ nestingLevel: number = 0
+): ReactNode {
+ let itemKey = key;
+ if (/^-?\d+$/.test(keySuffix)) {
+ itemKey = `#${keySuffix}`;
+ }
+ itemKey = camelToTitleCase(itemKey);
+
+ if (value === undefined) {
+ return (
+
+ null
+
+ );
+ }
+
+ if (
+ type === "u8" ||
+ type === "i8" ||
+ type === "u16" ||
+ type === "i16" ||
+ type === "u32" ||
+ type === "i32" ||
+ type === "f32" ||
+ type === "u64" ||
+ type === "i64" ||
+ type === "f64" ||
+ type === "u128" ||
+ type === "i128"
+ ) {
+ return (
+
+ {numberWithSeparator(value.toString())}
+
+ );
+ } else if (type === "bool" || type === "bytes" || type === "string") {
+ return (
+
+ {value.toString()}
+
+ );
+ } else if (type === "publicKey") {
+ return (
+
+
+
+ );
+ } else if ("defined" in type) {
+ const fieldType = idl.types?.find((t) => t.name === type.defined);
+ if (!fieldType) {
+ throw Error(`Could not type definition for ${type.defined} field in IDL`);
+ }
+ if (fieldType.type.kind === "struct") {
+ const structFields = fieldType.type.fields;
+ return (
+
+
+ {Object.entries(value).map(
+ ([innerKey, innerValue]: [string, any]) => {
+ const innerFieldType = structFields.find(
+ (t) => t.name === innerKey
+ );
+ if (!innerFieldType) {
+ throw Error(
+ `Could not type definition for ${innerKey} field in user-defined struct ${fieldType.name}`
+ );
+ }
+ return mapField(
+ innerKey,
+ innerValue,
+ innerFieldType?.type,
+ idl,
+ key,
+ nestingLevel + 1
+ );
+ }
+ )}
+
+
+ );
+ } else {
+ const enumValue = Object.keys(value)[0];
+ return (
+
+ {camelToTitleCase(enumValue)}
+
+ );
+ }
+ } else if ("option" in type) {
+ if (value === null) {
+ return (
+
+ Not provided
+
+ );
+ }
+ return mapField(key, value, type.option, idl, key, nestingLevel);
+ } else if ("vec" in type) {
+ const itemType = type.vec;
+ return (
+
+
+ {(value as any[]).map((item, i) =>
+ mapField(key, item, itemType, idl, i, nestingLevel + 1)
+ )}
+
+
+ );
+ } else if ("array" in type) {
+ const [itemType] = type.array;
+ return (
+
+
+ {(value as any[]).map((item, i) =>
+ mapField(key, item, itemType, idl, i, nestingLevel + 1)
+ )}
+
+
+ );
+ } else {
+ console.log("Impossible type:", type);
+ return (
+
+ {camelToTitleCase(key)} |
+ |
+ ??? |
+
+ );
+ }
+}
+
+function SimpleRow({
+ rawKey,
+ type,
+ keySuffix,
+ nestingLevel = 0,
+ children,
+}: {
+ rawKey: string;
+ type: IdlType | { enum: string };
+ keySuffix?: any;
+ nestingLevel: number;
+ children?: ReactNode;
+}) {
+ let itemKey = rawKey;
+ if (/^-?\d+$/.test(keySuffix)) {
+ itemKey = `#${keySuffix}`;
+ }
+ itemKey = camelToTitleCase(itemKey);
+ return (
+
+
+ {nestingLevel > 0 && (
+
+ )}
+ {itemKey}
+ |
+ {typeDisplayName(type)} |
+ {children} |
+
+ );
+}
+
+export function ExpandableRow({
+ fieldName,
+ fieldType,
+ nestingLevel,
+ children,
+}: {
+ fieldName: string;
+ fieldType: string;
+ nestingLevel: number;
+ children: React.ReactNode;
+}) {
+ const [expanded, setExpanded] = useState(false);
+ return (
+ <>
+
+
+ {nestingLevel > 0 && (
+
+ )}
+ {fieldName}
+ |
+ {fieldType} |
+ setExpanded((current) => !current)}
+ >
+
+ {expanded ? (
+ <>
+ Collapse
+
+ >
+ ) : (
+ <>
+ Expand
+
+ >
+ )}
+
+ |
+
+ {expanded && <>{children}>}
+ >
+ );
+}
+
+function typeDisplayName(
+ type:
+ | IdlType
+ | {
+ enum: string;
+ }
+): string {
+ switch (type) {
+ case "bool":
+ case "u8":
+ case "i8":
+ case "u16":
+ case "i16":
+ case "u32":
+ case "i32":
+ case "f32":
+ case "u64":
+ case "i64":
+ case "f64":
+ case "u128":
+ case "i128":
+ case "bytes":
+ case "string":
+ return type.toString();
+ case "publicKey":
+ return "PublicKey";
+ default:
+ if ("enum" in type) return `${type.enum} (enum)`;
+ if ("defined" in type) return type.defined;
+ if ("option" in type) return `${typeDisplayName(type.option)} (optional)`;
+ if ("vec" in type) return `${typeDisplayName(type.vec)}[]`;
+ if ("array" in type)
+ return `${typeDisplayName(type.array[0])}[${type.array[1]}]`;
+ return "unkonwn";
+ }
+}
diff --git a/explorer/src/utils/index.tsx b/explorer/src/utils/index.tsx
index a9097fe573677c..ab87413af8c51f 100644
--- a/explorer/src/utils/index.tsx
+++ b/explorer/src/utils/index.tsx
@@ -56,6 +56,10 @@ export function lamportsToSolString(
return new Intl.NumberFormat("en-US", { maximumFractionDigits }).format(sol);
}
+export function numberWithSeparator(s: string) {
+ return s.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+}
+
export function SolBalance({
lamports,
maximumFractionDigits = 9,
@@ -126,6 +130,27 @@ export function camelToTitleCase(str: string): string {
return result.charAt(0).toUpperCase() + result.slice(1);
}
+export function snakeToTitleCase(str: string): string {
+ const result = str.replace(/([-_]\w)/g, (g) => ` ${g[1].toUpperCase()}`);
+ return result.charAt(0).toUpperCase() + result.slice(1);
+}
+
+export function snakeToPascal(string: string) {
+ return string
+ .split("/")
+ .map((snake) =>
+ snake
+ .split("_")
+ .map((substr) => substr.charAt(0).toUpperCase() + substr.slice(1))
+ .join("")
+ )
+ .join("/");
+}
+
+export function capitalizeFirstLetter(input: string) {
+ return input.charAt(0).toUpperCase() + input.slice(1);
+}
+
export function abbreviatedNumber(value: number, fixed = 1) {
if (value < 1e3) return value;
if (value >= 1e3 && value < 1e6) return +(value / 1e3).toFixed(fixed) + "K";