Skip to content

Commit

Permalink
feat: decode ComputeBudget data at inspector (#466)
Browse files Browse the repository at this point in the history
PR allows to use ComputeBudgetDetailsCard at the inspector page.
This is achieved by allowing to specify component that would be used to
decode the data.
<!-- ELLIPSIS_HIDDEN -->


----

> [!IMPORTANT]
> Adds `BaseInstructionCard` for instruction decoding and integrates
`ComputeBudgetDetailsCard` into the inspector with specific handling for
ComputeBudget instructions.
> 
>   - **Behavior**:
> - Introduces `BaseInstructionCard` in `BaseInstructionCard.tsx` for
decoding and displaying instruction data.
> - Integrates `ComputeBudgetDetailsCard` into the inspector via
`InstructionsSection.tsx` and `InspectorInstructionCard`.
> - Handles ComputeBudget instructions specifically, with fallback to
`UnknownDetailsCard` for others.
>   - **Refactoring**:
> - Renames `RawDetails` to `BaseRawDetails` and `RawParsedDetails` to
`BaseRawParsedDetails`.
>     - Refactors `InstructionCard` to use `BaseInstructionCard`.
>   - **Testing**:
> - Adds tests for `ComputeBudgetDetailsCard` in
`ComputeBudgetDetailsCard.spec.tsx`.
> - Adds utility tests in
`into-transaction-instruction-from-versioned-message.spec.tsx`.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=solana-foundation%2Fexplorer&utm_source=github&utm_medium=referral)<sup>
for 159823e. It will automatically
update as commits are pushed.</sup>


<!-- ELLIPSIS_HIDDEN -->
  • Loading branch information
rogaldh authored Feb 27, 2025
1 parent d68f9b2 commit 940fdd4
Show file tree
Hide file tree
Showing 12 changed files with 556 additions and 233 deletions.
124 changes: 124 additions & 0 deletions app/components/common/BaseInstructionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Address } from '@components/common/Address';
import { useScrollAnchor } from '@providers/scroll-anchor';
import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js';
import getInstructionCardScrollAnchorId from '@utils/get-instruction-card-scroll-anchor-id';
import React from 'react';
import { Code } from 'react-feather';

import { BaseRawDetails } from './BaseRawDetails';
import { BaseRawParsedDetails } from './BaseRawParsedDetails';

type InstructionProps = {
title: string;
children?: React.ReactNode;
result: SignatureResult;
index: number;
ix: TransactionInstruction | ParsedInstruction;
defaultRaw?: boolean;
innerCards?: JSX.Element[];
childIndex?: number;
// raw can be used to display raw instruction information
raw?: TransactionInstruction;
// will be triggered on requesting raw data for instruction, if present
onRequestRaw?: () => void;
};

