Skip to content

Document @solana/transaction-messages with TypeDoc #386

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

Open
wants to merge 1 commit into
base: 04-14-replace_transaction_with_transactionmessage_where_the_latter_term_was_used_incorrectly
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { Address } from '@solana/addresses';

/**
* Represents a mapping of lookup table addresses to the addresses of the accounts that are stored
* in them.
*/
export type AddressesByLookupTableAddress = { [lookupTableAddress: Address]: Address[] };
82 changes: 82 additions & 0 deletions packages/transaction-messages/src/blockhash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,62 @@ import { assertIsBlockhash, type Blockhash } from '@solana/rpc-types';
import { TransactionMessageWithDurableNonceLifetime } from './durable-nonce';
import { BaseTransactionMessage } from './transaction-message';

/**
* A constraint which, when applied to a transaction message, makes that transaction message
* eligible to land on the network. The transaction message will continue to be eligible to land
* until the network considers the `blockhash` to be expired.
*
* This can happen when the network proceeds past the `lastValidBlockHeight` for which the blockhash
* is considered valid, or when the network switches to a fork where that blockhash is not present.
*/
type BlockhashLifetimeConstraint = Readonly<{
/**
* A recent blockhash observed by the transaction proposer.
*
* The transaction message will be considered eligible to land until the network determines this
* blockhash to be too old, or has switched to a fork where it is not present.
*/
blockhash: Blockhash;
/**
* This is the block height beyond which the network will consider the blockhash to be too old
* to make a transaction message eligible to land.
*/
lastValidBlockHeight: bigint;
}>;

/**
* Represents a transaction message whose lifetime is defined by the age of the blockhash it
* includes.
*
* Such a transaction can only be landed on the network if the current block height of the network
* is less than or equal to the value of
* `TransactionMessageWithBlockhashLifetime['lifetimeConstraint']['lastValidBlockHeight']`.
*/
export interface TransactionMessageWithBlockhashLifetime {
readonly lifetimeConstraint: BlockhashLifetimeConstraint;
}

/**
* A type guard that returns `true` if the transaction message conforms to the
* {@link TransactionMessageWithBlockhashLifetime} type, and refines its type for use in your
* program.
*
* @example
* ```ts
* import { isTransactionMessageWithBlockhashLifetime } from '@solana/transaction-messages';
*
* if (isTransactionMessageWithBlockhashLifetime(message)) {
* // At this point, `message` has been refined to a `TransactionMessageWithBlockhashLifetime`.
* const { blockhash } = message.lifetimeConstraint;
* const { value: blockhashIsValid } = await rpc.isBlockhashValid(blockhash).send();
* setBlockhashIsValid(blockhashIsValid);
* } else {
* setError(
* `${getSignatureFromTransaction(transaction)} does not have a blockhash-based lifetime`,
* );
* }
* ```
*/
export function isTransactionMessageWithBlockhashLifetime(
transactionMessage: BaseTransactionMessage | (BaseTransactionMessage & TransactionMessageWithBlockhashLifetime),
): transactionMessage is BaseTransactionMessage & TransactionMessageWithBlockhashLifetime {
Expand All @@ -29,6 +76,28 @@ export function isTransactionMessageWithBlockhashLifetime(
}
}

/**
* From time to time you might acquire a transaction message, that you expect to have a
* blockhash-based lifetime, from an untrusted network API or user input. Use this function to
* assert that such a transaction message actually has a blockhash-based lifetime.
*
* @example
* ```ts
* import { assertIsTransactionMessageWithBlockhashLifetime } from '@solana/transaction-messages';
*
* try {
* // If this type assertion function doesn't throw, then
* // Typescript will upcast `message` to `TransactionMessageWithBlockhashLifetime`.
* assertIsTransactionMessageWithBlockhashLifetime(message);
* // At this point, `message` is a `TransactionMessageWithBlockhashLifetime` that can be used
* // with the RPC.
* const { blockhash } = message.lifetimeConstraint;
* const { value: blockhashIsValid } = await rpc.isBlockhashValid(blockhash).send();
* } catch (e) {
* // `message` turned out not to have a blockhash-based lifetime
* }
* ```
*/
export function assertIsTransactionMessageWithBlockhashLifetime(
transactionMessage: BaseTransactionMessage | (BaseTransactionMessage & TransactionMessageWithBlockhashLifetime),
): asserts transactionMessage is BaseTransactionMessage & TransactionMessageWithBlockhashLifetime {
Expand All @@ -37,6 +106,19 @@ export function assertIsTransactionMessageWithBlockhashLifetime(
}
}

