Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f5cc5b6
feat: use ComputeBudgetDetailsCard to decode ComputeBudgetProgram dat…
rogaldh Feb 19, 2025
4e8731f
fix: reuse base card for instruction
rogaldh Feb 19, 2025
896a188
fix: misc
rogaldh Feb 19, 2025
bd5c446
fix: misc
rogaldh Feb 19, 2025
f59d496
WIP
rogaldh Feb 21, 2025
7d2382a
WIP
rogaldh Mar 3, 2025
3d9d0a7
WIP
rogaldh Mar 4, 2025
86f1c70
chore: remove forgotten console.log
rogaldh Mar 5, 2025
45e643c
chore: improve mocks
rogaldh Mar 5, 2025
73b4891
chore: improve existing tests according improved mocks
rogaldh Mar 5, 2025
e1c3617
feat: add AssociatedTokenDetailsCard to inspector
rogaldh Mar 5, 2025
72efc05
feat: highlight "raw" button at instruction card to make it visible
rogaldh Mar 5, 2025
fd58154
fix: ellipsis
rogaldh Mar 5, 2025
8150f18
chore: remove not needed jest-mock & update testing-library/react to …
rogaldh Mar 6, 2025
8989c78
feat: add support for create & recoverNested instructions
rogaldh Mar 7, 2025
83302ab
feat: use @solana-program/token to get proper discriminators
rogaldh Mar 7, 2025
769e679
fix: minor changes
rogaldh Mar 7, 2025
f030396
WIP
rogaldh Mar 10, 2025
21660ce
feat: use AddressLookupTable component to resolve addresses properly
rogaldh Mar 10, 2025
5223be4
fix: replace factory with separate methods
rogaldh Mar 10, 2025
d74a1ff
feat: wrap AnchorProgram card with boundary
rogaldh Mar 10, 2025
08bd1af
Merge branch 'master' into feat/decode-ata-data-at-inspector
rogaldh Mar 10, 2025
3fe3e8a
WIP
rogaldh Mar 10, 2025
0d21d50
WIP
rogaldh Mar 11, 2025
ebdc0d7
WIP
rogaldh Mar 11, 2025
8eabc41
WIP
rogaldh Mar 11, 2025
4a0c74f
feat: improve CreateIdempotentDetailsCard
rogaldh Mar 13, 2025
7c533e1
feat: improve tests to check for needed fields for each AssociatedTok…
rogaldh Mar 13, 2025
2a2045e
improve props
rogaldh Mar 13, 2025
c52a012
chore: improve tests to handle address-lookup-table addresses
rogaldh Mar 13, 2025
86f81ed
chore: improve types to allow code to be buildable
rogaldh Mar 13, 2025
f0266fd
misc
rogaldh Mar 13, 2025
bd18bf1
sync account labels
rogaldh Mar 13, 2025
dfa4bcb
chore: replace @solana/kit with web3js-experimental
rogaldh Mar 14, 2025
70faa6a
fix: override @solana/addresses version to 2.1.0 to resolve issues wi…
rogaldh Mar 14, 2025
eadf497
feat: make CreateDetailsCard similar
rogaldh Mar 14, 2025
bf86d1e
misc
rogaldh Mar 14, 2025
08c05a6
fix: remove unused component
rogaldh Mar 14, 2025
45461e5
fix: code-review
rogaldh Mar 14, 2025
705c9ec
fix: enrich account data to make cards similar
rogaldh Mar 14, 2025
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
14 changes: 14 additions & 0 deletions app/__tests__/mock-stubs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Contains serialized VersionedMessages which can be viewed at the inspector by related URLs
*/

// stub a test to not allow passing without tests
test('stub', () => expect(true).toBeTruthy());

