Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9a461da
feat: Add SPL Token batch instruction decoding
askov Mar 24, 2026
8b9830b
chore: run build:info
askov Mar 24, 2026
66efa26
refactor: Move test utilities to a dedicated file and update imports
askov Mar 24, 2026
9754ec3
fix: Update authority handling in `readOptionalAuthority`
askov Mar 24, 2026
8e7d165
refactor: Enhance SPL Token sub-instruction decoder with new SDK deco…
askov Mar 24, 2026
99ab32f
fix: Update SPL Token sub-instruction to include Token-2022 extension…
askov Mar 25, 2026
5f0b1cf
feat: Add decimal resolution for unchecked SPL Token batch instructions
askov Mar 26, 2026
ac78b41
fix: resolve review comments
askov Mar 26, 2026
48cf24a
fix: Enhance token batch instruction tests
askov Mar 26, 2026
ce263f3
refactor: improve batch decoder structure
askov Mar 26, 2026
99b7c77
chore: fix build
askov Mar 26, 2026
6a42fc6
docs: Add comment to clarify wire format for num_accounts and data_le…
askov Mar 26, 2026
dce9441
chore: run lint
askov Mar 27, 2026
d8953fc
fix: attempt to fetch the mint info if not provided
rogaldh Mar 26, 2026
88bdbb9
fix: introduce token-amount entity to handle token amount operations
rogaldh Mar 26, 2026
c6763fe
fix: use AccountsProvider via hook to reduce number of RPC calls
rogaldh Mar 27, 2026
d4dbe9f
fix: improve HexData comp to reuse it withing batch
rogaldh Mar 27, 2026
bc992c9
fix: improve batch instruction UI
rogaldh Mar 27, 2026
12f8be6
feat: use intermediate context to keep the decimals for temporary acc…
rogaldh Mar 27, 2026
0205156
fix: add badge for instruciton number to make it look better
rogaldh Mar 27, 2026
a4be381
fix: improve batch card ui
rogaldh Mar 27, 2026
6eaa147
fix: change font size
rogaldh Mar 27, 2026
94f1920
fix: codestyle
rogaldh Mar 27, 2026
9594ab5
fix: unify naming for tests
rogaldh Mar 27, 2026
e560cf9
fixes
rogaldh Mar 27, 2026
dfbfdd2
improve the impl
rogaldh Mar 27, 2026
f3fc788
fix: unify handling for token amounts
rogaldh Mar 27, 2026
6633bd4
fix: improve number formatting
rogaldh Mar 27, 2026
3c6ed4d
fix
rogaldh Mar 27, 2026
f06526a
chore: collect build info
rogaldh Mar 27, 2026
39c7b35
fix: codestyle
rogaldh Mar 27, 2026
3901024
fix: render all available data for each instruction
rogaldh Mar 27, 2026
d755f4b
fix: add asterisk to the fields with inferred values
rogaldh Mar 27, 2026
2b581e1
fix: codestyle
rogaldh Mar 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .storybook/decorators.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ClusterProvider } from '@providers/cluster';
import { TransactionsProvider } from '@providers/transactions';
import type { Decorator, Parameters } from '@storybook/react';
import React from 'react';
import { fn } from 'storybook/test';
Expand Down Expand Up @@ -40,6 +41,17 @@ export const withCardTableField: Decorator = Story => (
</ClusterProvider>
);

/** Wraps stories with ClusterProvider, TransactionsProvider, and MockAccountsProvider. Usage: `decorators: [withTransactions]` */
export const withTransactions: Decorator = Story => (
<ClusterProvider>
<TransactionsProvider>
<MockAccountsProvider>
<Story />
</MockAccountsProvider>
</TransactionsProvider>
</ClusterProvider>
);

