diff --git a/packages/library/src/__tests__/decode-transaction-test.ts b/packages/library/src/__tests__/decode-transaction-test.ts index 4f154b81eda3..570dec1be60a 100644 --- a/packages/library/src/__tests__/decode-transaction-test.ts +++ b/packages/library/src/__tests__/decode-transaction-test.ts @@ -2,7 +2,12 @@ import { FetchAccountsConfig, fetchJsonParsedAccounts } from '@solana/accounts'; import type { Address } from '@solana/addresses'; import type { GetMultipleAccountsApi, Rpc } from '@solana/rpc'; import type { Blockhash, LamportsUnsafeBeyond2Pow53Minus1 } from '@solana/rpc-types'; -import { decompileTransaction, getCompiledTransactionDecoder } from '@solana/transactions'; +import { + CompilableTransaction, + getCompiledTransactionDecoder, + getTransactionDecoder, + ITransactionWithSignatures, +} from '@solana/transactions'; import type { CompiledTransaction } from '@solana/transactions/dist/types/compile-transaction'; jest.mock('@solana/accounts'); @@ -46,14 +51,25 @@ describe('decodeTransaction', () => { signatures: [], }; + const mockTransaction: CompilableTransaction = { version: 0 } as unknown as CompilableTransaction; + // mock `getCompiledTransactionDecoder` to return `compiledTransaction` let mockCompiledTransactionDecode: () => CompiledTransaction; + let mockTransactionDecode: () => CompilableTransaction | (CompilableTransaction & ITransactionWithSignatures); + beforeEach(() => { mockCompiledTransactionDecode = jest.fn().mockReturnValue(compiledTransaction); + mockTransactionDecode = jest.fn().mockReturnValue(mockTransaction); jest.mocked(getCompiledTransactionDecoder).mockReturnValue({ decode: mockCompiledTransactionDecode, } as unknown as ReturnType); + + // mock `getTransactionDecoder` + + jest.mocked(getTransactionDecoder).mockReturnValue({ + decode: mockTransactionDecode, + } as unknown as ReturnType); }); it('should pass the given encoded transaction to the compiled transaction decoder', async () => { @@ -68,19 +84,31 @@ describe('decodeTransaction', () => { expect(fetchJsonParsedAccounts).not.toHaveBeenCalled(); }); - it('should call `decompileTransaction` with the compiled transaction and no lookup tables', async () => { + it('should call the transaction decoder with no lookup tables', async () => { expect.assertions(1); await decodeTransaction(encodedTransaction, rpc); - expect(decompileTransaction).toHaveBeenCalledWith(compiledTransaction, { + expect(getTransactionDecoder).toHaveBeenCalledWith({ addressesByLookupTableAddress: {}, lastValidBlockHeight: undefined, }); }); + it('should return the result of the `getTransactionDecoder` decode function', async () => { + expect.assertions(1); + const decodePromise = decodeTransaction(encodedTransaction, rpc); + await expect(decodePromise).resolves.toStrictEqual(mockTransaction); + }); + + it('should call the `getTransactionDecoder` decode function', async () => { + expect.assertions(1); + await decodeTransaction(encodedTransaction, rpc); + expect(mockTransactionDecode).toHaveBeenLastCalledWith(encodedTransaction); + }); + it('should pass `lastValidBlockHeight` to `decompileTransaction`', async () => { expect.assertions(1); await decodeTransaction(encodedTransaction, rpc, { lastValidBlockHeight: 100n }); - expect(decompileTransaction).toHaveBeenCalledWith(compiledTransaction, { + expect(getTransactionDecoder).toHaveBeenCalledWith({ addressesByLookupTableAddress: {}, lastValidBlockHeight: 100n, }); @@ -104,14 +132,25 @@ describe('decodeTransaction', () => { signatures: [], }; + const mockTransaction: CompilableTransaction = { version: 0 } as unknown as CompilableTransaction; + // mock `getCompiledTransactionDecoder` to return `compiledTransaction` let mockCompiledTransactionDecode: () => CompiledTransaction; + let mockTransactionDecode: () => CompilableTransaction | (CompilableTransaction & ITransactionWithSignatures); + beforeEach(() => { mockCompiledTransactionDecode = jest.fn().mockReturnValue(compiledTransaction); + mockTransactionDecode = jest.fn().mockReturnValue(mockTransaction); jest.mocked(getCompiledTransactionDecoder).mockReturnValue({ decode: mockCompiledTransactionDecode, } as unknown as ReturnType); + + // mock `getTransactionDecoder` + + jest.mocked(getTransactionDecoder).mockReturnValue({ + decode: mockTransactionDecode, + } as unknown as ReturnType); }); it('should pass the given encoded transaction to the compiled transaction decoder', async () => { @@ -126,14 +165,26 @@ describe('decodeTransaction', () => { expect(fetchJsonParsedAccounts).not.toHaveBeenCalled(); }); - it('should call `decompileTransaction` with the compiled transaction and no lookup tables', async () => { + it('should call the transaction decoder with no lookup tables', async () => { expect.assertions(1); await decodeTransaction(encodedTransaction, rpc); - expect(decompileTransaction).toHaveBeenCalledWith(compiledTransaction, { + expect(getTransactionDecoder).toHaveBeenCalledWith({ addressesByLookupTableAddress: {}, lastValidBlockHeight: undefined, }); }); + + it('should return the result of the `getTransactionDecoder` decode function', async () => { + expect.assertions(1); + const decodePromise = decodeTransaction(encodedTransaction, rpc); + await expect(decodePromise).resolves.toStrictEqual(mockTransaction); + }); + + it('should call the `getTransactionDecoder` decode function', async () => { + expect.assertions(1); + await decodeTransaction(encodedTransaction, rpc); + expect(mockTransactionDecode).toHaveBeenLastCalledWith(encodedTransaction); + }); }); describe('for a versioned transaction with empty `addressTableLookups`', () => { @@ -153,14 +204,24 @@ describe('decodeTransaction', () => { signatures: [], }; + const mockTransaction: CompilableTransaction = { version: 0 } as unknown as CompilableTransaction; + // mock `getCompiledTransactionDecoder` to return `compiledTransaction` let mockCompiledTransactionDecode: () => CompiledTransaction; + let mockTransactionDecode: () => CompilableTransaction | (CompilableTransaction & ITransactionWithSignatures); + beforeEach(() => { mockCompiledTransactionDecode = jest.fn().mockReturnValue(compiledTransaction); + mockTransactionDecode = jest.fn().mockReturnValue(mockTransaction); jest.mocked(getCompiledTransactionDecoder).mockReturnValue({ decode: mockCompiledTransactionDecode, } as unknown as ReturnType); + + // mock `getTransactionDecoder` + jest.mocked(getTransactionDecoder).mockReturnValue({ + decode: mockTransactionDecode, + } as unknown as ReturnType); }); it('should pass the given encoded transaction to the compiled transaction decoder', async () => { @@ -175,14 +236,26 @@ describe('decodeTransaction', () => { expect(fetchJsonParsedAccounts).not.toHaveBeenCalled(); }); - it('should call `decompileTransaction` with the compiled transaction and no lookup tables', async () => { + it('should call the transaction decoder with no lookup tables', async () => { expect.assertions(1); await decodeTransaction(encodedTransaction, rpc); - expect(decompileTransaction).toHaveBeenCalledWith(compiledTransaction, { + expect(getTransactionDecoder).toHaveBeenCalledWith({ addressesByLookupTableAddress: {}, lastValidBlockHeight: undefined, }); }); + + it('should return the result of the `getTransactionDecoder` decode function', async () => { + expect.assertions(1); + const decodePromise = decodeTransaction(encodedTransaction, rpc); + await expect(decodePromise).resolves.toStrictEqual(mockTransaction); + }); + + it('should call the `getTransactionDecoder` decode function', async () => { + expect.assertions(1); + await decodeTransaction(encodedTransaction, rpc); + expect(mockTransactionDecode).toHaveBeenLastCalledWith(encodedTransaction); + }); }); describe('for a versioned transaction with non-empty `addressTableLookups`', () => { @@ -216,6 +289,8 @@ describe('decodeTransaction', () => { signatures: [], }; + const mockTransaction: CompilableTransaction = { version: 0 } as unknown as CompilableTransaction; + const addressInLookup1 = '3333' as Address; const addressInLookup2 = '4444' as Address; @@ -242,10 +317,11 @@ describe('decodeTransaction', () => { }, ]; - // mock `getCompiledTransactionDecoder` to return `compiledTransaction` let mockCompiledTransactionDecode: () => CompiledTransaction; + let mockTransactionDecode: () => CompilableTransaction | (CompilableTransaction & ITransactionWithSignatures); beforeEach(() => { mockCompiledTransactionDecode = jest.fn().mockReturnValue(compiledTransaction); + mockTransactionDecode = jest.fn().mockReturnValue(mockTransaction); jest.mocked(getCompiledTransactionDecoder).mockReturnValue({ decode: mockCompiledTransactionDecode, @@ -253,6 +329,11 @@ describe('decodeTransaction', () => { // mock `fetchJsonParsedAccounts` to resolve to `fetchedLookupTables` jest.mocked(fetchJsonParsedAccounts).mockResolvedValue(fetchedLookupTables); + + // mock `getTransactionDecoder` + jest.mocked(getTransactionDecoder).mockReturnValue({ + decode: mockTransactionDecode, + } as unknown as ReturnType); }); it('should pass the given encoded transaction to the compiled transaction decoder', async () => { @@ -285,10 +366,10 @@ describe('decodeTransaction', () => { ); }); - it('should call `decompileTransaction` with the compiled transaction and the fetched lookup tables', async () => { + it('should call the transaction decoder with no lookup tables', async () => { expect.assertions(1); await decodeTransaction(encodedTransaction, rpc); - expect(decompileTransaction).toHaveBeenCalledWith(compiledTransaction, { + expect(getTransactionDecoder).toHaveBeenCalledWith({ addressesByLookupTableAddress: { [lookupTableAddress1]: [addressInLookup1], [lookupTableAddress2]: [addressInLookup2], @@ -296,5 +377,17 @@ describe('decodeTransaction', () => { lastValidBlockHeight: undefined, }); }); + + it('should return the result of the `getTransactionDecoder` decode function', async () => { + expect.assertions(1); + const decodePromise = decodeTransaction(encodedTransaction, rpc); + await expect(decodePromise).resolves.toStrictEqual(mockTransaction); + }); + + it('should call the `getTransactionDecoder` decode function', async () => { + expect.assertions(1); + await decodeTransaction(encodedTransaction, rpc); + expect(mockTransactionDecode).toHaveBeenLastCalledWith(encodedTransaction); + }); }); }); diff --git a/packages/library/src/decode-transaction.ts b/packages/library/src/decode-transaction.ts index ebabea06e6e6..eb2f7decdf04 100644 --- a/packages/library/src/decode-transaction.ts +++ b/packages/library/src/decode-transaction.ts @@ -6,11 +6,11 @@ import { } from '@solana/accounts'; import type { Address } from '@solana/addresses'; import type { GetMultipleAccountsApi, Rpc } from '@solana/rpc'; +import { type AddressesByLookupTableAddress } from '@solana/transaction-messages'; import { - type AddressesByLookupTableAddress, type CompilableTransaction, - decompileTransaction, getCompiledTransactionDecoder, + getTransactionDecoder, type ITransactionWithSignatures, } from '@solana/transactions'; @@ -67,8 +67,8 @@ export async function decodeTransaction( const fetchedLookupTables = lookupTableAddresses.length > 0 ? await fetchLookupTables(lookupTableAddresses, rpc, fetchAccountsConfig) : {}; - return decompileTransaction(compiledTransaction, { + return getTransactionDecoder({ addressesByLookupTableAddress: fetchedLookupTables, lastValidBlockHeight, - }); + }).decode(encodedTransaction); } diff --git a/packages/transaction-messages/package.json b/packages/transaction-messages/package.json index d32d2139441a..8ec0eeca0e71 100644 --- a/packages/transaction-messages/package.json +++ b/packages/transaction-messages/package.json @@ -68,6 +68,7 @@ "@solana/codecs-data-structures": "workspace:*", "@solana/codecs-numbers": "workspace:*", "@solana/errors": "workspace:*", + "@solana/functional": "workspace:*", "@solana/instructions": "workspace:*", "@solana/rpc-types": "workspace:*" }, diff --git a/packages/transaction-messages/src/__tests__/decompile-message-test.ts b/packages/transaction-messages/src/__tests__/decompile-message-test.ts new file mode 100644 index 000000000000..405f13cb3245 --- /dev/null +++ b/packages/transaction-messages/src/__tests__/decompile-message-test.ts @@ -0,0 +1,1323 @@ +import { Address } from '@solana/addresses'; +import { + SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING, + SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE, + SolanaError, +} from '@solana/errors'; +import { AccountRole, IAccountLookupMeta, IAccountMeta, IInstruction } from '@solana/instructions'; + +import { CompiledTransactionMessage } from '../compile'; +import { decompileTransactionMessage } from '../decompile-message'; +import { NewNonce } from '../durable-nonce'; + +describe('decompileTransactionMessage', () => { + const U64_MAX = 2n ** 64n - 1n; + const feePayer = '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK' as Address; + + describe('for a transaction with a blockhash lifetime', () => { + const blockhash = 'J4yED2jcMAHyQUg61DBmm4njmEydUr2WqrV9cdEcDDgL'; + + it('converts a transaction with no instructions', () => { + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 0, + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, + }, + instructions: [], + lifetimeToken: blockhash, + staticAccounts: [feePayer], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + + expect(transaction.version).toBe(0); + expect(transaction.feePayer).toEqual(feePayer); + expect(transaction.lifetimeConstraint).toEqual({ + blockhash, + lastValidBlockHeight: U64_MAX, + }); + }); + + it('converts a transaction with version legacy', () => { + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 0, + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, + }, + instructions: [], + lifetimeToken: blockhash, + staticAccounts: [feePayer], + version: 'legacy', + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + expect(transaction.version).toBe('legacy'); + }); + + it('converts a transaction with one instruction with no accounts or data', () => { + const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; + + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 1, + // fee payer + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // program address + }, + instructions: [{ programAddressIndex: 1 }], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + const expectedInstruction: IInstruction = { + programAddress, + }; + expect(transaction.instructions).toStrictEqual([expectedInstruction]); + }); + + it('converts a transaction with one instruction with accounts and data', () => { + const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; + + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 2, // 1 passed into instruction + 1 program + numReadonlySignerAccounts: 1, + numSignerAccounts: 3, // fee payer + 2 passed into instruction + }, + instructions: [ + { + accountIndices: [1, 2, 3, 4], + data: new Uint8Array([0, 1, 2, 3, 4]), + programAddressIndex: 5, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [ + // writable signers + feePayer, + 'H4RdPRWYk3pKw2CkNznxQK6J6herjgQke2pzFJW4GC6x' as Address, + // read-only signers + 'G35QeFd4jpXWfRkuRKwn8g4vYrmn8DWJ5v88Kkpd8z1V' as Address, + // writable non-signers + '3LeBzRE9Yna5zi9R8vdT3MiNQYuEp4gJgVyhhwmqfCtd' as Address, + // read-only non-signers + '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e' as Address, + programAddress, + ], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + + const expectedInstruction: IInstruction = { + accounts: [ + { + address: 'H4RdPRWYk3pKw2CkNznxQK6J6herjgQke2pzFJW4GC6x' as Address, + role: AccountRole.WRITABLE_SIGNER, + }, + { + address: 'G35QeFd4jpXWfRkuRKwn8g4vYrmn8DWJ5v88Kkpd8z1V' as Address, + role: AccountRole.READONLY_SIGNER, + }, + { + address: '3LeBzRE9Yna5zi9R8vdT3MiNQYuEp4gJgVyhhwmqfCtd' as Address, + role: AccountRole.WRITABLE, + }, + { + address: '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e' as Address, + role: AccountRole.READONLY, + }, + ], + data: new Uint8Array([0, 1, 2, 3, 4]), + programAddress, + }; + + expect(transaction.instructions).toStrictEqual([expectedInstruction]); + }); + + it('converts a transaction with multiple instructions', () => { + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 3, // 3 programs + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [{ programAddressIndex: 1 }, { programAddressIndex: 2 }, { programAddressIndex: 3 }], + lifetimeToken: blockhash, + staticAccounts: [ + feePayer, + '3hpECiFPtnyxoWqWqcVyfBUDhPKSZXWDduNXFywo8ncP' as Address, + 'Cmqw16pVQvmW1b7Ek1ioQ5Ggf1PaoXi5XxsK9iVSbRKC' as Address, + 'GJRYBLa6XpfswT1AN5tpGp8NHtUirwAdTPdSYXsW9L3S' as Address, + ], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + + const expectedInstructions: IInstruction[] = [ + { + programAddress: '3hpECiFPtnyxoWqWqcVyfBUDhPKSZXWDduNXFywo8ncP' as Address, + }, + { + programAddress: 'Cmqw16pVQvmW1b7Ek1ioQ5Ggf1PaoXi5XxsK9iVSbRKC' as Address, + }, + { + programAddress: 'GJRYBLa6XpfswT1AN5tpGp8NHtUirwAdTPdSYXsW9L3S' as Address, + }, + ]; + + expect(transaction.instructions).toStrictEqual(expectedInstructions); + }); + + it('converts a transaction with a given lastValidBlockHeight', () => { + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 0, + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, + }, + instructions: [], + lifetimeToken: blockhash, + staticAccounts: [feePayer], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction, { lastValidBlockHeight: 100n }); + expect(transaction.lifetimeConstraint).toEqual({ + blockhash, + lastValidBlockHeight: 100n, + }); + }); + }); + + describe('for a transaction with a durable nonce lifetime', () => { + const nonce = '27kqzE1RifbyoFtibDRTjbnfZ894jsNpuR77JJkt3vgH' as NewNonce; + + // added as writable non-signer in the durable nonce instruction + const nonceAccountAddress = 'DhezFECsqmzuDxeuitFChbghTrwKLdsKdVsGArYbFEtm' as Address; + + // added as read-only signer in the durable nonce instruction + const nonceAuthorityAddress = '2KntmCrnaf63tpNb8UMFFjFGGnYYAKQdmW9SbuCiRvhM' as Address; + + const systemProgramAddress = '11111111111111111111111111111111' as Address; + const recentBlockhashesSysvarAddress = 'SysvarRecentB1ockHashes11111111111111111111' as Address; + + it('converts a transaction with one instruction which is advance nonce (fee payer is nonce authority)', () => { + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 2, // recent blockhashes sysvar, system program + numReadonlySignerAccounts: 0, // nonce authority already added as fee payer + numSignerAccounts: 1, // fee payer and nonce authority are the same account + }, + instructions: [ + { + accountIndices: [ + 1, // nonce account address + 3, // recent blockhashes sysvar + 0, // nonce authority address + ], + data: new Uint8Array([4, 0, 0, 0]), + programAddressIndex: 2, + }, + ], + lifetimeToken: nonce, + staticAccounts: [ + // writable signers + nonceAuthorityAddress, + // no read-only signers + // writable non-signers + nonceAccountAddress, + // read-only non-signers + systemProgramAddress, + recentBlockhashesSysvarAddress, + ], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + + const expectedInstruction: IInstruction = { + accounts: [ + { + address: nonceAccountAddress, + role: AccountRole.WRITABLE, + }, + { + address: recentBlockhashesSysvarAddress, + role: AccountRole.READONLY, + }, + { + address: nonceAuthorityAddress, + role: AccountRole.WRITABLE_SIGNER, + }, + ], + data: new Uint8Array([4, 0, 0, 0]), + programAddress: systemProgramAddress, + }; + + expect(transaction.instructions).toStrictEqual([expectedInstruction]); + expect(transaction.feePayer).toStrictEqual(nonceAuthorityAddress); + expect(transaction.lifetimeConstraint).toStrictEqual({ nonce }); + }); + + it('converts a transaction with one instruction which is advance nonce (fee payer is not nonce authority)', () => { + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 2, // recent blockhashes sysvar, system program + numReadonlySignerAccounts: 1, // nonce authority + numSignerAccounts: 2, // fee payer, nonce authority + }, + instructions: [ + { + accountIndices: [ + 2, // nonce account address + 4, // recent blockhashes sysvar + 1, // nonce authority address + ], + data: new Uint8Array([4, 0, 0, 0]), + programAddressIndex: 3, + }, + ], + lifetimeToken: nonce, + staticAccounts: [ + // writable signers + feePayer, + // read-only signers + nonceAuthorityAddress, + // writable non-signers + nonceAccountAddress, + // read-only non-signers + systemProgramAddress, + recentBlockhashesSysvarAddress, + ], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + + const expectedInstruction: IInstruction = { + accounts: [ + { + address: nonceAccountAddress, + role: AccountRole.WRITABLE, + }, + { + address: recentBlockhashesSysvarAddress, + role: AccountRole.READONLY, + }, + { + address: nonceAuthorityAddress, + role: AccountRole.READONLY_SIGNER, + }, + ], + data: new Uint8Array([4, 0, 0, 0]), + programAddress: systemProgramAddress, + }; + expect(transaction.instructions).toStrictEqual([expectedInstruction]); + }); + + it('converts a durable nonce transaction with multiple instruction', () => { + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 4, // recent blockhashes sysvar, system program, 2 other program addresses + numReadonlySignerAccounts: 0, // nonce authority already added as fee payer + numSignerAccounts: 1, // fee payer and nonce authority are the same account + }, + instructions: [ + { + accountIndices: [ + 1, // nonce account address + 3, // recent blockhashes sysvar + 0, // nonce authority address + ], + data: new Uint8Array([4, 0, 0, 0]), + programAddressIndex: 2, + }, + { + accountIndices: [0, 1], + data: new Uint8Array([1, 2, 3, 4]), + programAddressIndex: 4, + }, + { programAddressIndex: 5 }, + ], + lifetimeToken: nonce, + staticAccounts: [ + // writable signers + nonceAuthorityAddress, + // no read-only signers + // writable non-signers + nonceAccountAddress, + // read-only non-signers + systemProgramAddress, + recentBlockhashesSysvarAddress, + '3hpECiFPtnyxoWqWqcVyfBUDhPKSZXWDduNXFywo8ncP' as Address, + 'Cmqw16pVQvmW1b7Ek1ioQ5Ggf1PaoXi5XxsK9iVSbRKC' as Address, + ], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + + const expectedInstructions: IInstruction[] = [ + { + accounts: [ + { + address: nonceAccountAddress, + role: AccountRole.WRITABLE, + }, + { + address: recentBlockhashesSysvarAddress, + role: AccountRole.READONLY, + }, + { + address: nonceAuthorityAddress, + role: AccountRole.WRITABLE_SIGNER, + }, + ], + data: new Uint8Array([4, 0, 0, 0]), + programAddress: systemProgramAddress, + }, + { + accounts: [ + { + address: nonceAuthorityAddress, + role: AccountRole.WRITABLE_SIGNER, + }, + { + address: nonceAccountAddress, + role: AccountRole.WRITABLE, + }, + ], + data: new Uint8Array([1, 2, 3, 4]), + programAddress: '3hpECiFPtnyxoWqWqcVyfBUDhPKSZXWDduNXFywo8ncP' as Address, + }, + { + programAddress: 'Cmqw16pVQvmW1b7Ek1ioQ5Ggf1PaoXi5XxsK9iVSbRKC' as Address, + }, + ]; + + expect(transaction.instructions).toStrictEqual(expectedInstructions); + expect(transaction.lifetimeConstraint).toStrictEqual({ nonce }); + }); + }); + + describe('for a transaction with address lookup tables', () => { + const blockhash = 'J4yED2jcMAHyQUg61DBmm4njmEydUr2WqrV9cdEcDDgL'; + const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; + + describe('for one lookup table', () => { + const lookupTableAddress = '9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw' as Address; + + it('converts an instruction with a single readonly lookup', () => { + const addressInLookup = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const lookupTables = { + [lookupTableAddress]: [addressInLookup], + }; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress, + readableIndices: [0], + writableIndices: [], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [2], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction, { + addressesByLookupTableAddress: lookupTables, + }); + + const expectedAccountLookupMeta: IAccountLookupMeta = { + address: addressInLookup, + addressIndex: 0, + lookupTableAddress, + role: AccountRole.READONLY, + }; + + expect(transaction.instructions).toStrictEqual([ + { + accounts: [expectedAccountLookupMeta], + programAddress, + }, + ]); + }); + + it('converts an instruction with multiple readonly lookups', () => { + const addressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const addressInLookup2 = '5g6b4v8ivF7haRWMUXT1aewBGsc8xY7B6efGadNc3xYk' as Address; + const lookupTables = { + [lookupTableAddress]: [ + addressInLookup1, + 'HAv2PXRjwr4AL1odpoMNfvsw6bWxjDzURy1nPA6QBhDj' as Address, + addressInLookup2, + ], + }; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress, + readableIndices: [0, 2], + writableIndices: [], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [2, 3], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction, { + addressesByLookupTableAddress: lookupTables, + }); + + const expectedAccountLookupMetas: IAccountLookupMeta[] = [ + { + address: addressInLookup1, + addressIndex: 0, + lookupTableAddress, + role: AccountRole.READONLY, + }, + { + address: addressInLookup2, + addressIndex: 2, + lookupTableAddress, + role: AccountRole.READONLY, + }, + ]; + + expect(transaction.instructions).toStrictEqual([ + { + accounts: expectedAccountLookupMetas, + programAddress, + }, + ]); + }); + + it('converts an instruction with a single writable lookup', () => { + const addressInLookup = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const lookupTables = { + [lookupTableAddress]: [addressInLookup], + }; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress, + readableIndices: [], + writableIndices: [0], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [2], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction, { + addressesByLookupTableAddress: lookupTables, + }); + + const expectedAccountLookupMeta: IAccountLookupMeta = { + address: addressInLookup, + addressIndex: 0, + lookupTableAddress, + role: AccountRole.WRITABLE, + }; + + expect(transaction.instructions).toStrictEqual([ + { + accounts: [expectedAccountLookupMeta], + programAddress, + }, + ]); + }); + + it('converts an instruction with multiple writable lookups', () => { + const addressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const addressInLookup2 = '5g6b4v8ivF7haRWMUXT1aewBGsc8xY7B6efGadNc3xYk' as Address; + const lookupTables = { + [lookupTableAddress]: [ + addressInLookup1, + 'HAv2PXRjwr4AL1odpoMNfvsw6bWxjDzURy1nPA6QBhDj' as Address, + addressInLookup2, + ], + }; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress, + readableIndices: [], + writableIndices: [0, 2], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [2, 3], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction, { + addressesByLookupTableAddress: lookupTables, + }); + + const expectedAccountLookupMetas: IAccountLookupMeta[] = [ + { + address: addressInLookup1, + addressIndex: 0, + lookupTableAddress, + role: AccountRole.WRITABLE, + }, + { + address: addressInLookup2, + addressIndex: 2, + lookupTableAddress, + role: AccountRole.WRITABLE, + }, + ]; + + expect(transaction.instructions).toStrictEqual([ + { + accounts: expectedAccountLookupMetas, + programAddress, + }, + ]); + }); + + it('converts an instruction with a readonly and a writable lookup', () => { + const addressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const addressInLookup2 = '5g6b4v8ivF7haRWMUXT1aewBGsc8xY7B6efGadNc3xYk' as Address; + const lookupTables = { + [lookupTableAddress]: [ + addressInLookup1, + 'HAv2PXRjwr4AL1odpoMNfvsw6bWxjDzURy1nPA6QBhDj' as Address, + addressInLookup2, + ], + }; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress, + readableIndices: [0], + writableIndices: [2], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [2, 3], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction, { + addressesByLookupTableAddress: lookupTables, + }); + + const expectedAccountLookupMetas: IAccountLookupMeta[] = [ + // writable is first since we used account indices [2,3] + { + address: addressInLookup2, + addressIndex: 2, + lookupTableAddress, + role: AccountRole.WRITABLE, + }, + { + address: addressInLookup1, + addressIndex: 0, + lookupTableAddress, + role: AccountRole.READONLY, + }, + ]; + + expect(transaction.instructions).toStrictEqual([ + { + accounts: expectedAccountLookupMetas, + programAddress, + }, + ]); + }); + + it('converts an instruction with a combination of static and lookup accounts', () => { + const addressInLookup = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const lookupTables = { + [lookupTableAddress]: [addressInLookup], + }; + + const staticAddress = 'GbRuWcHyNaVuE9rJE4sKpkHYa9k76VJBCCwGtf87ikH3' as Address; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress, + readableIndices: [0], + writableIndices: [], + }, + ], + header: { + numReadonlyNonSignerAccounts: 2, // 1 static address, 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [1, 3], + programAddressIndex: 2, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, staticAddress, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction, { + addressesByLookupTableAddress: lookupTables, + }); + + const expectedAccountMeta: IAccountMeta = { + address: staticAddress, + role: AccountRole.READONLY, + }; + + const expectedAccountLookupMeta: IAccountLookupMeta = { + address: addressInLookup, + addressIndex: 0, + lookupTableAddress, + role: AccountRole.READONLY, + }; + + expect(transaction.instructions).toStrictEqual([ + { + accounts: [expectedAccountMeta, expectedAccountLookupMeta], + programAddress, + }, + ]); + }); + + it('converts multiple instructions with lookup accounts', () => { + const addressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const addressInLookup2 = '5g6b4v8ivF7haRWMUXT1aewBGsc8xY7B6efGadNc3xYk' as Address; + const lookupTables = { + [lookupTableAddress]: [ + addressInLookup1, + 'HAv2PXRjwr4AL1odpoMNfvsw6bWxjDzURy1nPA6QBhDj' as Address, + addressInLookup2, + ], + }; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress, + readableIndices: [0], + writableIndices: [2], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [2], + programAddressIndex: 1, + }, + { + accountIndices: [3], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction, { + addressesByLookupTableAddress: lookupTables, + }); + + const expectedAccountLookupMeta1: IAccountLookupMeta = { + address: addressInLookup1, + addressIndex: 0, + lookupTableAddress, + role: AccountRole.READONLY, + }; + + const expectedAccountLookupMeta2: IAccountLookupMeta = { + address: addressInLookup2, + addressIndex: 2, + lookupTableAddress, + role: AccountRole.WRITABLE, + }; + + expect(transaction.instructions).toStrictEqual([ + // first instruction uses index 2, which is the writable lookup + { + accounts: [expectedAccountLookupMeta2], + programAddress, + }, + // second instruction uses index 3, the readonly lookup + { + accounts: [expectedAccountLookupMeta1], + programAddress, + }, + ]); + }); + + it('throws if the lookup table is not passed in', () => { + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress, + readableIndices: [0], + writableIndices: [], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [2], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const fn = () => decompileTransactionMessage(compiledTransaction); + expect(fn).toThrow( + new SolanaError( + SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING, + { + lookupTableAddresses: ['9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw'], + }, + ), + ); + }); + + it('throws if a read index is outside the lookup table', () => { + const addressInLookup = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const lookupTables = { + [lookupTableAddress]: [addressInLookup], + }; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress, + readableIndices: [1], + writableIndices: [], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [2], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const fn = () => + decompileTransactionMessage(compiledTransaction, { addressesByLookupTableAddress: lookupTables }); + expect(fn).toThrow( + new SolanaError( + SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE, + { + highestKnownIndex: 0, + highestRequestedIndex: 1, + lookupTableAddress: '9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw', + }, + ), + ); + }); + + it('throws if a write index is outside the lookup table', () => { + const addressInLookup = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const lookupTables = { + [lookupTableAddress]: [addressInLookup], + }; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress, + readableIndices: [], + writableIndices: [1], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [2], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const fn = () => + decompileTransactionMessage(compiledTransaction, { addressesByLookupTableAddress: lookupTables }); + expect(fn).toThrow( + new SolanaError( + SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE, + { + highestKnownIndex: 0, + highestRequestedIndex: 1, + lookupTableAddress: '9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw', + }, + ), + ); + }); + }); + + describe('for multiple lookup tables', () => { + const lookupTableAddress1 = '9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw' as Address; + const lookupTableAddress2 = 'GS7Rphk6CZLoCGbTcbRaPZzD3k4ZK8XiA5BAj89Fi2Eg' as Address; + const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; + + it('converts an instruction with readonly accounts from two lookup tables', () => { + const addressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const addressInLookup2 = 'E7p56hzZZEs9vJ1yjxAFjhUP3fN2UJNk2nWvcY7Hz3ee' as Address; + const lookupTables = { + [lookupTableAddress1]: [addressInLookup1], + [lookupTableAddress2]: [addressInLookup2], + }; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress: lookupTableAddress1, + readableIndices: [0], + writableIndices: [], + }, + { + lookupTableAddress: lookupTableAddress2, + readableIndices: [0], + writableIndices: [], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [2, 3], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction, { + addressesByLookupTableAddress: lookupTables, + }); + + const expectedAccountLookupMetas: IAccountLookupMeta[] = [ + { + address: addressInLookup1, + addressIndex: 0, + lookupTableAddress: lookupTableAddress1, + role: AccountRole.READONLY, + }, + { + address: addressInLookup2, + addressIndex: 0, + lookupTableAddress: lookupTableAddress2, + role: AccountRole.READONLY, + }, + ]; + + expect(transaction.instructions).toStrictEqual([ + { + accounts: expectedAccountLookupMetas, + programAddress, + }, + ]); + }); + + it('converts an instruction with writable accounts from two lookup tables', () => { + const addressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const addressInLookup2 = 'E7p56hzZZEs9vJ1yjxAFjhUP3fN2UJNk2nWvcY7Hz3ee' as Address; + const lookupTables = { + [lookupTableAddress1]: [addressInLookup1], + [lookupTableAddress2]: [addressInLookup2], + }; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress: lookupTableAddress1, + readableIndices: [], + writableIndices: [0], + }, + { + lookupTableAddress: lookupTableAddress2, + readableIndices: [], + writableIndices: [0], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [2, 3], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction, { + addressesByLookupTableAddress: lookupTables, + }); + + const expectedAccountLookupMetas: IAccountLookupMeta[] = [ + { + address: addressInLookup1, + addressIndex: 0, + lookupTableAddress: lookupTableAddress1, + role: AccountRole.WRITABLE, + }, + { + address: addressInLookup2, + addressIndex: 0, + lookupTableAddress: lookupTableAddress2, + role: AccountRole.WRITABLE, + }, + ]; + + expect(transaction.instructions).toStrictEqual([ + { + accounts: expectedAccountLookupMetas, + programAddress, + }, + ]); + }); + + it('converts an instruction with readonly and writable accounts from two lookup tables', () => { + const readOnlyaddressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const readonlyAddressInLookup2 = 'E7p56hzZZEs9vJ1yjxAFjhUP3fN2UJNk2nWvcY7Hz3ee' as Address; + const writableAddressInLookup1 = 'FgNrG1D7AoqNJuLc5eqmsXSHWta6Tfu41mQ9dgc5yaXo' as Address; + const writableAddressInLookup2 = '9jEBzMuJfwWH1qcG4g1bj24iSLGCmTsedgisui7SVHes' as Address; + + const lookupTables = { + [lookupTableAddress1]: [readOnlyaddressInLookup1, writableAddressInLookup1], + [lookupTableAddress2]: [readonlyAddressInLookup2, writableAddressInLookup2], + }; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress: lookupTableAddress1, + readableIndices: [0], + writableIndices: [1], + }, + { + lookupTableAddress: lookupTableAddress2, + readableIndices: [0], + writableIndices: [1], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + /* + accountIndices: + 0 - feePayer + 1 - program + 2 - writable from lookup1 + 3 - writable from lookup2 + 4 - readonly from lookup1 + 5 - readonly from lookup2 + */ + instructions: [ + { + accountIndices: [2, 3, 4, 5], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction, { + addressesByLookupTableAddress: lookupTables, + }); + + const expectedAccountLookupMetas: IAccountLookupMeta[] = [ + { + address: writableAddressInLookup1, + addressIndex: 1, + lookupTableAddress: lookupTableAddress1, + role: AccountRole.WRITABLE, + }, + { + address: writableAddressInLookup2, + addressIndex: 1, + lookupTableAddress: lookupTableAddress2, + role: AccountRole.WRITABLE, + }, + { + address: readOnlyaddressInLookup1, + addressIndex: 0, + lookupTableAddress: lookupTableAddress1, + role: AccountRole.READONLY, + }, + { + address: readonlyAddressInLookup2, + addressIndex: 0, + lookupTableAddress: lookupTableAddress2, + role: AccountRole.READONLY, + }, + ]; + + expect(transaction.instructions).toStrictEqual([ + { + accounts: expectedAccountLookupMetas, + programAddress, + }, + ]); + }); + + it('converts multiple instructions with readonly and writable accounts from two lookup tables', () => { + const readOnlyaddressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; + const readonlyAddressInLookup2 = 'E7p56hzZZEs9vJ1yjxAFjhUP3fN2UJNk2nWvcY7Hz3ee' as Address; + const writableAddressInLookup1 = 'FgNrG1D7AoqNJuLc5eqmsXSHWta6Tfu41mQ9dgc5yaXo' as Address; + const writableAddressInLookup2 = '9jEBzMuJfwWH1qcG4g1bj24iSLGCmTsedgisui7SVHes' as Address; + + const lookupTables = { + [lookupTableAddress1]: [readOnlyaddressInLookup1, writableAddressInLookup1], + [lookupTableAddress2]: [readonlyAddressInLookup2, writableAddressInLookup2], + }; + + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress: lookupTableAddress1, + readableIndices: [0], + writableIndices: [1], + }, + { + lookupTableAddress: lookupTableAddress2, + readableIndices: [0], + writableIndices: [1], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + /* + accountIndices: + 0 - feePayer + 1 - program + 2 - writable from lookup1 + 3 - writable from lookup2 + 4 - readonly from lookup1 + 5 - readonly from lookup2 + */ + instructions: [ + { + accountIndices: [2, 5], + programAddressIndex: 1, + }, + { + accountIndices: [3, 4], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction, { + addressesByLookupTableAddress: lookupTables, + }); + + const expectedAccountLookupMetasInstruction1: IAccountLookupMeta[] = [ + // index 2 - writable from lookup1 + { + address: writableAddressInLookup1, + addressIndex: 1, + lookupTableAddress: lookupTableAddress1, + role: AccountRole.WRITABLE, + }, + // index 5 - readonly from lookup2 + { + address: readonlyAddressInLookup2, + addressIndex: 0, + lookupTableAddress: lookupTableAddress2, + role: AccountRole.READONLY, + }, + ]; + + const expectedAccountLookupMetasInstruction2: IAccountLookupMeta[] = [ + // index 3 - writable from lookup2 + { + address: writableAddressInLookup2, + addressIndex: 1, + lookupTableAddress: lookupTableAddress2, + role: AccountRole.WRITABLE, + }, + // index 4 - readonly from lookup1 + { + address: readOnlyaddressInLookup1, + addressIndex: 0, + lookupTableAddress: lookupTableAddress1, + role: AccountRole.READONLY, + }, + ]; + + expect(transaction.instructions).toStrictEqual([ + { + accounts: expectedAccountLookupMetasInstruction1, + programAddress, + }, + { + accounts: expectedAccountLookupMetasInstruction2, + programAddress, + }, + ]); + }); + + it('throws if multiple lookup tables are not passed in', () => { + const compiledTransaction: CompiledTransactionMessage = { + addressTableLookups: [ + { + lookupTableAddress: lookupTableAddress1, + readableIndices: [0], + writableIndices: [], + }, + { + lookupTableAddress: lookupTableAddress2, + readableIndices: [0], + writableIndices: [], + }, + ], + header: { + numReadonlyNonSignerAccounts: 1, // 1 program + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // fee payer + }, + instructions: [ + { + accountIndices: [2], + programAddressIndex: 1, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const fn = () => decompileTransactionMessage(compiledTransaction); + expect(fn).toThrow( + new SolanaError( + SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING, + { + lookupTableAddresses: [ + '9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw', + 'GS7Rphk6CZLoCGbTcbRaPZzD3k4ZK8XiA5BAj89Fi2Eg', + ], + }, + ), + ); + }); + }); + }); +}); diff --git a/packages/transactions/src/decompile-transaction.ts b/packages/transaction-messages/src/decompile-message.ts similarity index 69% rename from packages/transactions/src/decompile-transaction.ts rename to packages/transaction-messages/src/decompile-message.ts index b2a32c344c02..24061ae8f141 100644 --- a/packages/transactions/src/decompile-transaction.ts +++ b/packages/transaction-messages/src/decompile-message.ts @@ -8,22 +8,23 @@ import { } from '@solana/errors'; import { pipe } from '@solana/functional'; import { AccountRole, IAccountLookupMeta, IAccountMeta, IInstruction } from '@solana/instructions'; -import { SignatureBytes } from '@solana/keys'; import type { Blockhash } from '@solana/rpc-types'; -import type { getCompiledAddressTableLookups } from '@solana/transaction-messages'; - -import { setTransactionLifetimeUsingBlockhash } from './blockhash'; -import { CompilableTransaction } from './compilable-transaction'; -import { CompiledTransaction } from './compile-transaction'; -import { createTransaction } from './create-transaction'; -import { isAdvanceNonceAccountInstruction, Nonce, setTransactionLifetimeUsingDurableNonce } from './durable-nonce'; -import { setTransactionFeePayer } from './fee-payer'; -import { appendTransactionInstruction } from './instructions'; -import { CompiledMessage } from './message'; -import { ITransactionWithSignatures } from './signatures'; -import { TransactionVersion } from './types'; - -function getAccountMetas(message: CompiledMessage): IAccountMeta[] { + +import { setTransactionMessageLifetimeUsingBlockhash } from './blockhash'; +import { CompilableTransactionMessage } from './compilable-transaction-message'; +import { CompiledTransactionMessage } from './compile'; +import type { getCompiledAddressTableLookups } from './compile/address-table-lookups'; +import { createTransactionMessage } from './create-transaction-message'; +import { + newIsAdvanceNonceAccountInstruction, + NewNonce, + setTransactionMessageLifetimeUsingDurableNonce, +} from './durable-nonce'; +import { setTransactionMessageFeePayer } from './fee-payer'; +import { appendTransactionMessageInstruction } from './instructions'; +import { NewTransactionVersion } from './transaction-message'; + +function getAccountMetas(message: CompiledTransactionMessage): IAccountMeta[] { const { header } = message; const numWritableSignerAccounts = header.numSignerAccounts - header.numReadonlySignerAccounts; const numWritableNonSignerAccounts = @@ -122,7 +123,7 @@ function getAddressLookupMetas( } function convertInstruction( - instruction: CompiledMessage['instructions'][0], + instruction: CompiledTransactionMessage['instructions'][0], accountMetas: IAccountMeta[], ): IInstruction { const programAddress = accountMetas[instruction.programAddressIndex]?.address; @@ -148,7 +149,7 @@ type LifetimeConstraint = lastValidBlockHeight: bigint; } | { - nonce: Nonce; + nonce: NewNonce; nonceAccountAddress: Address; nonceAuthorityAddress: Address; }; @@ -158,7 +159,7 @@ function getLifetimeConstraint( firstInstruction?: IInstruction, lastValidBlockHeight?: bigint, ): LifetimeConstraint { - if (!firstInstruction || !isAdvanceNonceAccountInstruction(firstInstruction)) { + if (!firstInstruction || !newIsAdvanceNonceAccountInstruction(firstInstruction)) { // first instruction is not advance durable nonce, so use blockhash lifetime constraint return { blockhash: messageLifetimeToken as Blockhash, @@ -173,78 +174,60 @@ function getLifetimeConstraint( assertIsAddress(nonceAuthorityAddress); return { - nonce: messageLifetimeToken as Nonce, + nonce: messageLifetimeToken as NewNonce, nonceAccountAddress, nonceAuthorityAddress, }; } } -function convertSignatures(compiledTransaction: CompiledTransaction): ITransactionWithSignatures['signatures'] { - const { - compiledMessage: { staticAccounts }, - signatures, - } = compiledTransaction; - return signatures.reduce((acc, sig, index) => { - // compiled transaction includes a fake all 0 signature if it hasn't been signed - // we don't store those for the new tx model. So just skip if it's all 0s - const allZeros = sig.every(byte => byte === 0); - if (allZeros) return acc; - - const address = staticAccounts[index]; - return { ...acc, [address]: sig as SignatureBytes }; - }, {}); -} - -export type DecompileTransactionConfig = { +export type DecompileTransactionMessageConfig = { addressesByLookupTableAddress?: AddressesByLookupTableAddress; lastValidBlockHeight?: bigint; }; -export function decompileTransaction( - compiledTransaction: CompiledTransaction, - config?: DecompileTransactionConfig, -): CompilableTransaction | (CompilableTransaction & ITransactionWithSignatures) { - const { compiledMessage } = compiledTransaction; - - const feePayer = compiledMessage.staticAccounts[0]; +export function decompileTransactionMessage( + compiledTransactionMessage: CompiledTransactionMessage, + config?: DecompileTransactionMessageConfig, +): CompilableTransactionMessage { + const feePayer = compiledTransactionMessage.staticAccounts[0]; if (!feePayer) { throw new SolanaError(SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_FEE_PAYER_MISSING); } - const accountMetas = getAccountMetas(compiledMessage); + const accountMetas = getAccountMetas(compiledTransactionMessage); const accountLookupMetas = - 'addressTableLookups' in compiledMessage && - compiledMessage.addressTableLookups !== undefined && - compiledMessage.addressTableLookups.length > 0 - ? getAddressLookupMetas(compiledMessage.addressTableLookups, config?.addressesByLookupTableAddress ?? {}) + 'addressTableLookups' in compiledTransactionMessage && + compiledTransactionMessage.addressTableLookups !== undefined && + compiledTransactionMessage.addressTableLookups.length > 0 + ? getAddressLookupMetas( + compiledTransactionMessage.addressTableLookups, + config?.addressesByLookupTableAddress ?? {}, + ) : []; const transactionMetas = [...accountMetas, ...accountLookupMetas]; - const instructions: IInstruction[] = compiledMessage.instructions.map(compiledInstruction => + const instructions: IInstruction[] = compiledTransactionMessage.instructions.map(compiledInstruction => convertInstruction(compiledInstruction, transactionMetas), ); const firstInstruction = instructions[0]; const lifetimeConstraint = getLifetimeConstraint( - compiledMessage.lifetimeToken, + compiledTransactionMessage.lifetimeToken, firstInstruction, config?.lastValidBlockHeight, ); - const signatures = convertSignatures(compiledTransaction); - return pipe( - createTransaction({ version: compiledMessage.version as TransactionVersion }), - tx => setTransactionFeePayer(feePayer, tx), + createTransactionMessage({ version: compiledTransactionMessage.version as NewTransactionVersion }), + tx => setTransactionMessageFeePayer(feePayer, tx), tx => instructions.reduce((acc, instruction) => { - return appendTransactionInstruction(instruction, acc); + return appendTransactionMessageInstruction(instruction, acc); }, tx), tx => 'blockhash' in lifetimeConstraint - ? setTransactionLifetimeUsingBlockhash(lifetimeConstraint, tx) - : setTransactionLifetimeUsingDurableNonce(lifetimeConstraint, tx), - tx => (Object.keys(signatures).length > 0 ? { ...tx, signatures } : tx), + ? setTransactionMessageLifetimeUsingBlockhash(lifetimeConstraint, tx) + : setTransactionMessageLifetimeUsingDurableNonce(lifetimeConstraint, tx), ); } diff --git a/packages/transaction-messages/src/index.ts b/packages/transaction-messages/src/index.ts index 60a2686baae0..fb763a24e788 100644 --- a/packages/transaction-messages/src/index.ts +++ b/packages/transaction-messages/src/index.ts @@ -2,6 +2,7 @@ export * from './blockhash'; export * from './codecs'; export * from './compile'; // TODO later: can probably delete this at the end export * from './create-transaction-message'; +export * from './decompile-message'; export * from './durable-nonce'; export * from './fee-payer'; export * from './instructions'; diff --git a/packages/transactions/src/__tests__/decompile-transaction-test.ts b/packages/transactions/src/__tests__/decompile-transaction-test.ts deleted file mode 100644 index d70e391dc928..000000000000 --- a/packages/transactions/src/__tests__/decompile-transaction-test.ts +++ /dev/null @@ -1,1697 +0,0 @@ -import { Address } from '@solana/addresses'; -import { - SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING, - SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE, - SolanaError, -} from '@solana/errors'; -import { AccountRole, IAccountLookupMeta, IAccountMeta, IInstruction } from '@solana/instructions'; -import { SignatureBytes } from '@solana/keys'; - -import { decompileTransaction } from '../decompile-transaction'; -import { Nonce } from '../durable-nonce'; -import { CompiledMessage } from '../message'; -import { ITransactionWithSignatures } from '../signatures'; - -type CompiledTransaction = Readonly<{ - compiledMessage: CompiledMessage; - signatures: SignatureBytes[]; -}>; - -describe('decompileTransaction', () => { - const U64_MAX = 2n ** 64n - 1n; - const feePayer = '7EqQdEULxWcraVx3mXKFjc84LhCkMGZCkRuDpvcMwJeK' as Address; - - describe('for a transaction with a blockhash lifetime', () => { - const blockhash = 'J4yED2jcMAHyQUg61DBmm4njmEydUr2WqrV9cdEcDDgL'; - - it('converts a transaction with no instructions', () => { - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 0, - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, - }, - instructions: [], - lifetimeToken: blockhash, - staticAccounts: [feePayer], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction); - - expect(transaction.version).toBe(0); - expect(transaction.feePayer).toEqual(feePayer); - expect(transaction.lifetimeConstraint).toEqual({ - blockhash, - lastValidBlockHeight: U64_MAX, - }); - }); - - it('converts a transaction with version legacy', () => { - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 0, - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, - }, - instructions: [], - lifetimeToken: blockhash, - staticAccounts: [feePayer], - version: 'legacy', - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction); - expect(transaction.version).toBe('legacy'); - }); - - it('converts a transaction with one instruction with no accounts or data', () => { - const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 1, - // fee payer - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // program address - }, - instructions: [{ programAddressIndex: 1 }], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction); - const expectedInstruction: IInstruction = { - programAddress, - }; - expect(transaction.instructions).toStrictEqual([expectedInstruction]); - }); - - it('converts a transaction with one instruction with accounts and data', () => { - const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 2, // 1 passed into instruction + 1 program - numReadonlySignerAccounts: 1, - numSignerAccounts: 3, // fee payer + 2 passed into instruction - }, - instructions: [ - { - accountIndices: [1, 2, 3, 4], - data: new Uint8Array([0, 1, 2, 3, 4]), - programAddressIndex: 5, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [ - // writable signers - feePayer, - 'H4RdPRWYk3pKw2CkNznxQK6J6herjgQke2pzFJW4GC6x' as Address, - // read-only signers - 'G35QeFd4jpXWfRkuRKwn8g4vYrmn8DWJ5v88Kkpd8z1V' as Address, - // writable non-signers - '3LeBzRE9Yna5zi9R8vdT3MiNQYuEp4gJgVyhhwmqfCtd' as Address, - // read-only non-signers - '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e' as Address, - programAddress, - ], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction); - - const expectedInstruction: IInstruction = { - accounts: [ - { - address: 'H4RdPRWYk3pKw2CkNznxQK6J6herjgQke2pzFJW4GC6x' as Address, - role: AccountRole.WRITABLE_SIGNER, - }, - { - address: 'G35QeFd4jpXWfRkuRKwn8g4vYrmn8DWJ5v88Kkpd8z1V' as Address, - role: AccountRole.READONLY_SIGNER, - }, - { - address: '3LeBzRE9Yna5zi9R8vdT3MiNQYuEp4gJgVyhhwmqfCtd' as Address, - role: AccountRole.WRITABLE, - }, - { - address: '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e' as Address, - role: AccountRole.READONLY, - }, - ], - data: new Uint8Array([0, 1, 2, 3, 4]), - programAddress, - }; - - expect(transaction.instructions).toStrictEqual([expectedInstruction]); - }); - - it('converts a transaction with multiple instructions', () => { - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 3, // 3 programs - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [{ programAddressIndex: 1 }, { programAddressIndex: 2 }, { programAddressIndex: 3 }], - lifetimeToken: blockhash, - staticAccounts: [ - feePayer, - '3hpECiFPtnyxoWqWqcVyfBUDhPKSZXWDduNXFywo8ncP' as Address, - 'Cmqw16pVQvmW1b7Ek1ioQ5Ggf1PaoXi5XxsK9iVSbRKC' as Address, - 'GJRYBLa6XpfswT1AN5tpGp8NHtUirwAdTPdSYXsW9L3S' as Address, - ], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction); - - const expectedInstructions: IInstruction[] = [ - { - programAddress: '3hpECiFPtnyxoWqWqcVyfBUDhPKSZXWDduNXFywo8ncP' as Address, - }, - { - programAddress: 'Cmqw16pVQvmW1b7Ek1ioQ5Ggf1PaoXi5XxsK9iVSbRKC' as Address, - }, - { - programAddress: 'GJRYBLa6XpfswT1AN5tpGp8NHtUirwAdTPdSYXsW9L3S' as Address, - }, - ]; - - expect(transaction.instructions).toStrictEqual(expectedInstructions); - }); - - it('converts a transaction with a single signer', () => { - const feePayerSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 0, - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, - }, - instructions: [], - lifetimeToken: blockhash, - staticAccounts: [feePayer], - version: 0, - }, - signatures: [feePayerSignature], - }; - - const transaction = decompileTransaction(compiledTransaction) as ITransactionWithSignatures; - expect(transaction.signatures).toStrictEqual({ - [feePayer]: feePayerSignature as SignatureBytes, - }); - }); - - it('converts a transaction with multiple signers', () => { - const feePayerSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; - - const otherSigner1Address = '3LeBzRE9Yna5zi9R8vdT3MiNQYuEp4gJgVyhhwmqfCtd' as Address; - const otherSigner1Signature = new Uint8Array(Array(64).fill(2)) as SignatureBytes; - - const otherSigner2Address = '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e' as Address; - const otherSigner2Signature = new Uint8Array(Array(64).fill(3)) as SignatureBytes; - - const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 1, - numReadonlySignerAccounts: 2, - numSignerAccounts: 3, - }, - instructions: [ - { - accountIndices: [1, 2], - programAddressIndex: 3, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, otherSigner1Address, otherSigner2Address, programAddress], - version: 0, - }, - signatures: [feePayerSignature, otherSigner1Signature, otherSigner2Signature], - }; - - const transaction = decompileTransaction(compiledTransaction) as ITransactionWithSignatures; - expect(transaction.signatures).toStrictEqual({ - [feePayer]: feePayerSignature, - [otherSigner1Address]: otherSigner1Signature, - [otherSigner2Address]: otherSigner2Signature, - }); - }); - - it('converts a partially signed transaction with multiple signers', () => { - const feePayerSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; - - const otherSigner1Address = '3LeBzRE9Yna5zi9R8vdT3MiNQYuEp4gJgVyhhwmqfCtd' as Address; - const otherSigner2Address = '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e' as Address; - const otherSigner2Signature = new Uint8Array(Array(64).fill(3)) as SignatureBytes; - - const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; - - // Used in the signatures array for a missing signature - const noSignature = new Uint8Array(Array(64).fill(0)) as SignatureBytes; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 1, - numReadonlySignerAccounts: 2, - numSignerAccounts: 3, - }, - instructions: [ - { - accountIndices: [1, 2], - programAddressIndex: 3, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, otherSigner1Address, otherSigner2Address, programAddress], - version: 0, - }, - signatures: [feePayerSignature, noSignature, otherSigner2Signature], - }; - - const transaction = decompileTransaction(compiledTransaction) as ITransactionWithSignatures; - expect(transaction.signatures).toStrictEqual({ - [feePayer]: feePayerSignature, - [otherSigner2Address]: otherSigner2Signature, - }); - }); - - it('converts a transaction with a given lastValidBlockHeight', () => { - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 0, - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, - }, - instructions: [], - lifetimeToken: blockhash, - staticAccounts: [feePayer], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, { lastValidBlockHeight: 100n }); - expect(transaction.lifetimeConstraint).toEqual({ - blockhash, - lastValidBlockHeight: 100n, - }); - }); - - it('excludes the signatures field if the transaction has no signatures', () => { - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 0, - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, - }, - instructions: [], - lifetimeToken: blockhash, - staticAccounts: [feePayer], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, {}); - expect(transaction).not.toHaveProperty('signatures'); - }); - - it('excludes the signatures field if the transaction has only all-zero signatures', () => { - // when we compile a transaction we insert all-zero signatures where they're missing - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 0, - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, - }, - instructions: [], - lifetimeToken: blockhash, - staticAccounts: [feePayer], - version: 0, - }, - signatures: [ - new Uint8Array(64) as SignatureBytes, - new Uint8Array(64) as SignatureBytes, - new Uint8Array(64) as SignatureBytes, - ], - }; - - const transaction = decompileTransaction(compiledTransaction, {}); - expect(transaction).not.toHaveProperty('signatures'); - }); - }); - - describe('for a transaction with a durable nonce lifetime', () => { - const nonce = '27kqzE1RifbyoFtibDRTjbnfZ894jsNpuR77JJkt3vgH' as Nonce; - - // added as writable non-signer in the durable nonce instruction - const nonceAccountAddress = 'DhezFECsqmzuDxeuitFChbghTrwKLdsKdVsGArYbFEtm' as Address; - - // added as read-only signer in the durable nonce instruction - const nonceAuthorityAddress = '2KntmCrnaf63tpNb8UMFFjFGGnYYAKQdmW9SbuCiRvhM' as Address; - - const systemProgramAddress = '11111111111111111111111111111111' as Address; - const recentBlockhashesSysvarAddress = 'SysvarRecentB1ockHashes11111111111111111111' as Address; - - it('converts a transaction with one instruction which is advance nonce (fee payer is nonce authority)', () => { - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 2, // recent blockhashes sysvar, system program - numReadonlySignerAccounts: 0, // nonce authority already added as fee payer - numSignerAccounts: 1, // fee payer and nonce authority are the same account - }, - instructions: [ - { - accountIndices: [ - 1, // nonce account address - 3, // recent blockhashes sysvar - 0, // nonce authority address - ], - data: new Uint8Array([4, 0, 0, 0]), - programAddressIndex: 2, - }, - ], - lifetimeToken: nonce, - staticAccounts: [ - // writable signers - nonceAuthorityAddress, - // no read-only signers - // writable non-signers - nonceAccountAddress, - // read-only non-signers - systemProgramAddress, - recentBlockhashesSysvarAddress, - ], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction); - - const expectedInstruction: IInstruction = { - accounts: [ - { - address: nonceAccountAddress, - role: AccountRole.WRITABLE, - }, - { - address: recentBlockhashesSysvarAddress, - role: AccountRole.READONLY, - }, - { - address: nonceAuthorityAddress, - role: AccountRole.WRITABLE_SIGNER, - }, - ], - data: new Uint8Array([4, 0, 0, 0]), - programAddress: systemProgramAddress, - }; - - expect(transaction.instructions).toStrictEqual([expectedInstruction]); - expect(transaction.feePayer).toStrictEqual(nonceAuthorityAddress); - expect(transaction.lifetimeConstraint).toStrictEqual({ nonce }); - }); - - it('converts a transaction with one instruction which is advance nonce (fee payer is not nonce authority)', () => { - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 2, // recent blockhashes sysvar, system program - numReadonlySignerAccounts: 1, // nonce authority - numSignerAccounts: 2, // fee payer, nonce authority - }, - instructions: [ - { - accountIndices: [ - 2, // nonce account address - 4, // recent blockhashes sysvar - 1, // nonce authority address - ], - data: new Uint8Array([4, 0, 0, 0]), - programAddressIndex: 3, - }, - ], - lifetimeToken: nonce, - staticAccounts: [ - // writable signers - feePayer, - // read-only signers - nonceAuthorityAddress, - // writable non-signers - nonceAccountAddress, - // read-only non-signers - systemProgramAddress, - recentBlockhashesSysvarAddress, - ], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction); - - const expectedInstruction: IInstruction = { - accounts: [ - { - address: nonceAccountAddress, - role: AccountRole.WRITABLE, - }, - { - address: recentBlockhashesSysvarAddress, - role: AccountRole.READONLY, - }, - { - address: nonceAuthorityAddress, - role: AccountRole.READONLY_SIGNER, - }, - ], - data: new Uint8Array([4, 0, 0, 0]), - programAddress: systemProgramAddress, - }; - expect(transaction.instructions).toStrictEqual([expectedInstruction]); - }); - - it('converts a durable nonce transaction with multiple instruction', () => { - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 4, // recent blockhashes sysvar, system program, 2 other program addresses - numReadonlySignerAccounts: 0, // nonce authority already added as fee payer - numSignerAccounts: 1, // fee payer and nonce authority are the same account - }, - instructions: [ - { - accountIndices: [ - 1, // nonce account address - 3, // recent blockhashes sysvar - 0, // nonce authority address - ], - data: new Uint8Array([4, 0, 0, 0]), - programAddressIndex: 2, - }, - { - accountIndices: [0, 1], - data: new Uint8Array([1, 2, 3, 4]), - programAddressIndex: 4, - }, - { programAddressIndex: 5 }, - ], - lifetimeToken: nonce, - staticAccounts: [ - // writable signers - nonceAuthorityAddress, - // no read-only signers - // writable non-signers - nonceAccountAddress, - // read-only non-signers - systemProgramAddress, - recentBlockhashesSysvarAddress, - '3hpECiFPtnyxoWqWqcVyfBUDhPKSZXWDduNXFywo8ncP' as Address, - 'Cmqw16pVQvmW1b7Ek1ioQ5Ggf1PaoXi5XxsK9iVSbRKC' as Address, - ], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction); - - const expectedInstructions: IInstruction[] = [ - { - accounts: [ - { - address: nonceAccountAddress, - role: AccountRole.WRITABLE, - }, - { - address: recentBlockhashesSysvarAddress, - role: AccountRole.READONLY, - }, - { - address: nonceAuthorityAddress, - role: AccountRole.WRITABLE_SIGNER, - }, - ], - data: new Uint8Array([4, 0, 0, 0]), - programAddress: systemProgramAddress, - }, - { - accounts: [ - { - address: nonceAuthorityAddress, - role: AccountRole.WRITABLE_SIGNER, - }, - { - address: nonceAccountAddress, - role: AccountRole.WRITABLE, - }, - ], - data: new Uint8Array([1, 2, 3, 4]), - programAddress: '3hpECiFPtnyxoWqWqcVyfBUDhPKSZXWDduNXFywo8ncP' as Address, - }, - { - programAddress: 'Cmqw16pVQvmW1b7Ek1ioQ5Ggf1PaoXi5XxsK9iVSbRKC' as Address, - }, - ]; - - expect(transaction.instructions).toStrictEqual(expectedInstructions); - expect(transaction.lifetimeConstraint).toStrictEqual({ nonce }); - }); - - it('converts a durable nonce transaction with a single signer', () => { - const feePayerSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 2, // recent blockhashes sysvar, system program - numReadonlySignerAccounts: 0, // nonce authority already added as fee payer - numSignerAccounts: 1, // fee payer and nonce authority are the same account - }, - instructions: [ - { - accountIndices: [ - 1, // nonce account address - 3, // recent blockhashes sysvar - 0, // nonce authority address - ], - data: new Uint8Array([4, 0, 0, 0]), - programAddressIndex: 2, - }, - ], - lifetimeToken: nonce, - staticAccounts: [ - // writable signers - nonceAuthorityAddress, - // no read-only signers - // writable non-signers - nonceAccountAddress, - // read-only non-signers - systemProgramAddress, - recentBlockhashesSysvarAddress, - ], - version: 0, - }, - signatures: [feePayerSignature], - }; - - const transaction = decompileTransaction(compiledTransaction) as ITransactionWithSignatures; - - expect(transaction.signatures).toStrictEqual({ - [nonceAuthorityAddress]: feePayerSignature, - }); - }); - - it('converts a durable nonce transaction with multiple signers', () => { - const feePayerSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; - const authoritySignature = new Uint8Array(Array(64).fill(2)) as SignatureBytes; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 2, // recent blockhashes sysvar, system program - numReadonlySignerAccounts: 1, // nonce authority - numSignerAccounts: 2, // fee payer, nonce authority - }, - instructions: [ - { - accountIndices: [ - 2, // nonce account address - 4, // recent blockhashes sysvar - 1, // nonce authority address - ], - data: new Uint8Array([4, 0, 0, 0]), - programAddressIndex: 3, - }, - ], - lifetimeToken: nonce, - staticAccounts: [ - // writable signers - feePayer, - // read-only signers - nonceAuthorityAddress, - // writable non-signers - nonceAccountAddress, - // read-only non-signers - systemProgramAddress, - recentBlockhashesSysvarAddress, - ], - version: 0, - }, - signatures: [feePayerSignature, authoritySignature], - }; - - const transaction = decompileTransaction(compiledTransaction) as ITransactionWithSignatures; - - expect(transaction.signatures).toStrictEqual({ - [feePayer]: feePayerSignature, - [nonceAuthorityAddress]: authoritySignature, - }); - }); - - it('converts a partially signed durable nonce transaction with multiple signers', () => { - const extraSignerAddress = '9bXC3RtDN5MzDMWRCqjgVTeQK2anMhdkq1ZoGN1Tb1UE' as Address; - - const feePayerSignature = new Uint8Array(Array(64).fill(1)) as SignatureBytes; - const extraSignerSignature = new Uint8Array(Array(64).fill(2)) as SignatureBytes; - - // Used in the signatures array for a missing signature - const noSignature = new Uint8Array(Array(64).fill(0)) as SignatureBytes; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - header: { - numReadonlyNonSignerAccounts: 2, // recent blockhashes sysvar, system program - numReadonlySignerAccounts: 2, // nonce authority, another signer - numSignerAccounts: 3, // fee payer, nonce authority, another signer - }, - instructions: [ - { - accountIndices: [ - 3, // nonce account address - 5, // recent blockhashes sysvar - 1, // nonce authority address - ], - data: new Uint8Array([4, 0, 0, 0]), - programAddressIndex: 4, - }, - { - accountIndices: [2], - programAddressIndex: 4, - }, - ], - lifetimeToken: nonce, - staticAccounts: [ - // writable signers - feePayer, - // read-only signers - nonceAuthorityAddress, - extraSignerAddress, - // writable non-signers - nonceAccountAddress, - // read-only non-signers - systemProgramAddress, - recentBlockhashesSysvarAddress, - ], - version: 0, - }, - signatures: [feePayerSignature, noSignature, extraSignerSignature], - }; - - const transaction = decompileTransaction(compiledTransaction) as ITransactionWithSignatures; - - expect(transaction.signatures).toStrictEqual({ - [extraSignerAddress]: extraSignerSignature, - [feePayer]: feePayerSignature, - }); - }); - }); - - describe('for a transaction with address lookup tables', () => { - const blockhash = 'J4yED2jcMAHyQUg61DBmm4njmEydUr2WqrV9cdEcDDgL'; - const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; - - describe('for one lookup table', () => { - const lookupTableAddress = '9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw' as Address; - - it('converts an instruction with a single readonly lookup', () => { - const addressInLookup = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const lookupTables = { - [lookupTableAddress]: [addressInLookup], - }; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress, - readableIndices: [0], - writableIndices: [], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [2], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, { - addressesByLookupTableAddress: lookupTables, - }); - - const expectedAccountLookupMeta: IAccountLookupMeta = { - address: addressInLookup, - addressIndex: 0, - lookupTableAddress, - role: AccountRole.READONLY, - }; - - expect(transaction.instructions).toStrictEqual([ - { - accounts: [expectedAccountLookupMeta], - programAddress, - }, - ]); - }); - - it('converts an instruction with multiple readonly lookups', () => { - const addressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const addressInLookup2 = '5g6b4v8ivF7haRWMUXT1aewBGsc8xY7B6efGadNc3xYk' as Address; - const lookupTables = { - [lookupTableAddress]: [ - addressInLookup1, - 'HAv2PXRjwr4AL1odpoMNfvsw6bWxjDzURy1nPA6QBhDj' as Address, - addressInLookup2, - ], - }; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress, - readableIndices: [0, 2], - writableIndices: [], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [2, 3], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, { - addressesByLookupTableAddress: lookupTables, - }); - - const expectedAccountLookupMetas: IAccountLookupMeta[] = [ - { - address: addressInLookup1, - addressIndex: 0, - lookupTableAddress, - role: AccountRole.READONLY, - }, - { - address: addressInLookup2, - addressIndex: 2, - lookupTableAddress, - role: AccountRole.READONLY, - }, - ]; - - expect(transaction.instructions).toStrictEqual([ - { - accounts: expectedAccountLookupMetas, - programAddress, - }, - ]); - }); - - it('converts an instruction with a single writable lookup', () => { - const addressInLookup = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const lookupTables = { - [lookupTableAddress]: [addressInLookup], - }; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress, - readableIndices: [], - writableIndices: [0], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [2], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, { - addressesByLookupTableAddress: lookupTables, - }); - - const expectedAccountLookupMeta: IAccountLookupMeta = { - address: addressInLookup, - addressIndex: 0, - lookupTableAddress, - role: AccountRole.WRITABLE, - }; - - expect(transaction.instructions).toStrictEqual([ - { - accounts: [expectedAccountLookupMeta], - programAddress, - }, - ]); - }); - - it('converts an instruction with multiple writable lookups', () => { - const addressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const addressInLookup2 = '5g6b4v8ivF7haRWMUXT1aewBGsc8xY7B6efGadNc3xYk' as Address; - const lookupTables = { - [lookupTableAddress]: [ - addressInLookup1, - 'HAv2PXRjwr4AL1odpoMNfvsw6bWxjDzURy1nPA6QBhDj' as Address, - addressInLookup2, - ], - }; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress, - readableIndices: [], - writableIndices: [0, 2], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [2, 3], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, { - addressesByLookupTableAddress: lookupTables, - }); - - const expectedAccountLookupMetas: IAccountLookupMeta[] = [ - { - address: addressInLookup1, - addressIndex: 0, - lookupTableAddress, - role: AccountRole.WRITABLE, - }, - { - address: addressInLookup2, - addressIndex: 2, - lookupTableAddress, - role: AccountRole.WRITABLE, - }, - ]; - - expect(transaction.instructions).toStrictEqual([ - { - accounts: expectedAccountLookupMetas, - programAddress, - }, - ]); - }); - - it('converts an instruction with a readonly and a writable lookup', () => { - const addressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const addressInLookup2 = '5g6b4v8ivF7haRWMUXT1aewBGsc8xY7B6efGadNc3xYk' as Address; - const lookupTables = { - [lookupTableAddress]: [ - addressInLookup1, - 'HAv2PXRjwr4AL1odpoMNfvsw6bWxjDzURy1nPA6QBhDj' as Address, - addressInLookup2, - ], - }; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress, - readableIndices: [0], - writableIndices: [2], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [2, 3], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, { - addressesByLookupTableAddress: lookupTables, - }); - - const expectedAccountLookupMetas: IAccountLookupMeta[] = [ - // writable is first since we used account indices [2,3] - { - address: addressInLookup2, - addressIndex: 2, - lookupTableAddress, - role: AccountRole.WRITABLE, - }, - { - address: addressInLookup1, - addressIndex: 0, - lookupTableAddress, - role: AccountRole.READONLY, - }, - ]; - - expect(transaction.instructions).toStrictEqual([ - { - accounts: expectedAccountLookupMetas, - programAddress, - }, - ]); - }); - - it('converts an instruction with a combination of static and lookup accounts', () => { - const addressInLookup = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const lookupTables = { - [lookupTableAddress]: [addressInLookup], - }; - - const staticAddress = 'GbRuWcHyNaVuE9rJE4sKpkHYa9k76VJBCCwGtf87ikH3' as Address; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress, - readableIndices: [0], - writableIndices: [], - }, - ], - header: { - numReadonlyNonSignerAccounts: 2, // 1 static address, 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [1, 3], - programAddressIndex: 2, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, staticAddress, programAddress], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, { - addressesByLookupTableAddress: lookupTables, - }); - - const expectedAccountMeta: IAccountMeta = { - address: staticAddress, - role: AccountRole.READONLY, - }; - - const expectedAccountLookupMeta: IAccountLookupMeta = { - address: addressInLookup, - addressIndex: 0, - lookupTableAddress, - role: AccountRole.READONLY, - }; - - expect(transaction.instructions).toStrictEqual([ - { - accounts: [expectedAccountMeta, expectedAccountLookupMeta], - programAddress, - }, - ]); - }); - - it('converts multiple instructions with lookup accounts', () => { - const addressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const addressInLookup2 = '5g6b4v8ivF7haRWMUXT1aewBGsc8xY7B6efGadNc3xYk' as Address; - const lookupTables = { - [lookupTableAddress]: [ - addressInLookup1, - 'HAv2PXRjwr4AL1odpoMNfvsw6bWxjDzURy1nPA6QBhDj' as Address, - addressInLookup2, - ], - }; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress, - readableIndices: [0], - writableIndices: [2], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [2], - programAddressIndex: 1, - }, - { - accountIndices: [3], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, { - addressesByLookupTableAddress: lookupTables, - }); - - const expectedAccountLookupMeta1: IAccountLookupMeta = { - address: addressInLookup1, - addressIndex: 0, - lookupTableAddress, - role: AccountRole.READONLY, - }; - - const expectedAccountLookupMeta2: IAccountLookupMeta = { - address: addressInLookup2, - addressIndex: 2, - lookupTableAddress, - role: AccountRole.WRITABLE, - }; - - expect(transaction.instructions).toStrictEqual([ - // first instruction uses index 2, which is the writable lookup - { - accounts: [expectedAccountLookupMeta2], - programAddress, - }, - // second instruction uses index 3, the readonly lookup - { - accounts: [expectedAccountLookupMeta1], - programAddress, - }, - ]); - }); - - it('throws if the lookup table is not passed in', () => { - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress, - readableIndices: [0], - writableIndices: [], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [2], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const fn = () => decompileTransaction(compiledTransaction); - expect(fn).toThrow( - new SolanaError( - SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING, - { - lookupTableAddresses: ['9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw'], - }, - ), - ); - }); - - it('throws if a read index is outside the lookup table', () => { - const addressInLookup = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const lookupTables = { - [lookupTableAddress]: [addressInLookup], - }; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress, - readableIndices: [1], - writableIndices: [], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [2], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const fn = () => - decompileTransaction(compiledTransaction, { addressesByLookupTableAddress: lookupTables }); - expect(fn).toThrow( - new SolanaError( - SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE, - { - highestKnownIndex: 0, - highestRequestedIndex: 1, - lookupTableAddress: '9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw', - }, - ), - ); - }); - - it('throws if a write index is outside the lookup table', () => { - const addressInLookup = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const lookupTables = { - [lookupTableAddress]: [addressInLookup], - }; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress, - readableIndices: [], - writableIndices: [1], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [2], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const fn = () => - decompileTransaction(compiledTransaction, { addressesByLookupTableAddress: lookupTables }); - expect(fn).toThrow( - new SolanaError( - SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE, - { - highestKnownIndex: 0, - highestRequestedIndex: 1, - lookupTableAddress: '9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw', - }, - ), - ); - }); - }); - - describe('for multiple lookup tables', () => { - const lookupTableAddress1 = '9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw' as Address; - const lookupTableAddress2 = 'GS7Rphk6CZLoCGbTcbRaPZzD3k4ZK8XiA5BAj89Fi2Eg' as Address; - const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; - - it('converts an instruction with readonly accounts from two lookup tables', () => { - const addressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const addressInLookup2 = 'E7p56hzZZEs9vJ1yjxAFjhUP3fN2UJNk2nWvcY7Hz3ee' as Address; - const lookupTables = { - [lookupTableAddress1]: [addressInLookup1], - [lookupTableAddress2]: [addressInLookup2], - }; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress: lookupTableAddress1, - readableIndices: [0], - writableIndices: [], - }, - { - lookupTableAddress: lookupTableAddress2, - readableIndices: [0], - writableIndices: [], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [2, 3], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, { - addressesByLookupTableAddress: lookupTables, - }); - - const expectedAccountLookupMetas: IAccountLookupMeta[] = [ - { - address: addressInLookup1, - addressIndex: 0, - lookupTableAddress: lookupTableAddress1, - role: AccountRole.READONLY, - }, - { - address: addressInLookup2, - addressIndex: 0, - lookupTableAddress: lookupTableAddress2, - role: AccountRole.READONLY, - }, - ]; - - expect(transaction.instructions).toStrictEqual([ - { - accounts: expectedAccountLookupMetas, - programAddress, - }, - ]); - }); - - it('converts an instruction with writable accounts from two lookup tables', () => { - const addressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const addressInLookup2 = 'E7p56hzZZEs9vJ1yjxAFjhUP3fN2UJNk2nWvcY7Hz3ee' as Address; - const lookupTables = { - [lookupTableAddress1]: [addressInLookup1], - [lookupTableAddress2]: [addressInLookup2], - }; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress: lookupTableAddress1, - readableIndices: [], - writableIndices: [0], - }, - { - lookupTableAddress: lookupTableAddress2, - readableIndices: [], - writableIndices: [0], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [2, 3], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, { - addressesByLookupTableAddress: lookupTables, - }); - - const expectedAccountLookupMetas: IAccountLookupMeta[] = [ - { - address: addressInLookup1, - addressIndex: 0, - lookupTableAddress: lookupTableAddress1, - role: AccountRole.WRITABLE, - }, - { - address: addressInLookup2, - addressIndex: 0, - lookupTableAddress: lookupTableAddress2, - role: AccountRole.WRITABLE, - }, - ]; - - expect(transaction.instructions).toStrictEqual([ - { - accounts: expectedAccountLookupMetas, - programAddress, - }, - ]); - }); - - it('converts an instruction with readonly and writable accounts from two lookup tables', () => { - const readOnlyaddressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const readonlyAddressInLookup2 = 'E7p56hzZZEs9vJ1yjxAFjhUP3fN2UJNk2nWvcY7Hz3ee' as Address; - const writableAddressInLookup1 = 'FgNrG1D7AoqNJuLc5eqmsXSHWta6Tfu41mQ9dgc5yaXo' as Address; - const writableAddressInLookup2 = '9jEBzMuJfwWH1qcG4g1bj24iSLGCmTsedgisui7SVHes' as Address; - - const lookupTables = { - [lookupTableAddress1]: [readOnlyaddressInLookup1, writableAddressInLookup1], - [lookupTableAddress2]: [readonlyAddressInLookup2, writableAddressInLookup2], - }; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress: lookupTableAddress1, - readableIndices: [0], - writableIndices: [1], - }, - { - lookupTableAddress: lookupTableAddress2, - readableIndices: [0], - writableIndices: [1], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - /* - accountIndices: - 0 - feePayer - 1 - program - 2 - writable from lookup1 - 3 - writable from lookup2 - 4 - readonly from lookup1 - 5 - readonly from lookup2 - */ - instructions: [ - { - accountIndices: [2, 3, 4, 5], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, { - addressesByLookupTableAddress: lookupTables, - }); - - const expectedAccountLookupMetas: IAccountLookupMeta[] = [ - { - address: writableAddressInLookup1, - addressIndex: 1, - lookupTableAddress: lookupTableAddress1, - role: AccountRole.WRITABLE, - }, - { - address: writableAddressInLookup2, - addressIndex: 1, - lookupTableAddress: lookupTableAddress2, - role: AccountRole.WRITABLE, - }, - { - address: readOnlyaddressInLookup1, - addressIndex: 0, - lookupTableAddress: lookupTableAddress1, - role: AccountRole.READONLY, - }, - { - address: readonlyAddressInLookup2, - addressIndex: 0, - lookupTableAddress: lookupTableAddress2, - role: AccountRole.READONLY, - }, - ]; - - expect(transaction.instructions).toStrictEqual([ - { - accounts: expectedAccountLookupMetas, - programAddress, - }, - ]); - }); - - it('converts multiple instructions with readonly and writable accounts from two lookup tables', () => { - const readOnlyaddressInLookup1 = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; - const readonlyAddressInLookup2 = 'E7p56hzZZEs9vJ1yjxAFjhUP3fN2UJNk2nWvcY7Hz3ee' as Address; - const writableAddressInLookup1 = 'FgNrG1D7AoqNJuLc5eqmsXSHWta6Tfu41mQ9dgc5yaXo' as Address; - const writableAddressInLookup2 = '9jEBzMuJfwWH1qcG4g1bj24iSLGCmTsedgisui7SVHes' as Address; - - const lookupTables = { - [lookupTableAddress1]: [readOnlyaddressInLookup1, writableAddressInLookup1], - [lookupTableAddress2]: [readonlyAddressInLookup2, writableAddressInLookup2], - }; - - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress: lookupTableAddress1, - readableIndices: [0], - writableIndices: [1], - }, - { - lookupTableAddress: lookupTableAddress2, - readableIndices: [0], - writableIndices: [1], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - /* - accountIndices: - 0 - feePayer - 1 - program - 2 - writable from lookup1 - 3 - writable from lookup2 - 4 - readonly from lookup1 - 5 - readonly from lookup2 - */ - instructions: [ - { - accountIndices: [2, 5], - programAddressIndex: 1, - }, - { - accountIndices: [3, 4], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const transaction = decompileTransaction(compiledTransaction, { - addressesByLookupTableAddress: lookupTables, - }); - - const expectedAccountLookupMetasInstruction1: IAccountLookupMeta[] = [ - // index 2 - writable from lookup1 - { - address: writableAddressInLookup1, - addressIndex: 1, - lookupTableAddress: lookupTableAddress1, - role: AccountRole.WRITABLE, - }, - // index 5 - readonly from lookup2 - { - address: readonlyAddressInLookup2, - addressIndex: 0, - lookupTableAddress: lookupTableAddress2, - role: AccountRole.READONLY, - }, - ]; - - const expectedAccountLookupMetasInstruction2: IAccountLookupMeta[] = [ - // index 3 - writable from lookup2 - { - address: writableAddressInLookup2, - addressIndex: 1, - lookupTableAddress: lookupTableAddress2, - role: AccountRole.WRITABLE, - }, - // index 4 - readonly from lookup1 - { - address: readOnlyaddressInLookup1, - addressIndex: 0, - lookupTableAddress: lookupTableAddress1, - role: AccountRole.READONLY, - }, - ]; - - expect(transaction.instructions).toStrictEqual([ - { - accounts: expectedAccountLookupMetasInstruction1, - programAddress, - }, - { - accounts: expectedAccountLookupMetasInstruction2, - programAddress, - }, - ]); - }); - - it('throws if multiple lookup tables are not passed in', () => { - const compiledTransaction: CompiledTransaction = { - compiledMessage: { - addressTableLookups: [ - { - lookupTableAddress: lookupTableAddress1, - readableIndices: [0], - writableIndices: [], - }, - { - lookupTableAddress: lookupTableAddress2, - readableIndices: [0], - writableIndices: [], - }, - ], - header: { - numReadonlyNonSignerAccounts: 1, // 1 program - numReadonlySignerAccounts: 0, - numSignerAccounts: 1, // fee payer - }, - instructions: [ - { - accountIndices: [2], - programAddressIndex: 1, - }, - ], - lifetimeToken: blockhash, - staticAccounts: [feePayer, programAddress], - version: 0, - }, - signatures: [], - }; - - const fn = () => decompileTransaction(compiledTransaction); - expect(fn).toThrow( - new SolanaError( - SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING, - { - lookupTableAddresses: [ - '9wnrQTq5MKhYfp379pKvpy1PvRyteseQmKv4Bw3uQrUw', - 'GS7Rphk6CZLoCGbTcbRaPZzD3k4ZK8XiA5BAj89Fi2Eg', - ], - }, - ), - ); - }); - }); - }); -}); diff --git a/packages/transactions/src/index.ts b/packages/transactions/src/index.ts index 2257fdb27088..de3702b020af 100644 --- a/packages/transactions/src/index.ts +++ b/packages/transactions/src/index.ts @@ -1,7 +1,6 @@ export * from './blockhash'; export * from './compilable-transaction'; export * from './create-transaction'; -export * from './decompile-transaction'; export * from './durable-nonce'; export * from './fee-payer'; export * from './instructions'; diff --git a/packages/transactions/src/serializers/__tests__/transaction-test.ts b/packages/transactions/src/serializers/__tests__/transaction-test.ts index c7e3f1026b1c..6484d8ddf895 100644 --- a/packages/transactions/src/serializers/__tests__/transaction-test.ts +++ b/packages/transactions/src/serializers/__tests__/transaction-test.ts @@ -1,14 +1,17 @@ import { Address } from '@solana/addresses'; import { AccountRole } from '@solana/instructions'; +import { AddressesByLookupTableAddress, decompileTransactionMessage } from '@solana/transaction-messages'; -import { AddressesByLookupTableAddress, decompileTransaction } from '../../decompile-transaction'; import { CompiledMessage, compileTransactionMessage } from '../../message'; import { getCompiledMessageDecoder, getCompiledMessageEncoder } from '../message'; import { getTransactionCodec, getTransactionDecoder, getTransactionEncoder } from '../transaction'; jest.mock('../../message'); jest.mock('../message'); -jest.mock('../../decompile-transaction'); +jest.mock('@solana/transaction-messages', () => ({ + ...jest.requireActual('@solana/transaction-messages'), + decompileTransactionMessage: jest.fn(), +})); let _nextMockAddress = 0; function getMockAddress() { @@ -106,7 +109,7 @@ describe.each([getTransactionDecoder, getTransactionCodec])('Transaction deseria let addressB: Address; let mockCompiledMessage: CompiledMessage; let mockCompiledWireMessage: Uint8Array; - let mockDecompiledTransaction: ReturnType; + let mockDecompiledTransactionMessage: ReturnType; beforeEach(() => { addressA = getMockAddress(); @@ -120,7 +123,7 @@ describe.each([getTransactionDecoder, getTransactionCodec])('Transaction deseria staticAccounts: [addressB, addressA], } as CompiledMessage; mockCompiledWireMessage = new Uint8Array([1, 2, 3]); - mockDecompiledTransaction = { + mockDecompiledTransactionMessage = { instructions: [ { accounts: [ @@ -135,7 +138,7 @@ describe.each([getTransactionDecoder, getTransactionCodec])('Transaction deseria ], }, ], - } as unknown as ReturnType; + } as unknown as ReturnType; (getCompiledMessageEncoder as jest.Mock).mockReturnValue({ getSizeFromValue: jest.fn().mockReturnValue(mockCompiledWireMessage.length), @@ -147,7 +150,7 @@ describe.each([getTransactionDecoder, getTransactionCodec])('Transaction deseria (getCompiledMessageDecoder as jest.Mock).mockReturnValue({ read: jest.fn().mockReturnValue([mockCompiledMessage, 0]), }); - (decompileTransaction as jest.Mock).mockReturnValue(mockDecompiledTransaction); + (decompileTransactionMessage as jest.Mock).mockReturnValue(mockDecompiledTransactionMessage); transaction = deserializerFactory(); }); @@ -164,14 +167,8 @@ describe.each([getTransactionDecoder, getTransactionCodec])('Transaction deseria ]); const decodedTransaction = transaction.decode(bytes); - expect(decodedTransaction).toStrictEqual(mockDecompiledTransaction); - expect(decompileTransaction).toHaveBeenCalledWith( - { - compiledMessage: mockCompiledMessage, - signatures: [noSignature, noSignature], - }, - undefined, - ); + expect(decodedTransaction).toStrictEqual(mockDecompiledTransactionMessage); + expect(decompileTransactionMessage).toHaveBeenCalledWith(mockCompiledMessage, undefined); }); it('deserializes a partially signed transaction', () => { const noSignature = new Uint8Array(Array(64).fill(0)); @@ -188,14 +185,14 @@ describe.each([getTransactionDecoder, getTransactionCodec])('Transaction deseria ]); const decodedTransaction = transaction.decode(bytes); - expect(decodedTransaction).toStrictEqual(mockDecompiledTransaction); - expect(decompileTransaction).toHaveBeenCalledWith( - { - compiledMessage: mockCompiledMessage, - signatures: [noSignature, mockSignatureA], + const expected = { + ...mockDecompiledTransactionMessage, + signatures: { + [addressA]: mockSignatureA, }, - undefined, - ); + }; + expect(decodedTransaction).toStrictEqual(expected); + expect(decompileTransactionMessage).toHaveBeenCalledWith(mockCompiledMessage, undefined); }); it('deserializes a fully signed transaction', () => { const mockSignatureA = new Uint8Array(Array(64).fill(1)); @@ -212,16 +209,17 @@ describe.each([getTransactionDecoder, getTransactionCodec])('Transaction deseria ]); const decodedTransaction = transaction.decode(bytes); - expect(decodedTransaction).toStrictEqual(mockDecompiledTransaction); - expect(decompileTransaction).toHaveBeenCalledWith( - { - compiledMessage: mockCompiledMessage, - signatures: [mockSignatureB, mockSignatureA], + const expected = { + ...mockDecompiledTransactionMessage, + signatures: { + [addressA]: mockSignatureA, + [addressB]: mockSignatureB, }, - undefined, - ); + }; + expect(decodedTransaction).toStrictEqual(expected); + expect(decompileTransactionMessage).toHaveBeenCalledWith(mockCompiledMessage, undefined); }); - it('passes lastValidBlockHeight to decompileTransaction', () => { + it('passes lastValidBlockHeight to decompileTransactionMessage', () => { const noSignature = new Uint8Array(Array(64).fill(0)); const bytes = new Uint8Array([ /** SIGNATURES */ @@ -235,18 +233,12 @@ describe.each([getTransactionDecoder, getTransactionCodec])('Transaction deseria const transaction = deserializerFactory({ lastValidBlockHeight: 100n }); const decodedTransaction = transaction.decode(bytes); - expect(decodedTransaction).toStrictEqual(mockDecompiledTransaction); - expect(decompileTransaction).toHaveBeenCalledWith( - { - compiledMessage: mockCompiledMessage, - signatures: [noSignature, noSignature], - }, - { - lastValidBlockHeight: 100n, - }, - ); + expect(decodedTransaction).toStrictEqual(mockDecompiledTransactionMessage); + expect(decompileTransactionMessage).toHaveBeenCalledWith(mockCompiledMessage, { + lastValidBlockHeight: 100n, + }); }); - it('passes lookupTables to decompileTransaction', () => { + it('passes lookupTables to decompileTransactionMessage', () => { const noSignature = new Uint8Array(Array(64).fill(0)); const bytes = new Uint8Array([ /** SIGNATURES */ @@ -265,15 +257,9 @@ describe.each([getTransactionDecoder, getTransactionCodec])('Transaction deseria const transaction = deserializerFactory({ addressesByLookupTableAddress: lookupTables }); const decodedTransaction = transaction.decode(bytes); - expect(decodedTransaction).toStrictEqual(mockDecompiledTransaction); - expect(decompileTransaction).toHaveBeenCalledWith( - { - compiledMessage: mockCompiledMessage, - signatures: [noSignature, noSignature], - }, - { - addressesByLookupTableAddress: lookupTables, - }, - ); + expect(decodedTransaction).toStrictEqual(mockDecompiledTransactionMessage); + expect(decompileTransactionMessage).toHaveBeenCalledWith(mockCompiledMessage, { + addressesByLookupTableAddress: lookupTables, + }); }); }); diff --git a/packages/transactions/src/serializers/transaction.ts b/packages/transactions/src/serializers/transaction.ts index fad7a99f9338..2f0b74494781 100644 --- a/packages/transactions/src/serializers/transaction.ts +++ b/packages/transactions/src/serializers/transaction.ts @@ -19,10 +19,10 @@ import { } from '@solana/codecs-data-structures'; import { getShortU16Decoder, getShortU16Encoder } from '@solana/codecs-numbers'; import { SignatureBytes } from '@solana/keys'; +import { decompileTransactionMessage, DecompileTransactionMessageConfig } from '@solana/transaction-messages'; import { CompilableTransaction } from '../compilable-transaction'; import { CompiledTransaction, getCompiledTransaction } from '../compile-transaction'; -import { decompileTransaction, DecompileTransactionConfig } from '../decompile-transaction'; import { ITransactionWithSignatures } from '../signatures'; import { getCompiledMessageDecoder, getCompiledMessageEncoder } from './message'; @@ -52,15 +52,46 @@ export function getTransactionEncoder(): VariableSizeEncoder< } export function getTransactionDecoder( - config?: DecompileTransactionConfig, + config?: DecompileTransactionMessageConfig, ): VariableSizeDecoder { return transformDecoder(getCompiledTransactionDecoder(), compiledTransaction => - decompileTransaction(compiledTransaction, config), + tempDecompileTransaction(compiledTransaction, config), ); } export function getTransactionCodec( - config?: DecompileTransactionConfig, + config?: DecompileTransactionMessageConfig, ): VariableSizeCodec { return combineCodec(getTransactionEncoder(), getTransactionDecoder(config)); } + +// temporary adapter from decompileMessage to our old decompileTransaction +// temporary because this serializer will be removed eventually! +function tempDecompileTransaction( + compiledTransaction: CompiledTransaction, + config?: DecompileTransactionMessageConfig, +): CompilableTransaction | (CompilableTransaction & ITransactionWithSignatures) { + const message = decompileTransactionMessage(compiledTransaction.compiledMessage, config); + const signatures = convertSignatures(compiledTransaction); + + const out = Object.keys(signatures).length > 0 ? { ...message, signatures } : message; + return out as CompilableTransaction | (CompilableTransaction & ITransactionWithSignatures); +} + +// copied from decompile-transaction, +// which has moved to transaction-messages as decompile-message +function convertSignatures(compiledTransaction: CompiledTransaction): ITransactionWithSignatures['signatures'] { + const { + compiledMessage: { staticAccounts }, + signatures, + } = compiledTransaction; + return signatures.reduce((acc, sig, index) => { + // compiled transaction includes a fake all 0 signature if it hasn't been signed + // we don't store those for the ITransactionWithSignatures model. So just skip if it's all 0s + const allZeros = sig.every(byte => byte === 0); + if (allZeros) return acc; + + const address = staticAccounts[index]; + return { ...acc, [address]: sig as SignatureBytes }; + }, {}); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c871d4c5cbfd..79c3d2979bb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -981,6 +981,9 @@ importers: '@solana/errors': specifier: workspace:* version: link:../errors + '@solana/functional': + specifier: workspace:* + version: link:../functional '@solana/instructions': specifier: workspace:* version: link:../instructions