// Message with computeBudget instructions
// http://localhost:3000/tx/inspector?message=gAIABgwLFFx82WdzBluGohZnk8TmXq%252F0pUUnxhpjxSNMraGelLVLQTka3xzxe6hY3Q5dyh8MwvAULx%252BJiPZjdz6%252FaB0L9Lqt3uRNrVn1pxcSD2DLOW0u7YcBbR9bqZdxSdFeT%252FwUlFkNon7Uoh%252BC72aIW5YavKARbD1UjdRpAiXrDl0ryvzVcRVtGJcJNXfKuwGjzfmQhFehPY9M8ebYtDAMvSK8srY9g9IH4ppqsXreT6D%252BTmlKdu2JXmEcnGtzHE8Q9OIDBkZv5SEXMv%252Fsrbpyw5vnvIzlu8X3EmssQ5s6QAAAAIyXJY9OJInxuz0QKRSODYMLWhOZ2v8QhASOe9jb6fhZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqQR51VvyMcBu7nTFbs5oFQf9sbLeo%252FSOUQKxzaJWvBOPtD%252F6J%252FXX9kp0wJsfKVh53ksJqzbfyd1RSzIap7OM5eicyfP%252FJFK6VWlYc2EGEyE78uvE0UgAvaW5%252BvYLw8BggAYGAAkDZK1tAAAAAAAGAAUCEmACAAcGAAIBEggJAQEHBgADARMICQEBCi0JAQQCChIFCwoUCQ8VDxARDw8PDw8PDw8EAwEUCQwVDA0ODAwMDAwMDAwDAgEn5RfLl3rjrSoCAAAAB2QAAQdkAQIAypo7AAAAAKeXKAIAAAAAlgBVCQMDAQEBCQLiCK3IP8r%252FwRrlsnWHJ6M%252FxtgYtM4dwqjJg6tYf5r2EgMfIKEE2QYREMI4UPEU19u%252Bw1q7jjS3B54B4IVrQrOf4fF1hSod0iZUA1eTUwA%253D
export const computeBudgetMsg = '{"header":{"numRequiredSignatures":2,"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":6},"staticAccountKeys":["kFVZ5bdn3c9tMoY4ibqsLDNd6vxt3HwHVcZC5b6ra1y","DChMF3Q8TKaRFCk6LYHcJp8w4pyHKkLjfTYCmR7iyfox","HUKaYJCzbTXN7JzgtQhGGUThmES3NeksNcbyzD4QoBoh","2PLMWNViAaRdNnBvB6zutQ9Cg5bMyWtX7BEYb15XzE8R","J1xWWZPTZDWVydStvZwVgpt2VmqqbUKGLJuP7FajGu2T","D2ckBJDNmnJJkZSz1qDLCXjhcuBiY4T9tPJZ9cpbHZ3o","ComputeBudget111111111111111111111111111111","ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL","11111111111111111111111111111111","TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4","D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf"],"recentBlockhash":"BZ3D8mjoY3Exwh31DpPzAze1aH9hRMunJg9xrT8aTnCo","compiledInstructions":[{"programIdIndex":6,"accountKeyIndexes":[],"data":{"0":3,"1":100,"2":173,"3":109,"4":0,"5":0,"6":0,"7":0,"8":0}},{"programIdIndex":6,"accountKeyIndexes":[],"data":{"0":2,"1":18,"2":96,"3":2,"4":0}},{"programIdIndex":7,"accountKeyIndexes":[0,2,1,18,8,9],"data":{"0":1}},{"programIdIndex":7,"accountKeyIndexes":[0,3,1,19,8,9],"data":{"0":1}},{"programIdIndex":10,"accountKeyIndexes":[9,1,4,2,10,18,5,11,10,20,9,15,21,15,16,17,15,15,15,15,15,15,15,15,4,3,1,20,9,12,21,12,13,14,12,12,12,12,12,12,12,12,3,2,1],"data":{"0":229,"1":23,"2":203,"3":151,"4":122,"5":227,"6":173,"7":42,"8":2,"9":0,"10":0,"11":0,"12":7,"13":100,"14":0,"15":1,"16":7,"17":100,"18":1,"19":2,"20":0,"21":202,"22":154,"23":59,"24":0,"25":0,"26":0,"27":0,"28":167,"29":151,"30":40,"31":2,"32":0,"33":0,"34":0,"35":0,"36":150,"37":0,"38":85}},{"programIdIndex":9,"accountKeyIndexes":[3,1,1],"data":{"0":9}}],"addressTableLookups":[{"accountKey":"GDLpHg53y5sufRSftvZscFMwdSqP8kHaLwhsT4ZwYSaV","writableIndexes":[31,32,161],"readonlyIndexes":[217,6,17,16]},{"accountKey":"E59uBXGqn83xN17kMbBVfU1M7T4wHG91eiygHb88Aovb","writableIndexes":[87,147,83],"readonlyIndexes":[]}]}';

