From f060e526775b212b42271902de59f8ab25392685 Mon Sep 17 00:00:00 2001 From: Callum McIntyre Date: Wed, 10 Apr 2024 08:00:44 +0100 Subject: [PATCH] Move decompile-transaction to decompile-message in transaction-messages (#2465) This PR adds decompile-message to the transaction-messages package. This is the inverse of compile-message: it takes a compiled message (the form we serialise) and returns a TransactionMessage. I think this completes the transaction messages package, other than renaming stuff! The code and tests come from decompile-transaction, which is moved. Changes are very minimal, just renaming things and removing everything to do with signatures This was used by the previous transaction deserializer. I've inlined the signatures code into this, and refactored it to use decompile-message and then handle signatures itself. This is all just temporary since this previous transaction serializer will be removed. I've also modified the library `decodeTransaction`, which uses an RPC to fetch address lookup tables and then uses them to decompile the transaction message, to use the previous `getTransactionDecoder`. This will keep its behaviour unchanged for now. I think this will be refactored to `decodeTransactionMessage` that takes message bytes + an RPC and returns a transaction message object. But that's for another PR! --- .../src/__tests__/decode-transaction-test.ts | 115 +- packages/library/src/decode-transaction.ts | 8 +- packages/transaction-messages/package.json | 1 + .../src/__tests__/decompile-message-test.ts | 1323 +++++++++++++ .../src/decompile-message.ts} | 99 +- packages/transaction-messages/src/index.ts | 1 + .../__tests__/decompile-transaction-test.ts | 1697 ----------------- packages/transactions/src/index.ts | 1 - .../serializers/__tests__/transaction-test.ts | 86 +- .../src/serializers/transaction.ts | 39 +- pnpm-lock.yaml | 3 + 11 files changed, 1548 insertions(+), 1825 deletions(-) create mode 100644 packages/transaction-messages/src/__tests__/decompile-message-test.ts rename packages/{transactions/src/decompile-transaction.ts => transaction-messages/src/decompile-message.ts} (69%) delete mode 100644 packages/transactions/src/__tests__/decompile-transaction-test.ts 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