/**
* Given a blockhash and the last block height at which that blockhash is considered usable to land
* transactions, this method will return a new transaction message having the same type as the one
* supplied plus the `TransactionMessageWithBlockhashLifetime` type.
*
* @example
* ```ts
* import { setTransactionMessageLifetimeUsingBlockhash } from '@solana/transaction-messages';
*
* const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
* const txMessageWithBlockhashLifetime = setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, txMessage);
* ```
*/
export function setTransactionMessageLifetimeUsingBlockhash<
TTransactionMessage extends BaseTransactionMessage & TransactionMessageWithDurableNonceLifetime,
>(
Expand Down
20 changes: 20 additions & 0 deletions packages/transaction-messages/src/codecs/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ function getAddressTableLookupArrayDecoder() {
return getArrayDecoder(getAddressTableLookupDecoder(), { size: getShortU16Decoder() });
}

/**
* Returns an encoder that you can use to encode a {@link CompiledTransactionMessage} to a byte
* array.
*
* The wire format of a Solana transaction consists of signatures followed by a compiled transaction
* message. The byte array produced by this encoder is the message part.
*/
export function getCompiledTransactionMessageEncoder(): VariableSizeEncoder<CompiledTransactionMessage> {
return createEncoder({
getSizeFromValue: (compiledMessage: CompiledTransactionMessage) => {
Expand All @@ -92,6 +99,13 @@ export function getCompiledTransactionMessageEncoder(): VariableSizeEncoder<Comp
});
}

/**
* Returns a decoder that you can use to decode a byte array representing a
* {@link CompiledTransactionMessage}.
*
* The wire format of a Solana transaction consists of signatures followed by a compiled transaction
* message. You can use this decoder to decode the message part.
*/
export function getCompiledTransactionMessageDecoder(): VariableSizeDecoder<CompiledTransactionMessage> {
return transformDecoder(
getStructDecoder(getPreludeStructDecoderTuple()) as VariableSizeDecoder<
Expand All @@ -109,6 +123,12 @@ export function getCompiledTransactionMessageDecoder(): VariableSizeDecoder<Comp
);
}

/**
* Returns a codec that you can use to encode from or decode to {@link CompiledTransactionMessage}
*
* @see {@link getCompiledTransactionMessageDecoder}
* @see {@link getCompiledTransactionMessageEncoder}
*/
export function getCompiledTransactionMessageCodec(): VariableSizeCodec<CompiledTransactionMessage> {
return combineCodec(getCompiledTransactionMessageEncoder(), getCompiledTransactionMessageDecoder());
}
19 changes: 19 additions & 0 deletions packages/transaction-messages/src/codecs/transaction-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import { TransactionVersion } from '../transaction-message';

const VERSION_FLAG_MASK = 0x80;

/**
* Returns an encoder that you can use to encode a {@link TransactionVersion} to a byte array.
*
* Legacy messages will produce an empty array and will not advance the offset. Versioned messages
* will produce an array with a single byte.
*/
export function getTransactionVersionEncoder(): VariableSizeEncoder<TransactionVersion> {
return createEncoder({
getSizeFromValue: value => (value === 'legacy' ? 0 : 1),
Expand All @@ -31,6 +37,13 @@ export function getTransactionVersionEncoder(): VariableSizeEncoder<TransactionV
});
}

/**
* Returns a decoder that you can use to decode a byte array representing a
* {@link TransactionVersion}.
*
* When the byte at the current offset is determined to represent a legacy transaction, this decoder
* will return `'legacy'` and will not advance the offset.
*/
export function getTransactionVersionDecoder(): VariableSizeDecoder<TransactionVersion> {
return createDecoder({
maxSize: 1,
Expand All @@ -47,6 +60,12 @@ export function getTransactionVersionDecoder(): VariableSizeDecoder<TransactionV
});
}

/**
* Returns a codec that you can use to encode from or decode to {@link TransactionVersion}
*
* @see {@link getTransactionVersionDecoder}
* @see {@link getTransactionVersionEncoder}
*/
export function getTransactionVersionCodec(): VariableSizeCodec<TransactionVersion> {
return combineCodec(getTransactionVersionEncoder(), getTransactionVersionDecoder());
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import { TransactionMessageWithDurableNonceLifetime } from './durable-nonce';
import { ITransactionMessageWithFeePayer } from './fee-payer';
import { BaseTransactionMessage, TransactionVersion } from './transaction-message';

/**
* A transaction message having sufficient detail to be compiled for execution on the network.
*
* In essence, this means that it has at minimum a version, a fee payer, and a lifetime constraint.
*/
export type CompilableTransactionMessage<
TVersion extends TransactionVersion = TransactionVersion,
TInstruction extends IInstruction = IInstruction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import { AccountRole } from '@solana/instructions';
import { OrderedAccounts } from '../compile/accounts';

type AddressTableLookup = Readonly<{
/** The address of the address lookup table account. */
lookupTableAddress: Address;
/** Indices of accounts in a lookup table to load as read-only. */
readableIndices: readonly number[];
/** Indices of accounts in a lookup table to load as writable. */
writableIndices: readonly number[];
}>;

Expand Down
24 changes: 24 additions & 0 deletions packages/transaction-messages/src/compile/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,32 @@ import { isSignerRole, isWritableRole } from '@solana/instructions';
import { OrderedAccounts } from '../compile/accounts';

type MessageHeader = Readonly<{
/**
* The number of accounts in the static accounts list that are neither writable nor
* signers.
*
* Adding this number to `numSignerAccounts` yields the index of the first read-only non-signer
* account in the static accounts list.
*/
numReadonlyNonSignerAccounts: number;
/**
* The number of read-only accounts in the static accounts list that must sign this
* transaction.
*
* Subtracting this number from `numSignerAccounts` yields the index of the first read-only
* signer account in the static accounts list.
*/
numReadonlySignerAccounts: number;
/**
* The number of accounts in the static accounts list that must sign this transaction.
*
* Subtracting `numReadonlySignerAccounts` from this number yields the number of
* writable signer accounts in the static accounts list. Writable signer accounts always
* begin at index zero in the static accounts list.
*
* This number itself is the index of the first non-signer account in the static
* accounts list.
*/
numSignerAccounts: number;
}>;

Expand Down
9 changes: 9 additions & 0 deletions packages/transaction-messages/src/compile/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@ import { IInstruction } from '@solana/instructions';
import { OrderedAccounts } from './accounts';

type CompiledInstruction = Readonly<{
/**
* An ordered list of indices that indicate which accounts in the transaction message's
* accounts list are loaded by this instruction.
*/
accountIndices?: number[];
/** The input to the invoked program */
data?: ReadonlyUint8Array;
/**
* The index of the address in the transaction message's accounts list associated with the
* program to invoke.
*/
programAddressIndex: number;
}>;

Expand Down
31 changes: 31 additions & 0 deletions packages/transaction-messages/src/compile/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,31 @@ import { getCompiledLifetimeToken } from './lifetime-token';
import { getCompiledStaticAccounts } from './static-accounts';

type BaseCompiledTransactionMessage = Readonly<{
/**
* Information about the version of the transaction message and the role of the accounts it
* loads.
*/
header: ReturnType<typeof getCompiledMessageHeader>;
instructions: ReturnType<typeof getCompiledInstructions>;
/**
* 32 bytes of data observed by the transaction proposed that makes a transaction eligible to
* land on the network.
*
* In the case of a transaction message with a nonce lifetime constraint, this will be the value
* of the nonce itself. In all other cases this will be a recent blockhash.
*/
lifetimeToken: ReturnType<typeof getCompiledLifetimeToken>;
/** A list of addresses indicating which accounts to load */
staticAccounts: ReturnType<typeof getCompiledStaticAccounts>;
}>;

/**
* A transaction message in a form suitable for encoding for execution on the network.
*
* You can not fully reconstruct a source message from a compiled message without extra information.
* In particular, supporting details about the lifetime constraint and the concrete addresses of
* accounts sourced from account lookup tables are lost to compilation.
*/
export type CompiledTransactionMessage = LegacyCompiledTransactionMessage | VersionedCompiledTransactionMessage;

type LegacyCompiledTransactionMessage = BaseCompiledTransactionMessage &
Expand All @@ -22,10 +41,22 @@ type LegacyCompiledTransactionMessage = BaseCompiledTransactionMessage &

type VersionedCompiledTransactionMessage = BaseCompiledTransactionMessage &
Readonly<{
/** A list of address tables and the accounts that this transaction loads from them */
addressTableLookups?: ReturnType<typeof getCompiledAddressTableLookups>;
version: number;
}>;

/**
* Converts the type of transaction message data structure that you create in your application to
* the type of transaction message data structure that can be encoded for execution on the network.
*
* This is a lossy process; you can not fully reconstruct a source message from a compiled message
* without extra information. In particular, supporting details about the lifetime constraint and
* the concrete addresses of accounts sourced from account lookup tables will be lost to
* compilation.
*
* @see {@link decompileTransactionMessage}
*/
export function compileTransactionMessage(
transactionMessage: CompilableTransactionMessage & Readonly<{ version: 'legacy' }>,
): LegacyCompiledTransactionMessage;
Expand Down
28 changes: 28 additions & 0 deletions packages/transaction-messages/src/compress-transaction-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,34 @@ type WidenTransactionMessageInstructions<TTransactionMessage extends Transaction
>
: TTransactionMessage;

/**
* Given a transaction message and a mapping of lookup tables to the addresses stored in them, this
* function will return a new transaction message with the same instructions but with all non-signer
* accounts that are found in the given lookup tables represented by an {@link IAccountLookupMeta}
* instead of an {@link IAccountMeta}.
*
* This means that these accounts will take up less space in the compiled transaction message. This
* size reduction is most significant when the transaction includes many accounts from the same
* lookup table.
*
* @example
* ```ts
* import { address } from '@solana/addresses';
* import { compressTransactionMessageUsingAddressLookupTables } from '@solana/transaction-messages';
*
* const lookupTableAddress = address('4QwSwNriKPrz8DLW4ju5uxC2TN5cksJx6tPUPj7DGLAW');
* const accountAddress = address('5n2ADjHPsqB4EVUNEX48xRqtnmuLu5XSHDwkJRR98qpM');
* const lookupTableAddresses: AddressesByLookupTableAddress = {
* [lookupTableAddress]: [accountAddress],
* };
*
* const compressedTransactionMessage = compressTransactionMessageUsingAddressLookupTables(
* transactionMessage,
* lookupTableAddresses,
* );
* ```
*
*/
export function compressTransactionMessageUsingAddressLookupTables<
TTransactionMessage extends TransactionMessageNotLegacy = TransactionMessageNotLegacy,
>(
Expand Down
11 changes: 11 additions & 0 deletions packages/transaction-messages/src/create-transaction-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ type TransactionConfig<TVersion extends TransactionVersion> = Readonly<{
version: TVersion;
}>;

/**
* Given a {@link TransactionVersion} this method will return an empty transaction having the
* capabilities of that version.
*
* @example
* ```ts
* import { createTransactionMessage } from '@solana/transaction-messages';
*
* const message = createTransactionMessage({ version: 0 });
* ```
*/
export function createTransactionMessage<TVersion extends TransactionVersion>(
config: TransactionConfig<TVersion>,
): Extract<TransactionMessage, { version: TVersion }>;
Expand Down
Loading