Skip to content

Commit e749f18

Browse files
authored
feat: Display Token::Transfer, Token::TransferChecked and Token::SyncNative at Inspector (#511)
## Description PR fix parsing for TokenProgram's Instruction at Inspector: - Transfer - TransferChecked - SyncNative Several improvements also been made: - Display "Program" field for several cards in all modes - - Extended information about the owner program - Mock ALTs to not fetch them - Improve tests for Instruction Cards ## Type of change - [x] Bug fix - [x] New feature ## Screenshots BEFORE | AFTER | Link -- | -- | -- <img width="1037" height="857" alt="image" src="https://github.com/user-attachments/assets/04bac1bd-bb9d-4f36-b836-73235efdf11b" /> | <img width="1012" height="900" alt="image" src="https://github.com/user-attachments/assets/c72df96e-1cfb-4cf6-8197-f6ad6d375602" /> | [Link](http://localhost:3000/tx/28Z5go9RAfm8CywJHZ2kxXqyxmeAA4pacZgbKTourr4QjB7NpXozYwmctgDcR5qy5rFiGYSr4eizeMLt9BePQNRA/inspect) <img width="1006" height="864" alt="image" src="https://github.com/user-attachments/assets/ad674401-ccac-4f70-ae4b-31b01891e358" /> | <img width="1008" height="872" alt="image" src="https://github.com/user-attachments/assets/c13746d3-1e64-4096-97f1-63c0bd7394d0" /> | [Link](http://localhost:3000/tx/28Z5go9RAfm8CywJHZ2kxXqyxmeAA4pacZgbKTourr4QjB7NpXozYwmctgDcR5qy5rFiGYSr4eizeMLt9BePQNRA/inspect) <img width="1044" height="132" alt="image" src="https://github.com/user-attachments/assets/b7898021-9c5c-4be6-bed9-29bacfd0b219" /> | <img width="1009" height="469" alt="image" src="https://github.com/user-attachments/assets/e394e9c0-d56d-49ff-bee6-3880c3cf1f6a" /> | [Link](http://localhost:3000/tx/3b5pymMQPmkTje12xJLQrbnNWmEDE8CVZqEot8iPdT9gmFetDFBJfqgyCM9V3fiVYDkTwmCTAVE5bzKvDa8iZMJg/inspect#4) ## Testing `pnpm t` ## Related Issues [HOO-237](https://linear.app/solana-fndn/issue/HOO-237) ## Checklist - [x] My code follows the project's style guidelines - [x] I have added tests that prove my fix/feature works - [x] All tests pass locally and in CI - [x] CI/CD checks pass - [x] I have included screenshots for protocol screens (if applicable) <!-- ELLIPSIS_HIDDEN --> ---- > [!IMPORTANT] > Add support for parsing and displaying `Token::Transfer`, `Token::TransferChecked`, and `Token::SyncNative` instructions in the Inspector, with additional improvements and tests. > > - **Behavior**: > - Add parsing for `Token::Transfer`, `Token::TransferChecked`, and `Token::SyncNative` in `spl-token.parser.ts`. > - Display "Program" field in `InspectorInstructionCard` and `UnknownDetailsCard`. > - **Components**: > - Add `ProgramField` component in `ProgramField.tsx` for displaying program information. > - Update `BaseRawDetails` to handle loading state when `ix` is undefined or has no keys. > - **Tests**: > - Add tests for `BaseInstructionCard`, `AccountsCard`, `AssociatedTokenDetailsCard`, `ComputeBudgetDetailsCard`, `SystemDetailsCard`, and `TokenDetailsCard`. > - Add Storybook stories for `BaseRawDetails` and `ProgramField`. > - **Misc**: > - Mock accounts in `MockAccountsProvider.tsx` to avoid network requests in tests. > - Update `BUILD.md` with new route sizes and first load JS metrics. > > <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 1a80ba9. You can [customize](https://app.ellipsis.dev/solana-foundation/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> <!-- ELLIPSIS_HIDDEN -->
1 parent 29fe475 commit e749f18

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1168
-228
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
# next.js
1212
/.next/
13+
/.next-dev/
1314
/out/
1415

1516
# production
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Account, DispatchContext, FetchersContext, State, StateContext } from '@providers/accounts';
2+
import { TOKEN_PROGRAM_ID } from '@providers/accounts/tokens';
3+
import { PublicKey, SystemProgram } from '@solana/web3.js';
4+
import React from 'react';
5+
6+
import { FetchStatus } from '@/app/providers/cache';
7+
import { MAINNET_BETA_URL } from '@/app/utils/cluster';
8+
import { LOADER_IDS } from '@/app/utils/programs';
9+
10+
// BPF Loader 2 program ID - using key from LOADER_IDS
11+
const BPF_LOADER_2_KEY = Object.keys(LOADER_IDS).find(key => LOADER_IDS[key] === 'BPF Loader 2')!;
12+
const BPF_LOADER_2 = new PublicKey(BPF_LOADER_2_KEY);
13+
14+
// Mock account data for programs
15+
const createMockProgramAccount = (pubkey: PublicKey): Account => ({
16+
pubkey,
17+
lamports: 5_542_247_638, // ~5.54 SOL
18+
executable: true,
19+
owner: BPF_LOADER_2,
20+
space: 36,
21+
data: {},
22+
});
23+
24+
// Pre-populated mock entries for known programs
25+
const mockState: State = {
26+
url: MAINNET_BETA_URL,
27+
entries: {
28+
[SystemProgram.programId.toBase58()]: {
29+
data: createMockProgramAccount(SystemProgram.programId),
30+
status: FetchStatus.Fetched,
31+
},
32+
[TOKEN_PROGRAM_ID.toBase58()]: {
33+
data: createMockProgramAccount(TOKEN_PROGRAM_ID),
34+
status: FetchStatus.Fetched,
35+
},
36+
},
37+
};
38+
39+
// Mock fetchers that do nothing
40+
const mockFetchers = {
41+
parsed: { fetch: () => {} },
42+
raw: { fetch: () => {} },
43+
skip: { fetch: () => {} },
44+
};
45+
46+
type MockAccountsProviderProps = {
47+
children: React.ReactNode;
48+
};
49+
50+
/**
51+
* Mock provider for Storybook stories that replaces AccountsProvider.
52+
*
53+
* Provides pre-populated account data for commonly used programs
54+
* (System Program, Token Program, etc.) without making network requests.
55+
*
56+
* @example
57+
* ```tsx
58+
* import { MockAccountsProvider } from '../../../../../.storybook/__mocks__/MockAccountsProvider';
59+
*
60+
* const meta = {
61+
* decorators: [
62+
* Story => (
63+
* <ClusterProvider>
64+
* <MockAccountsProvider>
65+
* <Story />
66+
* </MockAccountsProvider>
67+
* </ClusterProvider>
68+
* ),
69+
* ],
70+
* };
71+
* ```
72+
*
73+
* To add more mock accounts, add entries to the `mockState.entries` object.
74+
*/
75+
export function MockAccountsProvider({ children }: MockAccountsProviderProps) {
76+
return (
77+
<StateContext.Provider value={mockState}>
78+
<DispatchContext.Provider value={() => {}}>
79+
<FetchersContext.Provider value={mockFetchers as any}>{children}</FetchersContext.Provider>
80+
</DispatchContext.Provider>
81+
</StateContext.Provider>
82+
);
83+
}

