From 94459126fb96b095902b1c2168945f58eff8ed03 Mon Sep 17 00:00:00 2001 From: Callum Date: Thu, 28 Mar 2024 14:15:20 +0000 Subject: [PATCH] refactor(experimental): add support for compiling to a new transaction type --- .../library-legacy-sham/src/public-key.ts | 2 +- .../src/__tests__/compile-transaction-test.ts | 4 +- .../src/__tests__/message-test.ts | 20 +++---- .../__tests__/new-compile-transaction-test.ts | 55 +++++++++++++++++++ .../src/__tests__/signatures-test.ts | 6 +- .../__typetests__/transaction-typetests.ts | 8 +-- .../transactions/src/compile-transaction.ts | 4 +- packages/transactions/src/index.ts | 1 + packages/transactions/src/message.ts | 6 +- .../src/new-compile-transaction.ts | 39 +++++++++++++ .../serializers/__tests__/transaction-test.ts | 4 +- packages/transactions/src/signatures.ts | 4 +- 12 files changed, 124 insertions(+), 29 deletions(-) create mode 100644 packages/transactions/src/__tests__/new-compile-transaction-test.ts create mode 100644 packages/transactions/src/new-compile-transaction.ts diff --git a/packages/library-legacy-sham/src/public-key.ts b/packages/library-legacy-sham/src/public-key.ts index b02b4c02f739..c5b89ea3e295 100644 --- a/packages/library-legacy-sham/src/public-key.ts +++ b/packages/library-legacy-sham/src/public-key.ts @@ -15,7 +15,7 @@ export class PublicKey { return this.#address; } #getByteArray() { - return getAddressEncoder().encode(this.#address); + return getAddressEncoder().encode(this.#address) as Uint8Array; } #getBuffer() { if (!__NODEJS__ && typeof Buffer === 'undefined') { diff --git a/packages/transactions/src/__tests__/compile-transaction-test.ts b/packages/transactions/src/__tests__/compile-transaction-test.ts index e33f98245171..99bf588393c1 100644 --- a/packages/transactions/src/__tests__/compile-transaction-test.ts +++ b/packages/transactions/src/__tests__/compile-transaction-test.ts @@ -1,7 +1,7 @@ import { Address } from '@solana/addresses'; import { getCompiledTransaction } from '../compile-transaction'; -import { CompiledMessage, compileMessage } from '../message'; +import { CompiledMessage, compileTransactionMessage } from '../message'; import { ITransactionWithSignatures } from '../signatures'; jest.mock('../message'); @@ -26,7 +26,7 @@ describe('getCompiledTransaction', () => { }, staticAccounts: [addressB, addressA], } as CompiledMessage; - (compileMessage as jest.Mock).mockReturnValue(mockCompiledMessage); + (compileTransactionMessage as jest.Mock).mockReturnValue(mockCompiledMessage); }); it('compiles the transaction message', () => { const compiledTransaction = getCompiledTransaction({} as Parameters[0]); diff --git a/packages/transactions/src/__tests__/message-test.ts b/packages/transactions/src/__tests__/message-test.ts index 98741a6662df..7b933361da0f 100644 --- a/packages/transactions/src/__tests__/message-test.ts +++ b/packages/transactions/src/__tests__/message-test.ts @@ -7,7 +7,7 @@ import { getCompiledInstructions } from '../compile-instructions'; import { getCompiledLifetimeToken } from '../compile-lifetime-token'; import { getCompiledStaticAccounts } from '../compile-static-accounts'; import { ITransactionWithFeePayer } from '../fee-payer'; -import { compileMessage } from '../message'; +import { compileTransactionMessage } from '../message'; import { BaseTransaction } from '../types'; jest.mock('../compile-address-table-lookups'); @@ -19,7 +19,7 @@ jest.mock('../compile-static-accounts'); const MOCK_LIFETIME_CONSTRAINT = 'SOME_CONSTRAINT' as unknown as ITransactionWithBlockhashLifetime['lifetimeConstraint']; -describe('compileMessage', () => { +describe('compileTransactionMessage', () => { let baseTx: BaseTransaction & ITransactionWithBlockhashLifetime & ITransactionWithFeePayer; beforeEach(() => { baseTx = { @@ -40,16 +40,16 @@ describe('compileMessage', () => { legacyBaseTx = { ...baseTx, version: 'legacy' }; }); it('does not set `addressTableLookups`', () => { - const message = compileMessage(legacyBaseTx); + const message = compileTransactionMessage(legacyBaseTx); expect(message).not.toHaveProperty('addressTableLookups'); }); it('does not call `getCompiledAddressTableLookups`', () => { - compileMessage(legacyBaseTx); + compileTransactionMessage(legacyBaseTx); expect(getCompiledAddressTableLookups).not.toHaveBeenCalled(); }); }); it('sets `addressTableLookups` to the return value of `getCompiledAddressTableLookups`', () => { - const message = compileMessage(baseTx); + const message = compileTransactionMessage(baseTx); expect(getCompiledAddressTableLookups).toHaveBeenCalled(); expect(message.addressTableLookups).toBe(expectedAddressTableLookups); }); @@ -64,7 +64,7 @@ describe('compileMessage', () => { jest.mocked(getCompiledMessageHeader).mockReturnValue(expectedCompiledMessageHeader); }); it('sets `header` to the return value of `getCompiledMessageHeader`', () => { - const message = compileMessage(baseTx); + const message = compileTransactionMessage(baseTx); expect(getCompiledMessageHeader).toHaveBeenCalled(); expect(message.header).toBe(expectedCompiledMessageHeader); }); @@ -75,7 +75,7 @@ describe('compileMessage', () => { jest.mocked(getCompiledInstructions).mockReturnValue(expectedInstructions); }); it('sets `instructions` to the return value of `getCompiledInstructions`', () => { - const message = compileMessage(baseTx); + const message = compileTransactionMessage(baseTx); expect(getCompiledInstructions).toHaveBeenCalledWith( baseTx.instructions, expect.any(Array) /* orderedAccounts */, @@ -88,7 +88,7 @@ describe('compileMessage', () => { jest.mocked(getCompiledLifetimeToken).mockReturnValue('abc'); }); it('sets `lifetimeToken` to the return value of `getCompiledLifetimeToken`', () => { - const message = compileMessage(baseTx); + const message = compileTransactionMessage(baseTx); expect(getCompiledLifetimeToken).toHaveBeenCalledWith('SOME_CONSTRAINT'); expect(message.lifetimeToken).toBe('abc'); }); @@ -99,14 +99,14 @@ describe('compileMessage', () => { jest.mocked(getCompiledStaticAccounts).mockReturnValue(expectedStaticAccounts); }); it('sets `staticAccounts` to the return value of `getCompiledStaticAccounts`', () => { - const message = compileMessage(baseTx); + const message = compileTransactionMessage(baseTx); expect(getCompiledStaticAccounts).toHaveBeenCalled(); expect(message.staticAccounts).toBe(expectedStaticAccounts); }); }); describe('versions', () => { it('compiles the version', () => { - const message = compileMessage(baseTx); + const message = compileTransactionMessage(baseTx); expect(message).toHaveProperty('version', 0); }); }); diff --git a/packages/transactions/src/__tests__/new-compile-transaction-test.ts b/packages/transactions/src/__tests__/new-compile-transaction-test.ts new file mode 100644 index 000000000000..1c2fb0924fdf --- /dev/null +++ b/packages/transactions/src/__tests__/new-compile-transaction-test.ts @@ -0,0 +1,55 @@ +import '@solana/test-matchers/toBeFrozenObject'; + +import { Address } from '@solana/addresses'; + +import { CompiledMessage, compileTransactionMessage } from '../message'; +import { compileTransaction } from '../new-compile-transaction'; +import { getCompiledMessageEncoder } from '../serializers'; + +jest.mock('../message'); +jest.mock('../serializers/message'); + +type TransactionMessage = Parameters[0]; + +describe('compileTransactionMessage', () => { + const mockAddressA = '2aaa' as Address; + const mockAddressB = '1aaa' as Address; + const mockCompiledMessage = { + header: { + numSignerAccounts: 2, + }, + staticAccounts: [mockAddressA, mockAddressB], + } as CompiledMessage; + const mockCompiledMessageBytes = new Uint8Array(Array(100)).fill(1); + beforeEach(() => { + (compileTransactionMessage as jest.Mock).mockReturnValue(mockCompiledMessage); + (getCompiledMessageEncoder as jest.Mock).mockReturnValue({ + encode: jest.fn().mockReturnValue(mockCompiledMessageBytes), + }); + }); + + it('compiles the supplied `TransactionMessage` and sets the `messageBytes` property to the result', () => { + const transaction = compileTransaction({} as TransactionMessage); + expect(transaction).toHaveProperty('messageBytes', mockCompiledMessageBytes); + }); + it('compiles an array of signatures the length of the number of signers', () => { + const transaction = compileTransaction({} as TransactionMessage); + expect(Object.keys(transaction.signatures)).toHaveLength(mockCompiledMessage.header.numSignerAccounts); + }); + it("inserts signers into the correct position in the signatures' array", () => { + const transaction = compileTransaction({} as TransactionMessage); + expect(Object.keys(transaction.signatures)).toStrictEqual([ + // Two signers, in the order they're found in `mockCompiledMessage.staticAccounts` + mockAddressA, + mockAddressB, + ]); + }); + it('inserts a null signature into the map for each signer', () => { + const transaction = compileTransaction({} as TransactionMessage); + expect(Object.values(transaction.signatures)).toStrictEqual([null, null]); + }); + it('freezes the returned transaction', () => { + const transaction = compileTransaction({} as TransactionMessage); + expect(transaction).toBeFrozenObject(); + }); +}); diff --git a/packages/transactions/src/__tests__/signatures-test.ts b/packages/transactions/src/__tests__/signatures-test.ts index ce507892e6ad..a1659360bbef 100644 --- a/packages/transactions/src/__tests__/signatures-test.ts +++ b/packages/transactions/src/__tests__/signatures-test.ts @@ -17,7 +17,7 @@ import { SignatureBytes, signBytes } from '@solana/keys'; import type { Blockhash } from '@solana/rpc-types'; import { CompilableTransaction } from '../compilable-transaction'; -import { CompiledMessage, compileMessage } from '../message'; +import { CompiledMessage, compileTransactionMessage } from '../message'; import { assertTransactionIsFullySigned, getSignatureFromTransaction, @@ -68,7 +68,7 @@ describe('partiallySignTransaction', () => { const mockPublicKeyAddressB = 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB' as Address<'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'>; const mockPublicKeyAddressC = 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC' as Address<'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC'>; beforeEach(() => { - (compileMessage as jest.Mock).mockReturnValue({ + (compileTransactionMessage as jest.Mock).mockReturnValue({ header: { numReadonlyNonSignerAccounts: 2, numReadonlySignerAccounts: 1, @@ -227,7 +227,7 @@ describe('signTransaction', () => { const mockKeyPairA = { privateKey: {} as CryptoKey, publicKey: {} as CryptoKey } as CryptoKeyPair; const mockKeyPairB = { privateKey: {} as CryptoKey, publicKey: {} as CryptoKey } as CryptoKeyPair; beforeEach(() => { - (compileMessage as jest.Mock).mockReturnValue({ + (compileTransactionMessage as jest.Mock).mockReturnValue({ header: { numReadonlyNonSignerAccounts: 1, numReadonlySignerAccounts: 1, diff --git a/packages/transactions/src/__typetests__/transaction-typetests.ts b/packages/transactions/src/__typetests__/transaction-typetests.ts index ca1a50d3e3dd..a2a1614321b9 100644 --- a/packages/transactions/src/__typetests__/transaction-typetests.ts +++ b/packages/transactions/src/__typetests__/transaction-typetests.ts @@ -18,7 +18,7 @@ import { } from '..'; import { createTransaction } from '../create-transaction'; import { ITransactionWithFeePayer, setTransactionFeePayer } from '../fee-payer'; -import { CompiledMessage, compileMessage } from '../message'; +import { CompiledMessage, compileTransactionMessage } from '../message'; import { BaseTransaction, Transaction } from '../types'; import { getUnsignedTransaction } from '../unsigned-transaction'; @@ -407,13 +407,13 @@ signTransaction( ITransactionWithFeePayer<'feePayer'> >; -// compileMessage -compileMessage( +// compileTransactionMessage +compileTransactionMessage( null as unknown as Extract & ITransactionWithBlockhashLifetime & ITransactionWithFeePayer<'feePayer'>, ) satisfies Extract; -compileMessage( +compileTransactionMessage( null as unknown as Extract & ITransactionWithBlockhashLifetime & ITransactionWithFeePayer<'feePayer'>, diff --git a/packages/transactions/src/compile-transaction.ts b/packages/transactions/src/compile-transaction.ts index c093a9d58e59..1e8041e32608 100644 --- a/packages/transactions/src/compile-transaction.ts +++ b/packages/transactions/src/compile-transaction.ts @@ -1,7 +1,7 @@ import { SignatureBytes } from '@solana/keys'; import { CompilableTransaction } from './compilable-transaction'; -import { CompiledMessage, compileMessage } from './message'; +import { CompiledMessage, compileTransactionMessage } from './message'; import { ITransactionWithSignatures } from './signatures'; export type CompiledTransaction = Readonly<{ @@ -12,7 +12,7 @@ export type CompiledTransaction = Readonly<{ export function getCompiledTransaction( transaction: CompilableTransaction | (CompilableTransaction & ITransactionWithSignatures), ): CompiledTransaction { - const compiledMessage = compileMessage(transaction); + const compiledMessage = compileTransactionMessage(transaction); let signatures; if ('signatures' in transaction) { signatures = []; diff --git a/packages/transactions/src/index.ts b/packages/transactions/src/index.ts index ab7968c6183f..5b92322bf246 100644 --- a/packages/transactions/src/index.ts +++ b/packages/transactions/src/index.ts @@ -6,6 +6,7 @@ export * from './durable-nonce'; export * from './fee-payer'; export * from './instructions'; export * from './message'; +export * from './new-compile-transaction'; export * from './serializers'; export * from './signatures'; export * from './types'; diff --git a/packages/transactions/src/message.ts b/packages/transactions/src/message.ts index d2295cd20052..491db43aebd7 100644 --- a/packages/transactions/src/message.ts +++ b/packages/transactions/src/message.ts @@ -26,11 +26,11 @@ type VersionedCompiledMessage = BaseCompiledMessage & version: number; }>; -export function compileMessage( +export function compileTransactionMessage( transaction: CompilableTransaction & Readonly<{ version: 'legacy' }>, ): LegacyCompiledMessage; -export function compileMessage(transaction: CompilableTransaction): VersionedCompiledMessage; -export function compileMessage(transaction: CompilableTransaction): CompiledMessage { +export function compileTransactionMessage(transaction: CompilableTransaction): VersionedCompiledMessage; +export function compileTransactionMessage(transaction: CompilableTransaction): CompiledMessage { const addressMap = getAddressMapFromInstructions(transaction.feePayer, transaction.instructions); const orderedAccounts = getOrderedAccountsFromAddressMap(addressMap); return { diff --git a/packages/transactions/src/new-compile-transaction.ts b/packages/transactions/src/new-compile-transaction.ts new file mode 100644 index 000000000000..e1e3388ae285 --- /dev/null +++ b/packages/transactions/src/new-compile-transaction.ts @@ -0,0 +1,39 @@ +import { Address } from '@solana/addresses'; +import { SignatureBytes } from '@solana/keys'; + +import { CompilableTransaction } from './compilable-transaction'; +import { compileTransactionMessage } from './message'; +import { getCompiledMessageEncoder } from './serializers/message'; + +type TypedArrayMutableProperties = 'copyWithin' | 'fill' | 'reverse' | 'set' | 'sort'; +interface ReadonlyUint8Array extends Omit { + readonly [n: number]: number; +} + +export type TransactionMessageBytes = ReadonlyUint8Array & { readonly __brand: unique symbol }; +export type OrderedMap = Record; + +export type NewTransaction = Readonly<{ + messageBytes: TransactionMessageBytes; + signatures: OrderedMap; +}>; + +export function compileTransaction(transactionMessage: CompilableTransaction): NewTransaction { + const compiledMessage = compileTransactionMessage(transactionMessage); + const messageBytes = getCompiledMessageEncoder().encode( + compiledMessage, + ) as ReadonlyUint8Array as TransactionMessageBytes; + + const transactionSigners = compiledMessage.staticAccounts.slice(0, compiledMessage.header.numSignerAccounts); + const signatures: OrderedMap = {}; + for (const signerAddress of transactionSigners) { + signatures[signerAddress] = null; + } + + const transaction: NewTransaction = { + messageBytes: messageBytes as TransactionMessageBytes, + signatures: Object.freeze(signatures), + }; + + return Object.freeze(transaction); +} diff --git a/packages/transactions/src/serializers/__tests__/transaction-test.ts b/packages/transactions/src/serializers/__tests__/transaction-test.ts index 30066a7c719a..c7e3f1026b1c 100644 --- a/packages/transactions/src/serializers/__tests__/transaction-test.ts +++ b/packages/transactions/src/serializers/__tests__/transaction-test.ts @@ -2,7 +2,7 @@ import { Address } from '@solana/addresses'; import { AccountRole } from '@solana/instructions'; import { AddressesByLookupTableAddress, decompileTransaction } from '../../decompile-transaction'; -import { CompiledMessage, compileMessage } from '../../message'; +import { CompiledMessage, compileTransactionMessage } from '../../message'; import { getCompiledMessageDecoder, getCompiledMessageEncoder } from '../message'; import { getTransactionCodec, getTransactionDecoder, getTransactionEncoder } from '../transaction'; @@ -44,7 +44,7 @@ describe.each([getTransactionEncoder, getTransactionCodec])('Transaction seriali (getCompiledMessageDecoder as jest.Mock).mockReturnValue({ read: jest.fn().mockReturnValue([mockCompiledMessage, 0]), }); - (compileMessage as jest.Mock).mockReturnValue(mockCompiledMessage); + (compileTransactionMessage as jest.Mock).mockReturnValue(mockCompiledMessage); transaction = serializerFactory({}); }); it('serializes a transaction with no signatures', () => { diff --git a/packages/transactions/src/signatures.ts b/packages/transactions/src/signatures.ts index 49d5d42b6b5b..7af4e42d5a92 100644 --- a/packages/transactions/src/signatures.ts +++ b/packages/transactions/src/signatures.ts @@ -11,7 +11,7 @@ import { Signature, SignatureBytes, signBytes } from '@solana/keys'; import { CompilableTransaction } from './compilable-transaction'; import { ITransactionWithFeePayer } from './fee-payer'; -import { compileMessage } from './message'; +import { compileTransactionMessage } from './message'; import { getCompiledMessageEncoder } from './serializers/message'; export interface IFullySignedTransaction extends ITransactionWithSignatures { @@ -40,7 +40,7 @@ export async function partiallySignTransaction { - const compiledMessage = compileMessage(transaction); + const compiledMessage = compileTransactionMessage(transaction); const nextSignatures: Record = 'signatures' in transaction ? { ...transaction.signatures } : {}; const wireMessageBytes = getCompiledMessageEncoder().encode(compiledMessage);