Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 14 additions & 8 deletions app/components/inspector/InspectorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useFetchAccountInfo } from '@providers/accounts';
import { FetchStatus } from '@providers/cache';
import { useFetchRawTransaction, useRawTransactionDetails } from '@providers/transactions/raw';
import usePrevious from '@react-hook/previous';
import { Connection, Message, PACKET_DATA_SIZE, PublicKey, VersionedMessage } from '@solana/web3.js';
import { Connection, MessageV0, PACKET_DATA_SIZE, PublicKey, VersionedMessage } from '@solana/web3.js';
import { generated, PROGRAM_ADDRESS as SQUADS_V4_PROGRAM_ADDRESS } from '@sqds/multisig';
const { VaultTransaction } = generated;

Expand Down Expand Up @@ -208,22 +208,28 @@ function SquadsProposalInspectorCard({ account, onClear }: { account: string; on
// Convert VaultTransactionMessage to a format compatible with Message
const convertVaultTransactionToMessage = (vaultTx: typeof VaultTransaction.prototype): VersionedMessage => {
const { message } = vaultTx;
const accountKeys = message.accountKeys;

// Create a standard Message object with the necessary fields
const solanaMessage = new Message({
accountKeys: message.accountKeys,
const solanaMessage = new MessageV0({
addressTableLookups: message.addressTableLookups.map(x => ({
...x,
readonlyIndexes: Array.from(x.readonlyIndexes),
writableIndexes: Array.from(x.writableIndexes),
})),
compiledInstructions: message.instructions.map(instruction => ({
accountKeyIndexes: Array.from(instruction.accountIndexes),
data: Buffer.from(instruction.data),
programIdIndex: instruction.programIdIndex,
})),
header: {
numReadonlySignedAccounts: message.numSigners - message.numWritableSigners,
numReadonlyUnsignedAccounts:
message.accountKeys.length - message.numSigners - message.numWritableNonSigners,
numRequiredSignatures: message.numSigners,
},
instructions: message.instructions.map(instruction => ({
accounts: Array.from(instruction.accountIndexes),
data: bs58.encode(Buffer.from(instruction.data)),
programIdIndex: instruction.programIdIndex,
})),
recentBlockhash: bs58.encode(Uint8Array.from(new Array(32).fill(0))),
staticAccountKeys: accountKeys,
});

return solanaMessage;
Expand Down
9 changes: 5 additions & 4 deletions app/components/inspector/InstructionsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ function InspectorInstructionCard({
index: number;
}) {
const { cluster, url } = useCluster();
const programId = message.staticAccountKeys[ix.programIdIndex];
const programName = getProgramName(programId.toBase58(), cluster);
const anchorProgram = useAnchorProgram(programId.toString(), url);

const transactionInstruction = intoTransactionInstructionFromVersionedMessage(ix, message);

const programId = transactionInstruction.programId;
const programName = getProgramName(programId.toBase58(), cluster);
const anchorProgram = useAnchorProgram(programId.toString(), url);

if (anchorProgram.program) {
return (
<ErrorBoundary
Expand All @@ -50,7 +51,7 @@ function InspectorInstructionCard({
index={index}
ix={ix}
message={message}
programName="Anchor Program"
programName="Unknown Program"
/>
}
>
Expand Down
56 changes: 56 additions & 0 deletions app/components/inspector/__tests__/InspectorPage.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ import { TransactionInspectorPage } from '../InspectorPage';
// Create mocks for the required dependencies
const mockUseSearchParams = () => {
const params = new URLSearchParams();
// Normal Squads transaction
params.set('squadsTx', 'ASwDJP5mzxV1dfov2eQz5WAVEy833nwK17VLcjsrZsZf');
// Squads transaction with lookup table
return params;
};

// From Squads transaction ASwDJP5mzxV1dfov2eQz5WAVEy833nwK17VLcjsrZsZf'
const MOCK_SQUADS_ACCOUNT_INFO: AccountInfo<Buffer> = {
data: Buffer.from(
'qPqiZFEOos+fErS/xkrbJRCvXG3UbwrUsJlVxCt0e4xgzjQyewfzMULyaPFkYPsCiNMe9FN//udpL5PwKAM/1qdskrvY+9nLCAAAAAAAAAD/AP8AAAAAAQEECAAAANCjHLRKvgiq2AoZK5QSGOfYj5bTGybeyAspA1+XDrVyM90v0fImaE0NQYcSinPuk++6GJEe5cKJZ4w9p0mAYgkJKhPulcQcugimf1rGfo334doRYl4dZBN/j08jgwN/FDCuVi3sTsjyvqU+oP8oI/e92Q78flUtkwuKGo3ug/s7V4efG9ifzqH+b9ldMvB714n0oZVW1d6xudyfhcoWP+0CqPaRToihsOIQFT73Y64rAMK5PRbBJNLAU3oQBIAAAAan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAAABAAAABQcAAAABAgMEBgcABAAAAAMAAAAAAAAA',
Expand All @@ -28,6 +31,17 @@ const MOCK_SQUADS_ACCOUNT_INFO: AccountInfo<Buffer> = {
owner: PROGRAM_ID,
};

// From Squads transaction D6zTKhuJdvU4aPcgnJrXhaL3AP54AGQKVaiQkikH7fwH
const MOCK_SQUADS_LOOKUP_TABLE_ACCOUNT_INFO: AccountInfo<Buffer> = {
data: Buffer.from(
'qPqiZFEOos8bpNmzOFnIgq7HtFDkjs0zoH+RjHiREtlTMLrrxCnOoOcFvY3L4K/GkofeZWEMwteLWwiE+IC8lnd8Ck5flvyb3QQAAAAAAAD+AP8AAAAAAQEFBgAAAEq4mP2n8jYC4uvQ/2riMoE0PhxgqIF66HAqkgBn4/7YWvNmtUiOi7IxoG9Yg+DNwzaHxoGjbIgVzFpOmwEZBmf9dPjWz8/N7PpjzVI1TulkO4Egf8ZYe7WLo0OjhhrzoYQzUnBMSyxrGPE/4v6Xp81WeB65mgEPCx6Nm2doqmmMmJEqbWg9L9Do0t/Tr7QiU2rSPiAV6W0bNxo4qIu+aRNLpsNxnQkq2EAyNB4e5Vx8/7kaTXVN+Y+DEOMrcIenQgEAAAAGCgAAAAABAgMEBQcICQowAAAA9r57/qtrEp4AZc0dAAAAAL9A3h92lHzal8AhXk0xQ6drSpPcsjemGX1gSwpnAfeuAQAAAC2j9Rh4Ufp3UyACH6zJgVGpNk7XhltxlBh5LvHTkFE+AAAAAAUAAAA4LwgHBQ==',
'base64'
),
executable: false,
lamports: 1000000,
owner: PROGRAM_ID,
};

// Mock SWR
vi.mock('swr', () => ({
__esModule: true,
Expand Down Expand Up @@ -134,4 +148,46 @@ describe('TransactionInspectorPage with Squads Transaction', () => {
// Initially it should show loading
expect(screen.getByText(/Error loading vault transaction/i)).not.toBeNull();
});

test('renders Squads transaction with lookup table without crashing', async () => {
// Setup SWR mock for successful response
const mockSWR = await import('swr');
(mockSWR.default as any).mockImplementation((key: any) => {
if (Array.isArray(key) && key[0] === specificAccountKey[0] && key[1] === specificAccountKey[1]) {
return {
data: VaultTransaction.fromAccountInfo(MOCK_SQUADS_LOOKUP_TABLE_ACCOUNT_INFO)[0],
error: null,
isLoading: false,
};
}
return { data: null, error: null, isLoading: true };
});

render(
<ScrollAnchorProvider>
<ClusterProvider>
<AccountsProvider>
<TransactionInspectorPage showTokenBalanceChanges={false} />
</AccountsProvider>
</ClusterProvider>
</ScrollAnchorProvider>
);

await vi.waitFor(
() => {
expect(screen.queryByText(/Inspector Input/i)).toBeNull();
},
{ interval: 50, timeout: 10000 }
);

// Check that the td with text Fee Payer has the text F3S4PD17Eo3FyCMropzDLCpBFuQuBmufUVBBdKEHbQFT
expect(screen.getByRole('row', { name: /Fee Payer/i })).toHaveTextContent(
'62gRsAdA6dcbf4Frjp7YRFLpFgdGu8emAACcnnREX3L3'
);

expect(screen.getByText(/Account List \(11\)/i)).not.toBeNull();
expect(
screen.getByText(/Unknown Program \(45AMNJMGuojexK1rEBHJSSVFDpTUcoHRcAUmRfLF8hrm\) Instruction/i)
).not.toBeNull();
});
});
25 changes: 17 additions & 8 deletions app/components/inspector/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {

type LookupsForAccountKeyIndex = { lookupTableIndex: number, lookupTableKey: PublicKey }

function findLookupAddressByIndex(accountIndex: number, message: VersionedMessage, lookupsForAccountKeyIndex: LookupsForAccountKeyIndex[]){
function findLookupAddressByIndex(accountIndex: number, message: VersionedMessage, lookupsForAccountKeyIndex: LookupsForAccountKeyIndex[]) {
let lookup: PublicKey;
// dynamic means that lookups are taken based not on staticAccountKeys
let dynamicLookups: { isStatic: true, lookups: undefined} | { isStatic: false, lookups: LookupsForAccountKeyIndex };
let dynamicLookups: { isStatic: true, lookups: undefined } | { isStatic: false, lookups: LookupsForAccountKeyIndex };

if (accountIndex >= message.staticAccountKeys.length) {
const lookupIndex = accountIndex - message.staticAccountKeys.length;
Expand All @@ -36,7 +36,7 @@ function fillAccountMetas(
accountKeyIndexes: number[],
message: VersionedMessage,
lookupsForAccountKeyIndex: LookupsForAccountKeyIndex[],
){
) {
const accountMetas = accountKeyIndexes.map((accountIndex) => {
const { lookup } = findLookupAddressByIndex(accountIndex, message, lookupsForAccountKeyIndex);

Expand All @@ -54,12 +54,12 @@ function fillAccountMetas(
return accountMetas;
}

export function findLookupAddress(accountIndex: number, message: VersionedMessage, lookupsForAccountKeyIndex: LookupsForAccountKeyIndex[]){
export function findLookupAddress(accountIndex: number, message: VersionedMessage, lookupsForAccountKeyIndex: LookupsForAccountKeyIndex[]) {
return findLookupAddressByIndex(accountIndex, message, lookupsForAccountKeyIndex);
}

export function fillAddressTableLookupsAccounts(addressTableLookups: MessageAddressTableLookup[]){
const lookupsForAccountKeyIndex: LookupsForAccountKeyIndex[]= [
export function fillAddressTableLookupsAccounts(addressTableLookups: MessageAddressTableLookup[]) {
const lookupsForAccountKeyIndex: LookupsForAccountKeyIndex[] = [
...addressTableLookups.flatMap(lookup =>
lookup.writableIndexes.map(index => ({
lookupTableIndex: index,
Expand All @@ -84,11 +84,20 @@ export function intoTransactionInstructionFromVersionedMessage(
const { accountKeyIndexes, data } = compiledInstruction;
const { addressTableLookups } = originalMessage;

const programId = originalMessage.staticAccountKeys.at(compiledInstruction.programIdIndex);
const lookupAccounts = fillAddressTableLookupsAccounts(addressTableLookups);

// When we're deserializing Squads vault transactions, an "outer" programIdIndex can be found in the addressTableLookups
// (You never need to lookup outer programIds for normal messages)
let programId: PublicKey | undefined;
if (compiledInstruction.programIdIndex < originalMessage.staticAccountKeys.length) {
programId = originalMessage.staticAccountKeys.at(compiledInstruction.programIdIndex);
} else {
// This is only needed for Squads vault transactions, in normal messages, outer program IDs cannot be in addressTableLookups
const lookupIndex = compiledInstruction.programIdIndex - originalMessage.staticAccountKeys.length;
programId = addressTableLookups[lookupIndex].accountKey;
Comment thread
ngundotra marked this conversation as resolved.
}
if (!programId) throw new Error("Program ID not found");

const lookupAccounts = fillAddressTableLookupsAccounts(addressTableLookups);
const accountMetas = fillAccountMetas(accountKeyIndexes, originalMessage, lookupAccounts);

const transactionInstruction: TransactionInstruction = new TransactionInstruction({
Expand Down