// Transaction with AssociatedToken instruction
// http://localhost:3000/tx/Nwa7VZEfhbPV2MmW4dQNebj6Pw1TrJH9j2G3pL7Vt4Hr2rTzyEPdh1LBd4SKtzEBhC1MKMDEeaQuz96FZzQSyYk/inspect
export const aTokenMsg = '{"header":{"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":6,"numRequiredSignatures":1},"staticAccountKeys":["EzdQH5zUfTMGb3vwU4oumxjVcxKMDpJ6dB78pbjfHmmb","2qZ3FFpD9kPVS3MRYQNDGqfrh6JzjNpqXNp5kopv2PMk","3wrAqm2ouWgzVnk7Cxv2GbM6VUeGBtZuSEakApjQk4VZ","4ahjvr2TN7yViD8b54S1TCC2CokRfSBYErYT3Zd7rK9u","6LXAHyQ9AZs8LcQoUqpj6RswaZGxkM6vmx9CPDoiy8cd","8DKit81gfTDtTYN4SMWHwAc3UJywbHNEJThXjLrzb17s","8czgpWaHCjyXpLAUqqZqWZEw4ZYrAonfaKyLCRN2M58X","9hDBGGCrLwQtdxbw2TUzpzLEZM8psxVBhBpbJDAMibq6","9jPRczqDFNkuQyF6MBhnsZqyURUzNJzGXpNM1NxxocQF","AUN7WQptCU5qSgmDSDtiSzQivQVhmTfDndJgEtj2RDD6","AZYJEfo2mjqLV3PsCPcDjmgyDnyq9ur5KCgbpCJokXLf","Big1xZ1pnMdrjkF3buyJTC2kfKgmrRVgBN7dpekF5LCd","ENE5MsxSPqoKVg2TH2QabAKRBRwdsAq2cMiqydsnSN4s","F3AwqJLiqFUGQnfq8QVEtJQWRPztq2WmuFEgfE1L2Nb7","Fv8YYjF2DUqj9RZhyXNzXa4yR9nHHwjg5bFjA82UidF1","GjFdibHr9YDtqnKW3pc7ChzmsqWeptvVwQijAnY3LkLb","Guo7CcsFvD8BrRPppdGZ9cRjXqjNYD33nHiN2gdijzdj","Ht7TduEGJjBMDXXozEUMJPq592r8x33UXuLdD5uPFc5m","11111111111111111111111111111111","ComputeBudget111111111111111111111111111111","JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4","74SBV4zDXxTRgv1pEMoECskKBkZHc2yGPnc7GYVepump","ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL","D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf"],"recentBlockhash":"9W45XzEpCW1PNZY6xpixc1AGTSTe1DVxA3PXaBvc6met","compiledInstructions":[{"programIdIndex":19,"accountKeyIndexes":[],"data":{"type":"Buffer","data":[2,160,247,3,0]}},{"programIdIndex":22,"accountKeyIndexes":[0,14,0,21,18,26],"data":{"type":"Buffer","data":[1]}},{"programIdIndex":20,"accountKeyIndexes":[26,0,1,1,20,25,20,23,20,24,3,24,8,7,1,14,21,25,16,24,0,26,26,28,24,4,11,9,20,27,0,29,12,14,1,13,15,2,26,17,10,6,20],"data":{"type":"Buffer","data":[229,23,203,151,122,227,173,42,2,0,0,0,38,100,0,1,26,100,1,0,128,240,250,2,0,0,0,0,176,101,251,2,0,0,0,0,0,0,0]}},{"programIdIndex":18,"accountKeyIndexes":[0,5],"data":{"type":"Buffer","data":[2,0,0,0,104,243,33,0,0,0,0,0]}}],"addressTableLookups":[{"accountKey":"EDDSpjZHrsFKYTMJDcBqXAjkLcu9EKdvrQR4XnqsXErH","readonlyIndexes":[89,123,69,80,90,94],"writableIndexes":[]}]}';
75 changes: 75 additions & 0 deletions app/__tests__/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { MessageCompiledInstruction, MessageV0, PublicKey, VersionedMessage } from "@solana/web3.js";
import { useSearchParams } from 'next/navigation';

// stub a test to not allow passing without tests
test('stub', () => expect(true).toBeTruthy());

jest.mock('next/navigation');
export function mockUseSearchParams(cluster = 'mainnet-beta', customUrl?: string) {
// @ts-expect-error mockReturnValue is not present
useSearchParams.mockReturnValue({
get: (param: string) => {
if (param === 'cluster') return cluster;
return null;
},
has: (param: string) => {
if (param === 'customUrl' && customUrl) return true;
return false;
},
toString: () => {
let clusterString;
if (cluster !== 'mainnet-beta') clusterString = `cluster=${cluster}`;
if (customUrl) {
return `customUrl=${customUrl}${clusterString ? `&${clusterString}` : ''}`;
}
return clusterString ?? '';
},
});
}

