Skip to content

Commit bdf6483

Browse files
committed
feat: add AssociatedTokenDetailsCard to inspector
1 parent c8b1065 commit bdf6483

File tree

13 files changed

+445
-101
lines changed

13 files changed

+445
-101
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { BaseInstructionCard } from '@components/common/BaseInstructionCard';
2+
import { useCluster } from '@providers/cluster';
3+
import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js';
4+
import { getProgramName } from '@utils/tx';
5+
import React from 'react';
6+
7+
export function BaseUnknownDetailsCard({
8+
ix,
9+
index,
10+
result,
11+
innerCards,
12+
childIndex,
13+
InstructionCardComponent = BaseInstructionCard,
14+
}: {
15+
ix: TransactionInstruction | ParsedInstruction;
16+
index: number;
17+
result: SignatureResult;
18+
innerCards?: JSX.Element[];
19+
childIndex?: number;
20+
InstructionCardComponent?: React.FC<Parameters<typeof BaseInstructionCard>[0]>;
21+
}) {
22+
const { cluster } = useCluster();
23+
const programName = getProgramName(ix.programId.toBase58(), cluster);
24+
return (
25+
<InstructionCardComponent
26+
ix={ix}
27+
index={index}
28+
result={result}
29+
title={`${programName}: Unknown Instruction`}
30+
innerCards={innerCards}
31+
childIndex={childIndex}
32+
defaultRaw
33+
/>
34+
);
35+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Address } from '@components/common/Address';
2+
import { BaseInstructionCard } from '@components/common/BaseInstructionCard';
3+
import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js';
4+
import React from 'react';
5+
6+
import { BaseRawDetails } from '../../BaseRawDetails';
7+
8+
export function BaseCreateDetailsCard({
9+
ix,
10+
index,
11+
raw,
12+
result,
13+
innerCards,
14+
childIndex,
15+
children,
16+
InstructionCardComponent = BaseInstructionCard,
17+
}: {
18+
ix: ParsedInstruction;
19+
index: number;
20+
raw: TransactionInstruction;
21+
result: SignatureResult;
22+
innerCards?: JSX.Element[];
23+
childIndex?: number;
24+
children?: React.ReactNode;
25+
InstructionCardComponent?: React.FC<Parameters<typeof BaseInstructionCard>[0]>;
26+
}) {
27+
return (
28+
<InstructionCardComponent
29+
ix={ix}
30+
index={index}
31+
raw={raw}
32+
result={result}
33+
title="Associated Token Program: Create"
34+
innerCards={innerCards}
35+
childIndex={childIndex}
36+
>
37+
<tr>
38+
<td>Program</td>
39+
<td className="text-lg-end">
40+
<Address pubkey={ix.programId} alignRight link />
41+
</td>
42+
</tr>
43+
{children ? children : <BaseRawDetails ix={raw} />}
44+
</InstructionCardComponent>
45+
);
46+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { BaseInstructionCard } from '@components/common/BaseInstructionCard';
2+
import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js';
3+
import React from 'react';
4+
5+
import { BaseRawDetails } from '../../BaseRawDetails';
6+
import { CreateIdempotentInfo } from './types';
7+
8+
export function BaseCreateIdempotentDetailsCard(props: {
9+
ix: ParsedInstruction;
10+
index: number;
11+
raw: TransactionInstruction;
12+
result: SignatureResult;
13+
info?: CreateIdempotentInfo;
14+
innerCards?: JSX.Element[];
15+
childIndex?: number;
16+
children?: React.ReactNode;
17+
InstructionCardComponent?: React.FC<Parameters<typeof BaseInstructionCard>[0]>;
18+
}) {
19+
const {
20+
ix,
21+
index,
22+
raw,
23+
result,
24+
children,
25+
innerCards,
26+
childIndex,
27+
InstructionCardComponent = BaseInstructionCard,
28+
} = props;
29+
30+
return (
31+
<InstructionCardComponent
32+
ix={ix}
33+
index={index}
34+
raw={raw}
35+
result={result}
36+
title="Associated Token Program: Create Idempotent"
37+
innerCards={innerCards}
38+
childIndex={childIndex}
39+
>
40+
{children ? children : <BaseRawDetails ix={raw} />}
41+
</InstructionCardComponent>
42+
);
43+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { BaseInstructionCard } from '@components/common/BaseInstructionCard';
2+
import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js';
3+
import React from 'react';
4+
5+
import { BaseRawDetails } from '../../BaseRawDetails';
6+
import { RecoverNestedInfo } from './types';
7+
8+
export function BaseRecoverNestedDetailsCard(props: {
9+
ix: ParsedInstruction;
10+
index: number;
11+
raw: TransactionInstruction;
12+
result: SignatureResult;
13+
info?: RecoverNestedInfo;
14+
innerCards?: JSX.Element[];
15+
childIndex?: number;
16+
children?: React.ReactNode;
17+
InstructionCardComponent?: React.FC<Parameters<typeof BaseInstructionCard>[0]>;
18+
}) {
19+
const {
20+
ix,
21+
index,
22+
raw,
23+
result,
24+
children,
25+
innerCards,
26+
childIndex,
27+
InstructionCardComponent = BaseInstructionCard,
28+
} = props;
29+
30+
return (
31+
<InstructionCardComponent
32+
ix={ix}
33+
index={index}
34+
raw={raw}
35+
result={result}
36+
title="Associated Token Program: Recover Nested"
37+
innerCards={innerCards}
38+
childIndex={childIndex}
39+
>
40+
{children ? children : <BaseRawDetails ix={raw} />}
41+
</InstructionCardComponent>
42+
);
43+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* eslint-disable @typescript-eslint/no-redeclare */
2+
3+
import { PublicKeyFromString } from '@validators/pubkey';
4+
import { enums, Infer, type } from 'superstruct';
5+
6+
export type CreateIdempotentInfo = Infer<typeof CreateIdempotentInfo>;
7+
export const CreateIdempotentInfo = type({
8+
account: PublicKeyFromString,
9+
mint: PublicKeyFromString,
10+
source: PublicKeyFromString,
11+
systemProgram: PublicKeyFromString,
12+
tokenProgram: PublicKeyFromString,
13+
wallet: PublicKeyFromString,
14+
});
15+
16+
export type RecoverNestedInfo = Infer<typeof RecoverNestedInfo>;
17+
export const RecoverNestedInfo = type({
18+
destination: PublicKeyFromString,
19+
nestedMint: PublicKeyFromString,
20+
nestedOwner: PublicKeyFromString,
21+
nestedSource: PublicKeyFromString,
22+
ownerMint: PublicKeyFromString,
23+
tokenProgram: PublicKeyFromString,
24+
wallet: PublicKeyFromString,
25+
});
26+
27+
export type SystemInstructionType = Infer<typeof SystemInstructionType>;
28+
export const SystemInstructionType = enums(['create', 'createIdempotent', 'recoverNested']);

app/components/inspector/InstructionsSection.tsx

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BaseInstructionCard } from '@components/common/BaseInstructionCard';
12
import { useCluster } from '@providers/cluster';
23
import { ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token';
34
import { ComputeBudgetProgram, MessageCompiledInstruction, VersionedMessage } from '@solana/web3.js';
@@ -6,17 +7,13 @@ import React from 'react';
67

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

9-
import { BaseInstructionCard } from '../common/BaseInstructionCard';
1010
import AnchorDetailsCard from '../instruction/AnchorDetailsCard';
11-
import { AssociatedTokenDetailsCard } from '../instruction/associated-token/AssociatedTokenDetailsCard';
1211
import { ComputeBudgetDetailsCard } from '../instruction/ComputeBudgetDetailsCard';
12+
import { AssociatedTokenDetailsCard } from './associated-token/AssociatedTokenDetailsCard';
1313
import { UnknownDetailsCard } from './UnknownDetailsCard';
14-
import { intoTransactionInstructionFromVersionedMessage } from './utils';
14+
import { intoTransactionInstructionFromVersionedMessage, ParsedInstructionFactory } from './utils';
1515

1616
export function InstructionsSection({ message }: { message: VersionedMessage }) {
17-
// @ts-expect-error
18-
globalThis.window.__txm = message;
19-
console.log(6661, message.serialize());
2017
return (
2118
<>
2219
{message.compiledInstructions.map((ix, index) => {
@@ -40,9 +37,7 @@ function InspectorInstructionCard({
4037
const programName = getProgramName(programId.toBase58(), cluster);
4138
const anchorProgram = useAnchorProgram(programId.toString(), url);
4239

43-
const transactionInstruction = intoTransactionInstructionFromVersionedMessage(ix, message, programId);
44-
45-
console.log('6660', ix, { transactionInstruction }, transactionInstruction.programId.toBase58());
40+
const transactionInstruction = intoTransactionInstructionFromVersionedMessage(ix, message);
4641

4742
if (anchorProgram.program) {
4843
return AnchorDetailsCard({
@@ -69,20 +64,19 @@ function InspectorInstructionCard({
6964
// - result is `err: null` as at this point there should not be errors
7065
const result = { err: null };
7166
const signature = '';
67+
const factory = ParsedInstructionFactory();
7268
switch (transactionInstruction?.programId.toString()) {
7369
case ASSOCIATED_TOKEN_PROGRAM_ID.toString(): {
74-
console.log(6667, 'AToken', transactionInstruction);
75-
//return (
76-
//<AssociatedTokenDetailsCard
77-
//key={index}
78-
//ix={transactionInstruction}
79-
//index={index}
80-
//result={result}
81-
//signature={signature}
82-
////InstructionCardComponent={BaseInstructionCard}
83-
///>
84-
//);
85-
break;
70+
return (
71+
<AssociatedTokenDetailsCard
72+
key={index}
73+
ix={factory.intoParsedInstruction(transactionInstruction)}
74+
raw={transactionInstruction}
75+
index={index}
76+
result={result}
77+
InstructionCardComponent={BaseInstructionCard}
78+
/>
79+
);
8680
}
8781
case ComputeBudgetProgram.programId.toString(): {
8882
return (
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { BaseInstructionCard } from '@components/common/BaseInstructionCard';
2+
import { intoTransactionInstructionFromVersionedMessage } from '@components/inspector/utils';
3+
import * as spl from '@solana/spl-token';
4+
import { render, screen } from '@testing-library/react';
5+
6+
import * as stubs from '@/app/__tests__/mock-stubs';
7+
import * as mock from '@/app/__tests__/mocks';
8+
import { ClusterProvider } from '@/app/providers/cluster';
9+
import { ScrollAnchorProvider } from '@/app/providers/scroll-anchor';
10+
11+
import { ParsedInstructionFactory } from '../../inspector/utils';
12+
import { AssociatedTokenDetailsCard } from '../associated-token/AssociatedTokenDetailsCard';
13+
14+
jest.mock('next/navigation');
15+
16+
describe('inspector::AssociatedTokenDetailsCard', () => {
17+
beforeEach(() => {
18+
mock.mockUseSearchParams();
19+
});
20+
21+
afterEach(() => {
22+
jest.clearAllMocks();
23+
});
24+
25+
const factory = ParsedInstructionFactory();
26+
27+
test('should render "CreateIdempotentDetailsCard"', async () => {
28+
const index = 1;
29+
const m = mock.deserializeMessageV0(stubs.aTokenMsg);
30+
const ti = intoTransactionInstructionFromVersionedMessage(m.compiledInstructions[index], m);
31+
expect(ti.programId.equals(spl.ASSOCIATED_TOKEN_PROGRAM_ID)).toBeTruthy();
32+
33+
const ix = factory.intoParsedInstruction(ti);
34+
35+
// check that component is rendered properly
36+
render(
37+
<ScrollAnchorProvider>
38+
<ClusterProvider>
39+
<AssociatedTokenDetailsCard
40+
ix={ix}
41+
raw={ti}
42+
index={index}
43+
result={{ err: null }}
44+
InstructionCardComponent={BaseInstructionCard}
45+
/>
46+
</ClusterProvider>
47+
</ScrollAnchorProvider>
48+
);
49+
expect(screen.getByText(/Associated Token Program: Create Idempotent/)).toBeInTheDocument();
50+
});
51+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Component is the resemblace of instruction/associated-token/AssociatedTokenDetailsCard
3+
*
4+
* The main difference is that we omit parsed data. Transaction created from VersionedMessage that is used at inspector does not have parsed data which is present at transaction fetched from blockchain by its signature.
5+
*/
6+
import { BaseInstructionCard as InstructionCard } from '@components/common/BaseInstructionCard';
7+
import { BaseCreateDetailsCard as CreateDetailsCard } from '@components/common/instruction/associated-token/BaseCreateDetailsCard';
8+
import { BaseCreateIdempotentDetailsCard as CreateIdempotentDetailsCard } from '@components/common/instruction/associated-token/BaseCreateIdempotentDetailsCard';
9+
import { BaseRecoverNestedDetailsCard as RecoverNestedDetailsCard } from '@components/common/instruction/associated-token/BaseRecoverNestedDetailsCard';
10+
import { BaseUnknownDetailsCard as UnknownDetailsCard } from '@components/common/instruction/BaseUnknownDetailsCard';
11+
import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js';
12+
import { ParsedInfo } from '@validators/index';
13+
import React from 'react';
14+
import { create } from 'superstruct';
15+
16+
type DetailsProps = {
17+
raw: TransactionInstruction;
18+
ix: ParsedInstruction;
19+
result: SignatureResult;
20+
index: number;
21+
innerCards?: JSX.Element[];
22+
childIndex?: number;
23+
InstructionCardComponent?: React.FC<Parameters<typeof InstructionCard>[0]>;
24+
};
25+
26+
export function AssociatedTokenDetailsCard(props: DetailsProps) {
27+
try {
28+
const parsed = create(props.ix.parsed, ParsedInfo);
29+
switch (parsed.type) {
30+
case 'create': {
31+
return <CreateDetailsCard {...props} />;
32+
}
33+
case 'createIdempotent': {
34+
return <CreateIdempotentDetailsCard info={parsed.info} {...props} />;
35+
}
36+
case 'recoverNested': {
37+
return <RecoverNestedDetailsCard info={parsed.info} {...props} />;
38+
}
39+
default:
40+
return <UnknownDetailsCard {...props} />;
41+
}
42+
} catch (_error) {
43+
return <UnknownDetailsCard {...props} />;
44+
}
45+
}

0 commit comments

Comments
 (0)