.storybook/decorators.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ClusterProvider } from '@providers/cluster';
2+
import type { Decorator, Parameters } from '@storybook/react';
3+
import React from 'react';
4+
5+
import { MockAccountsProvider } from './__mocks__/MockAccountsProvider';
6+
7+
/** Wraps stories with ClusterProvider. Usage: `decorators: [withCluster]` */
8+
export const withCluster: Decorator = Story => (
9+
<ClusterProvider>
10+
<Story />
11+
</ClusterProvider>
12+
);
13+
14+
/** Wraps stories with ClusterProvider and MockAccountsProvider. Usage: `decorators: [withClusterAndAccounts]` */
15+
export const withClusterAndAccounts: Decorator = Story => (
16+
<ClusterProvider>
17+
<MockAccountsProvider>
18+
<Story />
19+
</MockAccountsProvider>
20+
</ClusterProvider>
21+
);
22+
23+
/** Decorator for card table field components. Usage: `decorators: [withCardTableField]` */
24+
export const withCardTableField: Decorator = Story => (
25+
<ClusterProvider>
26+
<MockAccountsProvider>
27+
<div className="card">
28+
<div className="table-responsive mb-0">
29+
<style>{`.card-table tbody tr:first-child td { border-top: none !important; }`}</style>
30+
<table className="table table-sm table-nowrap card-table">
31+
<tbody>
32+
<Story />
33+
</tbody>
34+
</table>
35+
</div>
36+
</div>
37+
</MockAccountsProvider>
38+
</ClusterProvider>
39+
);
40+
41+
type NextjsNavigationOptions = {
42+
pathname?: string;
43+
query?: Record<string, string>;
44+
};
45+
46+
/** Creates parameters for components using Next.js navigation */
47+
export const createNextjsParameters = (options?: NextjsNavigationOptions): Parameters => ({
48+
nextjs: {
49+
appDirectory: true,
50+
navigation: {
51+
pathname: options?.pathname ?? '/',
52+
query: options?.query ?? {},
53+
},
54+
},
55+
});
56+
57+
export const nextjsParameters: Parameters = createNextjsParameters();

