Skip to content

Add transaction size helpers #425

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/legal-pillows-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@solana/transactions': patch
'@solana/errors': patch
---

Add a variety of types, constants and functions to help with transaction sizes and their limits
2 changes: 2 additions & 0 deletions packages/errors/src/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export const SOLANA_ERROR__TRANSACTION__CANNOT_ENCODE_WITH_EMPTY_SIGNATURES = 56
export const SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH = 5663017;
export const SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT = 5663018;
export const SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT = 5663019;
export const SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT = 5663020;

// Transaction errors.
// Reserve error codes starting with [7050000-7050999] for the Rust enum `TransactionError`.
Expand Down Expand Up @@ -490,6 +491,7 @@ export type SolanaErrorCode =
| typeof SOLANA_ERROR__TRANSACTION__ADDRESS_MISSING
| typeof SOLANA_ERROR__TRANSACTION__ADDRESSES_CANNOT_SIGN_TRANSACTION
| typeof SOLANA_ERROR__TRANSACTION__CANNOT_ENCODE_WITH_EMPTY_SIGNATURES
| typeof SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT
| typeof SOLANA_ERROR__TRANSACTION__EXPECTED_BLOCKHASH_LIFETIME
| typeof SOLANA_ERROR__TRANSACTION__EXPECTED_NONCE_LIFETIME
| typeof SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING
Expand Down
8 changes: 8 additions & 0 deletions packages/errors/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ import {
SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE,
SOLANA_ERROR__TRANSACTION__ADDRESS_MISSING,
SOLANA_ERROR__TRANSACTION__ADDRESSES_CANNOT_SIGN_TRANSACTION,
SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT,
SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING,
SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE,
SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND,
Expand Down Expand Up @@ -175,6 +176,9 @@ interface ReadonlyUint8Array extends Omit<Uint8Array, TypedArrayMutablePropertie
readonly [n: number]: number;
}

/** A amount of bytes. */
type Bytes = number;