export function deserializeMessageV0(message: string): VersionedMessage {
const m = JSON.parse(message);
const vm = new MessageV0({
addressTableLookups: m.addressTableLookups.map((atl: {
accountKey: string,
writableIndexes: number[],
readonlyIndexes: number[],
}) => {
return {
accountKey: new PublicKey(atl.accountKey),
readonlyIndexes: atl.readonlyIndexes,
writableIndexes: atl.writableIndexes,
};
}),
compiledInstructions: m.compiledInstructions.map((ci: {
programIdIndex: number,
accountKeyIndexes: number[],
data: { [key: string]: number } | { type: 'Buffer', data: number[] }
}) => {
let data: Uint8Array;
if ('type' in ci.data) {
data = Uint8Array.from(ci.data.data as number[]);
} else {
data = new Uint8Array([...Object.values(ci.data)]);
}

return {
accountKeyIndexes: ci.accountKeyIndexes,
data: data,
programIdIndex: ci.programIdIndex,
};
}),
header: m.header,
recentBlockhash: m.recentBlockhash,
staticAccountKeys: m.staticAccountKeys.map((sak: string) => new PublicKey(sak)),
});

return vm;
}

export function deserializeInstruction(instruction: string): MessageCompiledInstruction {
const data = JSON.parse(instruction);
data.data = Uint8Array.from(data.data.data);

return data;
}
64 changes: 64 additions & 0 deletions app/components/common/__tests__/BaseInstructionCard.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { intoTransactionInstructionFromVersionedMessage } from '@components/inspector/utils';
import { ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token';
import { render, screen } from '@testing-library/react';

import * as stubs from '@/app/__tests__/mock-stubs';
import * as mock from '@/app/__tests__/mocks';
import { ClusterProvider } from '@/app/providers/cluster';
import { ScrollAnchorProvider } from '@/app/providers/scroll-anchor';

import { BaseInstructionCard } from '../BaseInstructionCard';

describe('BaseInstructionCard', () => {
beforeEach(() => {
mock.mockUseSearchParams();
});

afterEach(() => {
jest.clearAllMocks();
});

test('should render "BaseInstructionCard"', async () => {
const index = 1;
const m = mock.deserializeMessageV0(stubs.aTokenMsg);
const ti = intoTransactionInstructionFromVersionedMessage(m.compiledInstructions[index], m);
expect(ti.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)).toBeTruthy();

// check that component is rendered properly
render(
<ScrollAnchorProvider>
<ClusterProvider>
<BaseInstructionCard ix={ti} index={index} title="Program: Instruction" result={{ err: null }} />
</ClusterProvider>
</ScrollAnchorProvider>
);
// check that card is rendered with proper title
expect(screen.getByText(/Program: Instruction/)).toBeInTheDocument();
});

test('should render "BaseInstructionCard" with raw data', async () => {
const index = 1;
const m = mock.deserializeMessageV0(stubs.aTokenMsg);
const ti = intoTransactionInstructionFromVersionedMessage(m.compiledInstructions[index], m);
expect(ti.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)).toBeTruthy();

// check that component is rendered properly
render(
<ScrollAnchorProvider>
<ClusterProvider>
<BaseInstructionCard
ix={ti}
index={index}
title="Program: Instruction"
result={{ err: null }}
defaultRaw
/>
</ClusterProvider>
</ScrollAnchorProvider>
);
// instruction should relate to specific program
expect(await screen.findAllByText(/Associated Token Program/)).toHaveLength(2);
// we expect specific internal component to be rendered with "defaultRaw"
expect(screen.getByText('Instruction Data')).toBeInTheDocument();
});
});
35 changes: 35 additions & 0 deletions app/components/common/instruction/BaseUnknownDetailsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { BaseInstructionCard } from '@components/common/BaseInstructionCard';
import { useCluster } from '@providers/cluster';
import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js';
import { getProgramName } from '@utils/tx';
import React from 'react';