.storybook/preview.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const preview: Preview = {
3232
date: /Date$/i,
3333
},
3434
},
35+
layout: 'padded',
3536
},
3637

3738
decorators: [

app/__tests__/mock-resolvers.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { AddressLookupTableAccount, PublicKey } from '@solana/web3.js';
2+
3+
import E5Alt from './stubs/E59uBXGqn83xN17kMbBVfU1M7T4wHG91eiygHb88Aovb.json';
4+
import EDAlt from './stubs/EDDSpjZHrsFKYTMJDcBqXAjkLcu9EKdvrQR4XnqsXErH.json';
5+
import GDAlt from './stubs/GDLpHg53y5sufRSftvZscFMwdSqP8kHaLwhsT4ZwYSaV.json';
6+
7+
/**
8+
* Mock Address Lookup Table data for tests.
9+
* Keys are ALT account addresses, values are arrays of resolved addresses.
10+
*
11+
* To get ALT data, run:
12+
* ```
13+
* const connection = new Connection(clusterApiUrl('mainnet-beta'));
14+
* const result = await connection.getAddressLookupTable(new PublicKey(altKey));
15+
* console.log(JSON.stringify(result.value.state.addresses.map(a => a.toBase58())));
16+
* ```
17+
*/
18+
const ALT_DATA: Record<string, string[]> = {
19+
// Used by computeBudgetMsg
20+
E59uBXGqn83xN17kMbBVfU1M7T4wHG91eiygHb88Aovb: E5Alt,
21+
22+
// Used by aTokenCreateIdempotentMsg
23+
EDDSpjZHrsFKYTMJDcBqXAjkLcu9EKdvrQR4XnqsXErH: EDAlt,
24+
25+
// Used by computeBudgetMsg
26+
GDLpHg53y5sufRSftvZscFMwdSqP8kHaLwhsT4ZwYSaV: GDAlt,
27+
};
28+
29+
/**
30+
* Creates a mock AddressLookupTableAccount from stored ALT data.
31+
*/
32+
function createMockLookupTable(accountKey: PublicKey): AddressLookupTableAccount | null {
33+
const addresses = ALT_DATA[accountKey.toBase58()];
34+
if (!addresses) {
35+
return null;
36+
}
37+
38+
return new AddressLookupTableAccount({
39+
key: accountKey,
40+
state: {
41+
addresses: addresses.map(addr => new PublicKey(addr)),
42+
authority: undefined,
43+
deactivationSlot: BigInt('18446744073709551615'),
44+
lastExtendedSlot: 0,
45+
lastExtendedSlotStartIndex: 0,
46+
},
47+
});
48+
}
49+
50+
/**
51+
* Resolves address lookup tables from mock data instead of making network requests.
52+
* Drop-in replacement for:
53+
* ```
54+
* const connection = new Connection(clusterApiUrl('mainnet-beta'));
55+
* const lookups = await Promise.all(
56+
* m.addressTableLookups.map(lookup =>
57+
* connection.getAddressLookupTable(lookup.accountKey).then(val => val.value)
58+
* )
59+
* );
60+
* ```
61+
*
62+
* Usage:
63+
* ```
64+
* const lookups = resolveAddressLookupTables(m.addressTableLookups);
65+
* ```
66+
*/
67+
export function resolveAddressLookupTables(
68+
addressTableLookups: Array<{ accountKey: PublicKey }>
69+
): AddressLookupTableAccount[] {
70+
return addressTableLookups
71+
.map(lookup => createMockLookupTable(lookup.accountKey))
72+
.filter((x): x is AddressLookupTableAccount => x !== null);
73+
}

app/__tests__/mock-stubs.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,23 @@ export const systemProgramAuthorizeNonceQueryParam =
7474
// http://localhost:3000/tx/inspector?message=gAEAAwUMMLe3YoGRo3K1%2FT5QfmVUXG8b7svyH9T32%2FUutO6CZQWWFyysN3wFkbv3PDJJ7%2BSesMz2jrhIaRUdLtZzgVKOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGp9UXGSxWjuCKhF9z0peIzwNcMUWyGrNE2AYuqUAAAAan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAAnMnz%2FyRSulVpWHNhBhMhO%2FLrxNFIAL2lufr2C8PAYIABAgMBAwQkBgAAAAWHion%2FERTdIzd%2Fc4fizy7EBdxS9i6CBqhVMDd%2BMKF0AA%3D%3D
7575
export const systemProgramInitializeNonceQueryParam =
7676
'gAEAAwUMMLe3YoGRo3K1%2FT5QfmVUXG8b7svyH9T32%2FUutO6CZQWWFyysN3wFkbv3PDJJ7%2BSesMz2jrhIaRUdLtZzgVKOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGp9UXGSxWjuCKhF9z0peIzwNcMUWyGrNE2AYuqUAAAAan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAAnMnz%2FyRSulVpWHNhBhMhO%2FLrxNFIAL2lufr2C8PAYIABAgMBAwQkBgAAAAWHion%2FERTdIzd%2Fc4fizy7EBdxS9i6CBqhVMDd%2BMKF0AA%3D%3D';
77+
78+
// Message with Token::TransferChecked instruction
79+
// http://localhost:3000/tx/X8DpDCrFN8Ljsh4wcCKLvrsBXAeuxCt8xj4re6dZkESYT95QiVraC9Q7ra6afekg9HvQt4B78K8VNgcQrzZqBb5/inspect
80+
export const tokenTransferCheckedMsg =
81+
'{"header":{"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":6,"numRequiredSignatures":1},"accountKeys":["37vWB5RfLRpnhNobhzmwCRGZbynGd4je2NvppSjUsEdJ","4aa42XQFo45wc2PHQH21vahuyuFYCEZAH4G27xpGYqf6","CFRWXYp8zc2ftkF2Bv8jXmQu1qW67goZjSkKMjv6UV3P","11111111111111111111111111111111","AGRidUXLeDij9CJprkZx7WBXtTQC67jtfiwz293mVrJ","ComputeBudget111111111111111111111111111111","TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","97PALEbpPj7muiQqi2HXS8QukLsrrr1yfgKfvXjWtsUG","ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"],"recentBlockhash":"3nD586uY1XkvvyNR2tKnC8tRweibv8fbo7mYMH5HGqhD","instructions":[{"accounts":[0,2,7,4,3,6],"data":"2","programIdIndex":8,"stackHeight":null},{"accounts":[1,4,2,0],"data":"g7KVp96UU5SJy","programIdIndex":6,"stackHeight":null},{"accounts":[],"data":"EMbWrb","programIdIndex":5,"stackHeight":null}],"indexToProgramIds":{}}';
82+
83+
// Message with Token::Transfer instruction
84+
// http://localhost:3000/tx/E7zy6gPCa3XRy5ykdb2Pt7nb2RHKPWhtnQ5jWgJat4M25jGy3PFAcowtBiby4YyvFRgipu1Js6iX9NpU1WNbFNJ?message=AgEDB4QQ%252BNoctdauTMavfjwKKEEKGqz4vm%252BgzaLMy32q%252BXWVOFuhw3pPUltszxrkhUJ46DV6f49FA9ZmHfbsrSFKs5BVvVDb8%252F6phw5Mikw2uolkYjgi9XPOIGGo15XIc3cbpfF2h%252BStcRX7QYlqbm2YCUb9PvVywyoH7McGfbJCYJofAwZGb%252BUhFzL%252F7K26csOb57yM5bvF9xJrLEObOkAAAAAGgcTOR%252BIjaLixVV7Ih68JLvx%252B%252B7Zso%252FUvv2jUrJy3qAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyMDI3DS%252BRZK6zwhQew2w5vNRZ6a5I8CGVIGkyB6G%252BgcDBAAFAghMAQAEAAkDEAkFAAAAAAAFBQIDAQAGmgEw%252Bk6o0OLa0wAAiwAAAAEBAAJgpkAy5oO19oHcg%252FO%252B4Bt4HHL7igKZ69m%252FAvha2eA6YAVKU1qZKSEGTSTocWDaOHx8NbXdvJK7geQfqEBBBUSNAQEBAEAANTRkNzk1MmY5OGY1OTJmNGI0NzIxMjQ4MTQyZjQ0MjUzNGQzNjU5NTQxNTZiNTBjYjY1YzIwODM4ZmZhYjk0NgAA
85+
export const tokenTransferMsg =
86+
'{"header":{"numReadonlySignedAccounts":1,"numReadonlyUnsignedAccounts":7,"numRequiredSignatures":2},"staticAccountKeys":["6nvUkyJG5j6rH3Hfm15mJNgPMJ1cxHQapfm5K3k647iv","Aoxj61PC8aLZvGw6Ad9QkW2yZGZh1prvUZn8PUghJZfx","A2UBBBAbLJw6Js5fvqRivd3nNWKD4H612814mUoTp8rC","Eq6hAvijAYQP4Yw3U1ESDiF7gZdYPfCo2AvD3t31LypQ","11111111111111111111111111111111","ComputeBudget111111111111111111111111111111","SysvarRent111111111111111111111111111111111","TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","vLbNrN3AGMTGkEmUsLZRUGXEAkFbSkjLTYb2w1fWPn3","ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL","EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"],"recentBlockhash":"4j667Zcmv9DGioGfNt4Nves34JPP43jGEy4zGcb7Lnsh","compiledInstructions":[{"programIdIndex":5,"accountKeyIndexes":[],"data":{"type":"Buffer","data":[3,173,143,0,0,0,0,0,0]}},{"programIdIndex":5,"accountKeyIndexes":[],"data":{"type":"Buffer","data":[2,3,138,0,0]}},{"programIdIndex":9,"accountKeyIndexes":[0,2,8,10,4,7,6],"data":{"type":"Buffer","data":[1]}},{"programIdIndex":7,"accountKeyIndexes":[3,2,1],"data":{"type":"Buffer","data":[3,192,16,237,127,38,0,0,0]}}],"addressTableLookups":[]}';
87+
88+
// Message with SystemProgram::TransferWithSeed instruction
89+
// http://localhost:3000/tx/inspector?message=AgEABCD2bYdQKJdSgj1QM%252BpZ72SIZpv2GYDFEJikyXElblc%252FAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPioCg1xVXrm6Z5RzYN7QLfJfK96HJQRMCHIKy%252BNUm1SiACWft94a8hVfpx%252FU1M7sE417sq9MZxiewNt1gqknOrYwO4BG3al53uPCK0GFPCoa1fp40X5y7csr6pS8m3QkBAQMCAQNHCwAAAEBCDwAAAAAAEwAAAAAAAABleGFtcGxlLXNlZWQtcGhyYXNlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%253D
90+
export const systemTransferWithSeedMsg =
91+
'{"header":{"numRequiredSignatures":2,"numReadonlySignedAccounts":1,"numReadonlyUnsignedAccounts":0},"accountKeys":["3DfzKrnfv8vxxGPPQi1LX6PE8a69V7g2AbvguSgAguan","11111111111111111111111111111111","23fcFSBcrszjjqd66qeqptM17oWXERStDUafP4zLypT6","3j6YZsXi9FQrWvFp5TRN6AdQ81GY3GtNWq6imr3EawyF"],"recentBlockhash":"CgTQBy9C4Y6DQuGwDbXcvTYUxjskpsN9JG6wAUGGDCY8","instructions":[{"programIdIndex":1,"accounts":[2,1,3],"data":"38kGFr8ikW7X4bCtZ8kfCoefSKoxGTzFwhvKg9oyby8wMWV3m8YRM1PxQALkEbYN3wdqtN1rN2xYwngeHs19WJ5C25Xe4NDhh"}],"indexToProgramIds":{}}';
92+
93+
// Message with SystemProgram::Transfer instruction
94+
// http://localhost:3000/tx/inspector?message=AgABBPFmGw2wO8jwEIBQypHH18EXc7vbGQyk6w8MRAhJrzWihW4JIxMJs9rl0X5O%252BxMD%252BHnmTgeu%252F5XAZ5SnUDEEsCMciV6RD3TnpfA5gBxnchikUAFUgzX7YkbfQF8%252Bg4ZHZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL0mnxZdAiBKKUa1MajdzDbTzPr9og9OGh2Pqn9zijcABAwIBAgwCAAAAQEIPAAAAAAA%253D
95+
export const systemTransferMsg =
96+
'{"header":{"numRequiredSignatures":2,"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":1},"accountKeys":["HFKZMufXXi8eQ7gguP7yyhhJDr5ztdP6v5ppN2DQEeth","9yrYKJxZKktutPzhUNgS92bzVjpHkgZPNpZCHRr6M2TC","2vPuXtAJLtxmkJRhuEwCuvUKyRemreh7q1DR4ns7wwzL","11111111111111111111111111111111"],"recentBlockhash":"4BbJaBaqatXh5gbRry2yGerZoDm8MP3Tdaw9yVbHSGa3","instructions":[{"programIdIndex":3,"accounts":[1,2],"data":"3Bxs4Bc3VYuGVB19"}],"indexToProgramIds":{}}';

0 commit comments

Comments
 (0)