/**
* A map of every {@link SolanaError} code to the type of its `context` property.
*/
Expand Down Expand Up @@ -575,6 +579,10 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
[SOLANA_ERROR__TRANSACTION__ADDRESS_MISSING]: {
index: number;
};
[SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT]: {
transactionSize: Bytes;
transactionSizeLimit: Bytes;
};
[SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING]: {
lookupTableAddresses: string[];
};
Expand Down
3 changes: 3 additions & 0 deletions packages/errors/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ import {
SOLANA_ERROR__TRANSACTION__ADDRESS_MISSING,
SOLANA_ERROR__TRANSACTION__ADDRESSES_CANNOT_SIGN_TRANSACTION,
SOLANA_ERROR__TRANSACTION__CANNOT_ENCODE_WITH_EMPTY_SIGNATURES,
SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT,
SOLANA_ERROR__TRANSACTION__EXPECTED_BLOCKHASH_LIFETIME,
SOLANA_ERROR__TRANSACTION__EXPECTED_NONCE_LIFETIME,
SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING,
Expand Down Expand Up @@ -578,6 +579,8 @@ export const SolanaErrorMessages: Readonly<{
[SOLANA_ERROR__TRANSACTION__ADDRESS_MISSING]: 'Transaction is missing an address at index: $index.',
[SOLANA_ERROR__TRANSACTION__CANNOT_ENCODE_WITH_EMPTY_SIGNATURES]:
'Transaction has no expected signers therefore it cannot be encoded',
[SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT]:
'Transaction size $transactionSize exceeds limit of $transactionSizeLimit bytes',
[SOLANA_ERROR__TRANSACTION__EXPECTED_BLOCKHASH_LIFETIME]: 'Transaction does not have a blockhash lifetime',
[SOLANA_ERROR__TRANSACTION__EXPECTED_NONCE_LIFETIME]: 'Transaction is not a durable nonce transaction',
[SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING]:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { address } from '@solana/addresses';
import { SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, SolanaError } from '@solana/errors';
import { pipe } from '@solana/functional';
import type { Blockhash } from '@solana/rpc-types';
import {
appendTransactionMessageInstruction,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
} from '@solana/transaction-messages';

import {
assertIsTransactionMessageWithinSizeLimit,
getTransactionMessageSize,
isTransactionMessageWithinSizeLimit,
} from '../transaction-message-size';
import { TRANSACTION_SIZE_LIMIT } from '../transaction-size';

const MOCK_BLOCKHASH = {
blockhash: '11111111111111111111111111111111' as Blockhash,
lastValidBlockHeight: 0n,
};

const SMALL_TRANSACTION_MESSAGE = pipe(
createTransactionMessage({ version: 0 }),
m => setTransactionMessageLifetimeUsingBlockhash(MOCK_BLOCKHASH, m),
m => setTransactionMessageFeePayer(address('22222222222222222222222222222222222222222222'), m),
);

const OVERSIZED_TRANSACTION_MESSAGE = pipe(SMALL_TRANSACTION_MESSAGE, m =>
appendTransactionMessageInstruction(
{
data: new Uint8Array(TRANSACTION_SIZE_LIMIT + 1),
programAddress: address('33333333333333333333333333333333333333333333'),
},
m,
),
);

describe('getTransactionMessageSize', () => {
it('gets the size of a compilable transaction message', () => {
expect(getTransactionMessageSize(SMALL_TRANSACTION_MESSAGE)).toBe(136);
});

it('gets the size of an oversized compilable transaction message', () => {
expect(getTransactionMessageSize(OVERSIZED_TRANSACTION_MESSAGE)).toBe(1405);
});
});

describe('isTransactionMessageWithinSizeLimit', () => {
it('returns true when the compiled size is under the transaction size limit', () => {
expect(isTransactionMessageWithinSizeLimit(SMALL_TRANSACTION_MESSAGE)).toBe(true);
});

it('returns false when the compiled size is above the transaction size limit', () => {
expect(isTransactionMessageWithinSizeLimit(OVERSIZED_TRANSACTION_MESSAGE)).toBe(false);
});
});

describe('assertIsTransactionMessageWithinSizeLimit', () => {
it('does not throw when the compiled size is under the transaction size limit', () => {
expect(() => assertIsTransactionMessageWithinSizeLimit(SMALL_TRANSACTION_MESSAGE)).not.toThrow();
});

it('throws when the compiled size is above the transaction size limit', () => {
expect(() => assertIsTransactionMessageWithinSizeLimit(OVERSIZED_TRANSACTION_MESSAGE)).toThrow(
new SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, {
transactionSize: 1405,
transactionSizeLimit: TRANSACTION_SIZE_LIMIT,
}),
);
});
});
78 changes: 78 additions & 0 deletions packages/transactions/src/__tests__/transaction-size-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { address } from '@solana/addresses';
import { SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, SolanaError } from '@solana/errors';
import { pipe } from '@solana/functional';
import type { Blockhash } from '@solana/rpc-types';
import {
appendTransactionMessageInstruction,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
} from '@solana/transaction-messages';

import { compileTransaction } from '../compile-transaction';
import {
assertIsTransactionWithinSizeLimit,
getTransactionSize,
isTransactionWithinSizeLimit,
TRANSACTION_SIZE_LIMIT,
} from '../transaction-size';

const MOCK_BLOCKHASH = {
blockhash: '11111111111111111111111111111111' as Blockhash,
lastValidBlockHeight: 0n,
};

const SMALL_TRANSACTION_MESSAGE = pipe(
createTransactionMessage({ version: 0 }),
m => setTransactionMessageLifetimeUsingBlockhash(MOCK_BLOCKHASH, m),
m => setTransactionMessageFeePayer(address('22222222222222222222222222222222222222222222'), m),
);

const SMALL_TRANSACTION = compileTransaction(SMALL_TRANSACTION_MESSAGE);

const OVERSIZED_TRANSACTION = compileTransaction(
pipe(SMALL_TRANSACTION_MESSAGE, m =>
appendTransactionMessageInstruction(
{
data: new Uint8Array(TRANSACTION_SIZE_LIMIT + 1),
programAddress: address('33333333333333333333333333333333333333333333'),
},
m,
),
),
);

describe('getTransactionSize', () => {
it('gets the size of a transaction', () => {
expect(getTransactionSize(SMALL_TRANSACTION)).toBe(136);
});

it('gets the size of an oversized transaction', () => {
expect(getTransactionSize(OVERSIZED_TRANSACTION)).toBe(1405);
});
});

describe('isTransactionWithinSizeLimit', () => {
it('returns true when the transaction size is under the transaction size limit', () => {
expect(isTransactionWithinSizeLimit(SMALL_TRANSACTION)).toBe(true);
});

it('returns false when the transaction size is above the transaction size limit', () => {
expect(isTransactionWithinSizeLimit(OVERSIZED_TRANSACTION)).toBe(false);
});
});

describe('assertIsTransactionWithinSizeLimit', () => {
it('does not throw when the transaction size is under the transaction size limit', () => {
expect(() => assertIsTransactionWithinSizeLimit(SMALL_TRANSACTION)).not.toThrow();
});

it('throws when the transaction size is above the transaction size limit', () => {
expect(() => assertIsTransactionWithinSizeLimit(OVERSIZED_TRANSACTION)).toThrow(
new SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, {
transactionSize: 1405,
transactionSizeLimit: TRANSACTION_SIZE_LIMIT,
}),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { CompilableTransactionMessage } from '@solana/transaction-messages';

import {
assertIsTransactionMessageWithinSizeLimit,
isTransactionMessageWithinSizeLimit,
TransactionMessageWithinSizeLimit,
} from '../transaction-message-size';

// isTransactionMessageWithinSizeLimit
{
const transactionMessage = null as unknown as CompilableTransactionMessage & { some: 1 };
if (isTransactionMessageWithinSizeLimit(transactionMessage)) {
transactionMessage satisfies CompilableTransactionMessage & TransactionMessageWithinSizeLimit & { some: 1 };
}
}

// assertIsTransactionMessageWithinSizeLimit
{
const transactionMessage = null as unknown as CompilableTransactionMessage & { some: 1 };
assertIsTransactionMessageWithinSizeLimit(transactionMessage);
transactionMessage satisfies CompilableTransactionMessage & TransactionMessageWithinSizeLimit & { some: 1 };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Transaction } from '../transaction';
import {
assertIsTransactionWithinSizeLimit,
isTransactionWithinSizeLimit,
TransactionWithinSizeLimit,
} from '../transaction-size';

// isTransactionWithinSizeLimit
{
const transaction = null as unknown as Transaction & { some: 1 };
if (isTransactionWithinSizeLimit(transaction)) {
transaction satisfies Transaction & TransactionWithinSizeLimit & { some: 1 };
}
}

// assertIsTransactionWithinSizeLimit
{
const transaction = null as unknown as Transaction & { some: 1 };
assertIsTransactionWithinSizeLimit(transaction);
transaction satisfies Transaction & TransactionWithinSizeLimit & { some: 1 };
}
2 changes: 2 additions & 0 deletions packages/transactions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ export * from './lifetime';
export * from './compile-transaction';
export * from './signatures';
export * from './wire-transaction';
export * from './transaction-message-size';
export * from './transaction-size';
export * from './transaction';
70 changes: 70 additions & 0 deletions packages/transactions/src/transaction-message-size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, SolanaError } from '@solana/errors';
import type { NominalType } from '@solana/nominal-types';
import type { CompilableTransactionMessage } from '@solana/transaction-messages';

import { compileTransaction } from './compile-transaction';
import { getTransactionSize, TRANSACTION_SIZE_LIMIT } from './transaction-size';

/**
* Gets the compiled transaction size of a given transaction message in bytes.
*
* @example
* ```ts
* const transactionSize = getTransactionMessageSize(transactionMessage);
* ```
*/
export function getTransactionMessageSize(transactionMessage: CompilableTransactionMessage): number {
return getTransactionSize(compileTransaction(transactionMessage));
}

/**
* A type guard that checks if a transaction message is within the size limit
* when compiled into a transaction.
*/
export type TransactionMessageWithinSizeLimit = NominalType<'transactionSize', 'withinLimit'>;

/**
* Checks if a transaction message is within the size limit
* when compiled into a transaction.
*
* @typeParam TTransactionMessage - The type of the given transaction message.
*
* @example
* ```ts
* if (isTransactionMessageWithinSizeLimit(transactionMessage)) {
* transactionMessage satisfies TransactionMessageWithinSizeLimit;
* }
* ```
*/
export function isTransactionMessageWithinSizeLimit<TTransactionMessage extends CompilableTransactionMessage>(
transactionMessage: TTransactionMessage,
): transactionMessage is TransactionMessageWithinSizeLimit & TTransactionMessage {
return getTransactionMessageSize(transactionMessage) <= TRANSACTION_SIZE_LIMIT;
}

/**
* Asserts that a given transaction message is within the size limit
* when compiled into a transaction.
*
* Throws a {@link SolanaError} of code {@link SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT}
* if the transaction message exceeds the size limit.
*
* @typeParam TTransactionMessage - The type of the given transaction message.
*
* @example
* ```ts
* assertIsTransactionMessageWithinSizeLimit(transactionMessage);
* transactionMessage satisfies TransactionMessageWithinSizeLimit;
* ```
*/
export function assertIsTransactionMessageWithinSizeLimit<TTransactionMessage extends CompilableTransactionMessage>(
transactionMessage: TTransactionMessage,
): asserts transactionMessage is TransactionMessageWithinSizeLimit & TTransactionMessage {
const transactionSize = getTransactionMessageSize(transactionMessage);
if (transactionSize > TRANSACTION_SIZE_LIMIT) {
throw new SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, {
transactionSize,
transactionSizeLimit: TRANSACTION_SIZE_LIMIT,
});
}
}
Loading