Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
Move decompile-transaction to decompile-message in transaction-messag…
Browse files Browse the repository at this point in the history
…es (#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!
  • Loading branch information
mcintyre94 authored Apr 10, 2024
1 parent be788e3 commit f060e52
Show file tree
Hide file tree
Showing 11 changed files with 1,548 additions and 1,825 deletions.
115 changes: 104 additions & 11 deletions packages/library/src/__tests__/decode-transaction-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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<typeof getCompiledTransactionDecoder>);

// mock `getTransactionDecoder`

jest.mocked(getTransactionDecoder).mockReturnValue({
decode: mockTransactionDecode,
} as unknown as ReturnType<typeof getTransactionDecoder>);
});

it('should pass the given encoded transaction to the compiled transaction decoder', async () => {
Expand All @@ -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,
});
Expand All @@ -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<typeof getCompiledTransactionDecoder>);

// mock `getTransactionDecoder`

jest.mocked(getTransactionDecoder).mockReturnValue({
decode: mockTransactionDecode,
} as unknown as ReturnType<typeof getTransactionDecoder>);
});

it('should pass the given encoded transaction to the compiled transaction decoder', async () => {
Expand All @@ -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`', () => {
Expand All @@ -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<typeof getCompiledTransactionDecoder>);

// mock `getTransactionDecoder`
jest.mocked(getTransactionDecoder).mockReturnValue({
decode: mockTransactionDecode,
} as unknown as ReturnType<typeof getTransactionDecoder>);
});

it('should pass the given encoded transaction to the compiled transaction decoder', async () => {
Expand All @@ -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`', () => {
Expand Down Expand Up @@ -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;

Expand All @@ -242,17 +317,23 @@ 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,
} as unknown as ReturnType<typeof getCompiledTransactionDecoder>);

// mock `fetchJsonParsedAccounts` to resolve to `fetchedLookupTables`
jest.mocked(fetchJsonParsedAccounts).mockResolvedValue(fetchedLookupTables);

// mock `getTransactionDecoder`
jest.mocked(getTransactionDecoder).mockReturnValue({
decode: mockTransactionDecode,
} as unknown as ReturnType<typeof getTransactionDecoder>);
});

it('should pass the given encoded transaction to the compiled transaction decoder', async () => {
Expand Down Expand Up @@ -285,16 +366,28 @@ 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],
},
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);
});
});
});
8 changes: 4 additions & 4 deletions packages/library/src/decode-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions packages/transaction-messages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*"
},
Expand Down
Loading

0 comments on commit f060e52

Please sign in to comment.