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

Commit

Permalink
refactor(experimental): add support for compiling to a new transactio…
Browse files Browse the repository at this point in the history
…n type
  • Loading branch information
mcintyre94 committed Apr 2, 2024
1 parent b45474d commit 9445912
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 29 deletions.
2 changes: 1 addition & 1 deletion packages/library-legacy-sham/src/public-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class PublicKey<TAddress extends string = string> {
return this.#address;
}
#getByteArray() {
return getAddressEncoder().encode(this.#address);
return getAddressEncoder().encode(this.#address) as Uint8Array;
}
#getBuffer() {
if (!__NODEJS__ && typeof Buffer === 'undefined') {
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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<typeof getCompiledTransaction>[0]);
Expand Down
20 changes: 10 additions & 10 deletions packages/transactions/src/__tests__/message-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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 = {
Expand All @@ -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);
});
Expand All @@ -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);
});
Expand All @@ -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 */,
Expand All @@ -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');
});
Expand All @@ -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);
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof compileTransaction>[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();
});
});
6 changes: 3 additions & 3 deletions packages/transactions/src/__tests__/signatures-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -407,13 +407,13 @@ signTransaction(
ITransactionWithFeePayer<'feePayer'>
>;

// compileMessage
compileMessage(
// compileTransactionMessage
compileTransactionMessage(
null as unknown as Extract<Transaction, { version: 'legacy' }> &
ITransactionWithBlockhashLifetime &
ITransactionWithFeePayer<'feePayer'>,
) satisfies Extract<CompiledMessage, { version: 'legacy' }>;
compileMessage(
compileTransactionMessage(
null as unknown as Extract<Transaction, { version: 0 }> &
ITransactionWithBlockhashLifetime &
ITransactionWithFeePayer<'feePayer'>,
Expand Down
4 changes: 2 additions & 2 deletions packages/transactions/src/compile-transaction.ts
Original file line number Diff line number Diff line change
@@ -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<{
Expand All @@ -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 = [];
Expand Down
1 change: 1 addition & 0 deletions packages/transactions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
6 changes: 3 additions & 3 deletions packages/transactions/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
39 changes: 39 additions & 0 deletions packages/transactions/src/new-compile-transaction.ts
Original file line number Diff line number Diff line change
@@ -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<Uint8Array, TypedArrayMutableProperties> {
readonly [n: number]: number;
}

export type TransactionMessageBytes = ReadonlyUint8Array & { readonly __brand: unique symbol };
export type OrderedMap<K extends string, V> = Record<K, V>;

export type NewTransaction = Readonly<{
messageBytes: TransactionMessageBytes;
signatures: OrderedMap<Address, SignatureBytes | null>;
}>;

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<Address, SignatureBytes | null> = {};
for (const signerAddress of transactionSigners) {
signatures[signerAddress] = null;
}

const transaction: NewTransaction = {
messageBytes: messageBytes as TransactionMessageBytes,
signatures: Object.freeze(signatures),
};

return Object.freeze(transaction);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/transactions/src/signatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -40,7 +40,7 @@ export async function partiallySignTransaction<TTransaction extends CompilableTr
keyPairs: CryptoKeyPair[],
transaction: TTransaction | (ITransactionWithSignatures & TTransaction),
): Promise<ITransactionWithSignatures & TTransaction> {
const compiledMessage = compileMessage(transaction);
const compiledMessage = compileTransactionMessage(transaction);
const nextSignatures: Record<Address, SignatureBytes> =
'signatures' in transaction ? { ...transaction.signatures } : {};
const wireMessageBytes = getCompiledMessageEncoder().encode(compiledMessage);
Expand Down

0 comments on commit 9445912

Please sign in to comment.