Skip to content

Commit

Permalink
Iterate on IDL account/instruction decoding (#24239)
Browse files Browse the repository at this point in the history
* Switch to more integrated Anchor data decoding

* Revert anchor account data tab and better error handling
  • Loading branch information
losman0s authored Apr 13, 2022
1 parent 96e3555 commit b4b2689
Show file tree
Hide file tree
Showing 10 changed files with 623 additions and 238 deletions.
54 changes: 18 additions & 36 deletions explorer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions explorer/src/components/ProgramLogsCardBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
166 changes: 47 additions & 119 deletions explorer/src/components/account/AnchorAccountCard.tsx
Original file line number Diff line number Diff line change
@@ -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 <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>
Expand All @@ -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>
);
}
6 changes: 6 additions & 0 deletions explorer/src/components/common/Address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Props = {
truncateUnknown?: boolean;
truncateChars?: number;
useMetadata?: boolean;
overrideText?: string;
};

export function Address({
Expand All @@ -29,6 +30,7 @@ export function Address({
truncateUnknown,
truncateChars,
useMetadata,
overrideText,
}: Props) {
const address = pubkey.toBase58();
const { tokenRegistry } = useTokenRegistry();
Expand All @@ -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">
Expand Down
Loading

1 comment on commit b4b2689

@denispalab
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for explorer ready!

✅ Preview
https://explorer-gpu8e17zi-solana-labs.vercel.app

Built with commit b4b2689.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.