export function BaseInstructionCard({
title,
children,
result,
index,
ix,
defaultRaw,
innerCards,
childIndex,
raw,
onRequestRaw,
}: InstructionProps) {
const [resultClass] = ixResult(result, index);
const [showRaw, setShowRaw] = React.useState(defaultRaw || false);
const rawClickHandler = () => {
if (!defaultRaw && !showRaw && !raw) {
// trigger handler to simulate behaviour for the InstructionCard for the transcation which contains logic in it to fetch raw transaction data
onRequestRaw?.();
}

return setShowRaw(r => !r);
};
const scrollAnchorRef = useScrollAnchor(
getInstructionCardScrollAnchorId(childIndex != null ? [index + 1, childIndex + 1] : [index + 1])
);
return (
<div className="card" ref={scrollAnchorRef}>
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
<span className={`badge bg-${resultClass}-soft me-2`}>
#{index + 1}
{childIndex !== undefined ? `.${childIndex + 1}` : ''}
</span>
{title}
</h3>

<button
disabled={defaultRaw}
className={`btn btn-sm d-flex align-items-center ${showRaw ? 'btn-black active' : 'btn-white'}`}
onClick={rawClickHandler}
>
<Code className="me-2" size={13} /> Raw
</button>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<tbody className="list">
{showRaw ? (
<>
<tr>
<td>Program</td>
<td className="text-lg-end">
<Address pubkey={ix.programId} alignRight link />
</td>
</tr>
{'parsed' in ix ? (
<BaseRawParsedDetails ix={ix}>
{raw ? <BaseRawDetails ix={raw} /> : null}
</BaseRawParsedDetails>
) : (
<BaseRawDetails ix={ix} />
)}
</>
) : (
children
)}
{innerCards && innerCards.length > 0 && (
<>
<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>
</div>
</div>
);
}

function ixResult(result: SignatureResult, index: number) {
if (result.err) {
const err = result.err as any;
const ixError = err['InstructionError'];
if (ixError && Array.isArray(ixError)) {
const [errorIndex, error] = ixError;
if (Number.isInteger(errorIndex) && errorIndex === index) {
return ['warning', `Error: ${JSON.stringify(error)}`];
}
}
return ['dark'];
}
return ['success'];
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Address } from '@components/common/Address';
import { HexData } from '@components/common/HexData';
import { TransactionInstruction } from '@solana/web3.js';
import React from 'react';

export function RawDetails({ ix }: { ix: TransactionInstruction }) {
import { HexData } from './HexData';

export function BaseRawDetails({ ix }: { ix: TransactionInstruction }) {
return (
<>
{ix.keys.map(({ pubkey, isSigner, isWritable }, keyIndex) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ParsedInstruction } from '@solana/web3.js';
import React from 'react';

export function RawParsedDetails({ ix, children }: { ix: ParsedInstruction; children?: React.ReactNode }) {
export function BaseRawParsedDetails({ ix, children }: { ix: ParsedInstruction; children?: React.ReactNode }) {
return (
<>
{children}
Expand Down
159 changes: 32 additions & 127 deletions app/components/inspector/InstructionsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,27 @@
import { HexData } from '@components/common/HexData';
import { TableCardBody } from '@components/common/TableCardBody';
import { useCluster } from '@providers/cluster';
import { useScrollAnchor } from '@providers/scroll-anchor';
import {
AccountMeta,
MessageCompiledInstruction,
PublicKey,
TransactionInstruction,
VersionedMessage,
} from '@solana/web3.js';
import getInstructionCardScrollAnchorId from '@utils/get-instruction-card-scroll-anchor-id';
import { ComputeBudgetProgram, MessageCompiledInstruction, VersionedMessage } from '@solana/web3.js';
import { getProgramName } from '@utils/tx';
import React from 'react';

import { useAnchorProgram } from '@/app/providers/anchor';

import { BaseInstructionCard } from '../common/BaseInstructionCard';
import AnchorDetailsCard from '../instruction/AnchorDetailsCard';
import { AddressFromLookupTableWithContext, AddressWithContext, programValidator } from './AddressWithContext';
import { ComputeBudgetDetailsCard } from '../instruction/ComputeBudgetDetailsCard';
import { UnknownDetailsCard } from './UnknownDetailsCard';
import { intoTransactionInstructionFromVersionedMessage } from './utils';

export function InstructionsSection({ message }: { message: VersionedMessage }) {
return (
<>
{message.compiledInstructions.map((ix, index) => {
return <InstructionCard key={index} {...{ index, ix, message }} />;
return <InspectorInstructionCard key={index} {...{ index, ix, message }} />;
})}
</>
);
}

function InstructionCard({
function InspectorInstructionCard({
message,
ix,
index,
Expand All @@ -37,53 +30,14 @@ function InstructionCard({
ix: MessageCompiledInstruction;
index: number;
}) {
const [expanded, setExpanded] = React.useState(false);
const { cluster, url } = useCluster();
const programId = message.staticAccountKeys[ix.programIdIndex];
const programName = getProgramName(programId.toBase58(), cluster);
const scrollAnchorRef = useScrollAnchor(getInstructionCardScrollAnchorId([index + 1]));
const lookupsForAccountKeyIndex = [
...message.addressTableLookups.flatMap(lookup =>
lookup.writableIndexes.map(index => ({
lookupTableIndex: index,
lookupTableKey: lookup.accountKey,
}))
),
...message.addressTableLookups.flatMap(lookup =>
lookup.readonlyIndexes.map(index => ({
lookupTableIndex: index,
lookupTableKey: lookup.accountKey,
}))
),
];
const anchorProgram = useAnchorProgram(programId.toString(), url);

if (anchorProgram.program) {
const accountMetas = ix.accountKeyIndexes.map((accountIndex, _index) => {
let lookup: PublicKey;
if (accountIndex >= message.staticAccountKeys.length) {
const lookupIndex = accountIndex - message.staticAccountKeys.length;
lookup = lookupsForAccountKeyIndex[lookupIndex].lookupTableKey;
} else {
lookup = message.staticAccountKeys[accountIndex];
}

const isSigner = accountIndex < message.header.numRequiredSignatures;
const isWritable = message.isAccountWritable(accountIndex);
const accountMeta: AccountMeta = {
isSigner,
isWritable,
pubkey: lookup,
};
return accountMeta;
});

const transactionInstruction: TransactionInstruction = new TransactionInstruction({
data: Buffer.from(message.compiledInstructions[index].data),
keys: accountMetas,
programId: programId,
});
const transactionInstruction = intoTransactionInstructionFromVersionedMessage(ix, message, programId);

if (anchorProgram.program) {
return AnchorDetailsCard({
anchorProgram: anchorProgram.program,
childIndex: undefined,
Expand All @@ -103,77 +57,28 @@ function InstructionCard({
});
}

return (
<div className="card" key={index} ref={scrollAnchorRef}>
<div className={`card-header${!expanded ? ' border-bottom-none' : ''}`}>
<h3 className="card-header-title mb-0 d-flex align-items-center">
<span className={`badge bg-info-soft me-2`}>#{index + 1}</span>
{programName} Instruction
</h3>

<button
className={`btn btn-sm d-flex ${expanded ? 'btn-black active' : 'btn-white'}`}
onClick={() => setExpanded(e => !e)}
>
{expanded ? 'Collapse' : 'Expand'}
</button>
</div>
{expanded && (
<TableCardBody>
<tr>
<td>Program</td>
<td className="text-lg-end">
<AddressWithContext
pubkey={message.staticAccountKeys[ix.programIdIndex]}
validator={programValidator}
/>
</td>
</tr>
{ix.accountKeyIndexes.map((accountIndex, index) => {
let lookup;
if (accountIndex >= message.staticAccountKeys.length) {
const lookupIndex = accountIndex - message.staticAccountKeys.length;
lookup = lookupsForAccountKeyIndex[lookupIndex];
}
/// Handle program-specific cards here
// - keep signature (empty string as we do not submit anything) for backward compatibility with the data from Transaction
// - result is `err: null` as at this point there should not be errors
const result = { err: null };
const signature = '';
switch (transactionInstruction?.programId.toString()) {
case ComputeBudgetProgram.programId.toString(): {
return (
<ComputeBudgetDetailsCard
key={index}
ix={transactionInstruction}
index={index}
result={result}
signature={signature}
InstructionCardComponent={BaseInstructionCard}
/>
);
}
default: {
// unknown program; allow to render the next card
}
}

return (
<tr key={index}>
<td>
<div className="d-flex align-items-start flex-column">
Account #{index + 1}
<span className="mt-1">
{accountIndex < message.header.numRequiredSignatures && (
<span className="badge bg-info-soft me-2">Signer</span>
)}
{message.isAccountWritable(accountIndex) && (
<span className="badge bg-danger-soft me-2">Writable</span>
)}
</span>
</div>
</td>
<td className="text-lg-end">
{lookup === undefined ? (
<AddressWithContext pubkey={message.staticAccountKeys[accountIndex]} />
) : (
<AddressFromLookupTableWithContext
lookupTableKey={lookup.lookupTableKey}
lookupTableIndex={lookup.lookupTableIndex}
/>
)}
</td>
</tr>
);
})}
<tr>
<td>
Instruction Data <span className="text-muted">(Hex)</span>
</td>
<td className="text-lg-end">
<HexData raw={Buffer.from(ix.data)} />
</td>
</tr>
</TableCardBody>
)}
</div>
);
return <UnknownDetailsCard key={index} index={index} ix={ix} message={message} programName={programName} />;
}
Loading

0 comments on commit 940fdd4

Please sign in to comment.