/** Wraps stories with MockTokenInfoBatchProvider. Usage: `decorators: [withTokenInfoBatch]` */
export const withTokenInfoBatch: Decorator = Story => (
<MockTokenInfoBatchProvider>
Expand Down
4 changes: 3 additions & 1 deletion app/components/common/Address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Props = {
overrideText?: string;
tokenLabelInfo?: TokenLabelInfo;
fetchTokenLabelInfo?: boolean;
'aria-label'?: string;
};

export function Address({
Expand All @@ -43,6 +44,7 @@ export function Address({
overrideText,
tokenLabelInfo,
fetchTokenLabelInfo,
'aria-label': ariaLabel,
}: Props) {
const address = pubkey.toBase58();
const { cluster, clusterInfo } = useCluster();
Expand Down Expand Up @@ -95,7 +97,7 @@ export function Address({
};

const content = (
<div className="d-flex align-items-center gap-2">
<div className="d-flex align-items-center gap-2" aria-label={ariaLabel}>
<Copyable text={address}>
<span
data-address={address}
Expand Down
136 changes: 81 additions & 55 deletions app/components/common/BaseInstructionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type InstructionProps = {
raw?: TransactionInstruction;
// will be triggered on requesting raw data for instruction, if present
onRequestRaw?: () => void;
// Extra buttons rendered in the card header next to Raw
headerButtons?: React.ReactNode;
// Show a Collapse/Expand button that hides all card content
collapsible?: boolean;
};

export function BaseInstructionCard({
Expand All @@ -37,9 +41,12 @@ export function BaseInstructionCard({
childIndex,
raw,
onRequestRaw,
headerButtons,
collapsible = false,
}: InstructionProps) {
const [resultClass] = ixResult(result, index);
const [showRaw, setShowRaw] = React.useState(defaultRaw || false);
const [expanded, setExpanded] = React.useState(true);
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
Expand All @@ -62,63 +69,82 @@ export function BaseInstructionCard({
{title}
</h3>

<button
disabled={defaultRaw}
className={cn('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>
</>
)}
{eventCards && eventCards.length > 0 && (
<>
<tr className="table-sep">
<td colSpan={3}>Events</td>
</tr>
<tr>
<td colSpan={3}>
<div className="inner-cards">{eventCards}</div>
</td>
</tr>
</>
<div className="d-flex align-items-center gap-2">
{headerButtons}
{collapsible && (
<button
className="btn btn-sm d-flex align-items-center btn-white"
onClick={() => setExpanded(v => !v)}
>
{expanded ? 'Collapse' : 'Expand'}
</button>
)}
<button
disabled={defaultRaw || !expanded}
className={cn(
'btn btn-sm d-flex align-items-center',
showRaw ? 'btn-black active' : 'btn-white',
(defaultRaw || !expanded) && '!e-pointer-events-auto e-cursor-not-allowed',
)}
</tbody>
</table>
onClick={rawClickHandler}
>
<Code className="me-2" size={13} /> Raw
</button>
</div>
</div>
{expanded && (
<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}>
{/* !e-m-0 overrides the 1.5rem margin from inner-cards
so the card aligns with the "Inner Instructions" label above */}
<div className="inner-cards !e-m-0">{innerCards}</div>
</td>
</tr>
</>
)}
{eventCards && eventCards.length > 0 && (
<>
<tr className="table-sep">
<td colSpan={3}>Events</td>
</tr>
<tr>
<td colSpan={3}>
<div className="inner-cards">{eventCards}</div>
</td>
</tr>
</>
)}
</tbody>
</table>
</div>
)}
</div>
);
}
Expand Down
68 changes: 2 additions & 66 deletions app/components/common/HexData.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,2 @@
import React, { ReactNode } from 'react';

import { ByteArray, toHex } from '@/app/shared/lib/bytes';

import { cn } from '../shared/utils';
import { Copyable } from './Copyable';

export function HexData({
raw,
className,
copyableRaw,
}: {
raw: ByteArray;
copyableRaw?: ByteArray;
className?: string;
}) {
if (!raw || raw.length === 0) {
return <span>No data</span>;
}

const chunks = [];
const hexString = toHex(raw);
for (let i = 0; i < hexString.length; i += 2) {
chunks.push(hexString.slice(i, i + 2));
}

const SPAN_SIZE = 4;
const ROW_SIZE = 4 * SPAN_SIZE;

const divs: ReactNode[] = [];
let spans: ReactNode[] = [];
for (let i = 0; i < chunks.length; i += SPAN_SIZE) {
const color = i % (2 * SPAN_SIZE) === 0 ? 'text-white' : 'text-gray-500';
spans.push(
<span key={i} className={color}>
{chunks.slice(i, i + SPAN_SIZE).join(' ')}&emsp;
</span>,
);

if (i % ROW_SIZE === ROW_SIZE - SPAN_SIZE || i >= chunks.length - SPAN_SIZE) {
divs.push(<div key={i / ROW_SIZE}>{spans}</div>);

// clear spans
spans = [];
}
}

function Content() {
return (
<Copyable text={copyableRaw ? toHex(copyableRaw) : hexString}>
<pre className="d-inline-block text-start mb-0">{divs}</pre>
</Copyable>
);
}

return (
<>
<div className={cn('d-none d-lg-flex align-items-center justify-content-end', className)}>
<Content />
</div>
<div className={cn('d-flex d-lg-none align-items-center', className)}>
<Content />
</div>
</>
);
}
// @deprecated — Use `@shared/HexData` instead. This re-export exists for backward compatibility.
export { HexData } from '@shared/HexData';
6 changes: 6 additions & 0 deletions app/components/instruction/InstructionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type InstructionProps = {
childIndex?: number;
// Raw instruction for displaying accounts and hex data in raw mode (used by inspector)
raw?: TransactionInstruction;
headerButtons?: React.ReactNode;
collapsible?: boolean;
};

export function InstructionCard({
Expand All @@ -30,6 +32,8 @@ export function InstructionCard({
eventCards,
childIndex,
raw: rawProp,
headerButtons,
collapsible,
}: InstructionProps) {
const signature = useContext(SignatureContext);
const rawDetails = useRawTransactionDetails(signature);
Expand Down Expand Up @@ -58,6 +62,8 @@ export function InstructionCard({
childIndex={childIndex}
raw={raw}
onRequestRaw={canFetchRaw ? fetchRawTrigger : undefined}
headerButtons={headerButtons}
collapsible={collapsible}
>
{children}
</BaseInstructionCard>
Expand Down
Loading
Loading