export function BaseUnknownDetailsCard({
ix,
index,
result,
innerCards,
childIndex,
InstructionCardComponent = BaseInstructionCard,
}: {
ix: TransactionInstruction | ParsedInstruction;
index: number;
result: SignatureResult;
innerCards?: JSX.Element[];
childIndex?: number;
InstructionCardComponent?: React.FC<Parameters<typeof BaseInstructionCard>[0]>;
}) {
const { cluster } = useCluster();
const programName = getProgramName(ix.programId.toBase58(), cluster);
return (
<InstructionCardComponent
ix={ix}
index={index}
result={result}
title={`${programName}: Unknown Instruction`}
innerCards={innerCards}
childIndex={childIndex}
defaultRaw
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Address } from '@components/common/Address';
import { BaseInstructionCard } from '@components/common/BaseInstructionCard';
import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js';
import React from 'react';

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

export function BaseCreateDetailsCard({
ix,
index,
raw,
result,
innerCards,
childIndex,
children,
InstructionCardComponent = BaseInstructionCard,
}: {
ix: ParsedInstruction;
index: number;
raw: TransactionInstruction;
result: SignatureResult;
innerCards?: JSX.Element[];
childIndex?: number;
children?: React.ReactNode;
InstructionCardComponent?: React.FC<Parameters<typeof BaseInstructionCard>[0]>;
}) {
return (
<InstructionCardComponent
ix={ix}
index={index}
raw={raw}
result={result}
title="Associated Token Program: Create"
innerCards={innerCards}
childIndex={childIndex}
>
<tr>
<td>Program</td>
<td className="text-lg-end">
<Address pubkey={ix.programId} alignRight link />
</td>
</tr>
{children ? children : <BaseRawDetails ix={raw} />}
</InstructionCardComponent>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { BaseInstructionCard } from '@components/common/BaseInstructionCard';
import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js';
import React from 'react';

import { BaseRawDetails } from '../../BaseRawDetails';
import { CreateIdempotentInfo } from './types';

export function BaseCreateIdempotentDetailsCard(props: {
ix: ParsedInstruction;
index: number;
raw: TransactionInstruction;
result: SignatureResult;
info?: CreateIdempotentInfo;
innerCards?: JSX.Element[];
childIndex?: number;
children?: React.ReactNode;
InstructionCardComponent?: React.FC<Parameters<typeof BaseInstructionCard>[0]>;
}) {
const {
ix,
index,
raw,
result,
children,
innerCards,
childIndex,
InstructionCardComponent = BaseInstructionCard,
} = props;

return (
<InstructionCardComponent
ix={ix}
index={index}
raw={raw}
result={result}
title="Associated Token Program: Create Idempotent"
innerCards={innerCards}
childIndex={childIndex}
>
{children ? children : <BaseRawDetails ix={raw} />}
</InstructionCardComponent>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { BaseInstructionCard } from '@components/common/BaseInstructionCard';
import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js';
import React from 'react';

import { BaseRawDetails } from '../../BaseRawDetails';
import { RecoverNestedInfo } from './types';

export function BaseRecoverNestedDetailsCard(props: {
ix: ParsedInstruction;
index: number;
raw: TransactionInstruction;
result: SignatureResult;
info?: RecoverNestedInfo;
innerCards?: JSX.Element[];
childIndex?: number;
children?: React.ReactNode;
InstructionCardComponent?: React.FC<Parameters<typeof BaseInstructionCard>[0]>;
}) {
const {
ix,
index,
raw,
result,
children,
innerCards,
childIndex,
InstructionCardComponent = BaseInstructionCard,
} = props;

return (
<InstructionCardComponent
ix={ix}
index={index}
raw={raw}
result={result}
title="Associated Token Program: Recover Nested"
innerCards={innerCards}
childIndex={childIndex}
>
{children ? children : <BaseRawDetails ix={raw} />}
</InstructionCardComponent>
);
}
28 changes: 28 additions & 0 deletions app/components/common/instruction/associated-token/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable @typescript-eslint/no-redeclare */

import { PublicKeyFromString } from '@validators/pubkey';
import { enums, Infer, type } from 'superstruct';

export type CreateIdempotentInfo = Infer<typeof CreateIdempotentInfo>;
export const CreateIdempotentInfo = type({
account: PublicKeyFromString,
mint: PublicKeyFromString,
source: PublicKeyFromString,
systemProgram: PublicKeyFromString,
tokenProgram: PublicKeyFromString,
wallet: PublicKeyFromString,
});

export type RecoverNestedInfo = Infer<typeof RecoverNestedInfo>;
export const RecoverNestedInfo = type({
destination: PublicKeyFromString,
nestedMint: PublicKeyFromString,
nestedOwner: PublicKeyFromString,
nestedSource: PublicKeyFromString,
ownerMint: PublicKeyFromString,
tokenProgram: PublicKeyFromString,
wallet: PublicKeyFromString,
});

export type SystemInstructionType = Infer<typeof SystemInstructionType>;
export const SystemInstructionType = enums(['create', 'createIdempotent', 'recoverNested']);
Loading