From 087a3a2076d599a62e9dc3ef0d1809e650278b98 Mon Sep 17 00:00:00 2001 From: Callum Date: Wed, 1 Oct 2025 18:40:33 +0000 Subject: [PATCH 1/2] Update the signer API to return Transaction & TransactionWithLifetime --- examples/signers/src/example.ts | 2 + examples/transfer-lamports/src/example.ts | 2 + .../src/useWalletAccountTransactionSigner.ts | 9 +++- .../src/__tests__/keypair-signer-test.ts | 11 +++-- .../signers/src/__tests__/noop-signer-test.ts | 10 ++-- .../sign-transaction-typetest.ts | 28 +++++------ packages/signers/src/sign-transaction.ts | 49 +++++++------------ .../src/transaction-modifying-signer.ts | 17 ++++--- .../signers/src/transaction-partial-signer.ts | 4 +- .../signers/src/transaction-sending-signer.ts | 4 +- 10 files changed, 68 insertions(+), 68 deletions(-) diff --git a/examples/signers/src/example.ts b/examples/signers/src/example.ts index 65a86ab10..fed83953f 100644 --- a/examples/signers/src/example.ts +++ b/examples/signers/src/example.ts @@ -11,6 +11,7 @@ import { createLogger } from '@solana/example-utils/createLogger.js'; import { address, appendTransactionMessageInstruction, + assertIsTransactionWithinSizeLimit, Blockhash, compileTransaction, createKeyPairSignerFromBytes, @@ -92,6 +93,7 @@ async function signTransaction( transactionMessage: TransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithLifetime, ) { const transaction = compileTransaction(transactionMessage); + assertIsTransactionWithinSizeLimit(transaction); const [signatureDictionary] = await signer.signTransactions([transaction]); const signature = signatureDictionary[signer.address]; log.info( diff --git a/examples/transfer-lamports/src/example.ts b/examples/transfer-lamports/src/example.ts index 89246a7ae..12e3742ac 100644 --- a/examples/transfer-lamports/src/example.ts +++ b/examples/transfer-lamports/src/example.ts @@ -13,6 +13,7 @@ import { address, appendTransactionMessageInstruction, assertIsSendableTransaction, + assertIsTransactionWithBlockhashLifetime, createKeyPairSignerFromBytes, createSolanaRpc, createSolanaRpcSubscriptions, @@ -184,6 +185,7 @@ log.warn( ); try { assertIsSendableTransaction(signedTransaction); + assertIsTransactionWithBlockhashLifetime(signedTransaction); await sendAndConfirmTransaction(signedTransaction, { commitment: 'confirmed' }); log.info('[success] Transfer confirmed'); await pressAnyKeyPrompt('Press any key to quit'); diff --git a/packages/react/src/useWalletAccountTransactionSigner.ts b/packages/react/src/useWalletAccountTransactionSigner.ts index 61d9481d9..ec59e7708 100644 --- a/packages/react/src/useWalletAccountTransactionSigner.ts +++ b/packages/react/src/useWalletAccountTransactionSigner.ts @@ -5,8 +5,11 @@ import { getAbortablePromise } from '@solana/promises'; import { TransactionModifyingSigner } from '@solana/signers'; import { getCompiledTransactionMessageDecoder } from '@solana/transaction-messages'; import { + assertIsTransactionWithinSizeLimit, getTransactionCodec, getTransactionLifetimeConstraintFromCompiledTransactionMessage, + Transaction, + TransactionWithinSizeLimit, TransactionWithLifetime, } from '@solana/transactions'; import { UiWalletAccount } from '@wallet-standard/ui'; @@ -74,7 +77,9 @@ export function useWalletAccountTransactionSigner { // And given we have a couple of mock transactions to sign. const mockTransactions = [ - {} as Transaction & TransactionWithLifetime, - {} as Transaction & TransactionWithLifetime, + {} as Transaction & TransactionWithinSizeLimit & TransactionWithLifetime, + {} as Transaction & TransactionWithinSizeLimit & TransactionWithLifetime, ]; // And given we mock the next two calls of the partiallySignTransaction function. diff --git a/packages/signers/src/__tests__/noop-signer-test.ts b/packages/signers/src/__tests__/noop-signer-test.ts index 6b486d91c..a6efa1be7 100644 --- a/packages/signers/src/__tests__/noop-signer-test.ts +++ b/packages/signers/src/__tests__/noop-signer-test.ts @@ -1,7 +1,7 @@ import '@solana/test-matchers/toBeFrozenObject'; import { address } from '@solana/addresses'; -import { Transaction, TransactionWithLifetime } from '@solana/transactions'; +import { Transaction, TransactionWithinSizeLimit, TransactionWithLifetime } from '@solana/transactions'; import { createNoopSigner, NoopSigner } from '../noop-signer'; import { createSignableMessage } from '../signable-message'; @@ -53,8 +53,8 @@ describe('createNoopSigner', () => { // And given we have a couple of mock transactions to sign. const mockTransactions = [ - {} as Transaction & TransactionWithLifetime, - {} as Transaction & TransactionWithLifetime, + {} as Transaction & TransactionWithinSizeLimit & TransactionWithLifetime, + {} as Transaction & TransactionWithinSizeLimit & TransactionWithLifetime, ]; // When we sign both transactions using that signer. @@ -75,8 +75,8 @@ describe('createNoopSigner', () => { // And given we have a couple of mock transactions to sign. const mockTransactions = [ - {} as Transaction & TransactionWithLifetime, - {} as Transaction & TransactionWithLifetime, + {} as Transaction & TransactionWithinSizeLimit & TransactionWithLifetime, + {} as Transaction & TransactionWithinSizeLimit & TransactionWithLifetime, ]; // When we sign both transactions using that signer. diff --git a/packages/signers/src/__typetests__/sign-transaction-typetest.ts b/packages/signers/src/__typetests__/sign-transaction-typetest.ts index a680beebe..2c4d45135 100644 --- a/packages/signers/src/__typetests__/sign-transaction-typetest.ts +++ b/packages/signers/src/__typetests__/sign-transaction-typetest.ts @@ -11,8 +11,6 @@ import { import { FullySignedTransaction, Transaction, - TransactionWithBlockhashLifetime, - TransactionWithDurableNonceLifetime, TransactionWithinSizeLimit, TransactionWithLifetime, } from '@solana/transactions'; @@ -26,29 +24,29 @@ import { import { TransactionMessageWithSingleSendingSigner } from '../transaction-with-single-sending-signer'; { - // [partiallySignTransactionMessageWithSigners]: returns a transaction with a blockhash lifetime + // [partiallySignTransactionMessageWithSigners]: returns a transaction with a lifetime when the input message has a blockhash lifetime const transactionMessage = null as unknown as BaseTransactionMessage & TransactionMessageWithBlockhashLifetime & TransactionMessageWithFeePayer & TransactionMessageWithSigners; partiallySignTransactionMessageWithSigners(transactionMessage) satisfies Promise< - Readonly + Readonly >; } { - // [partiallySignTransactionMessageWithSigners]: returns a transaction with a durable nonce lifetime + // [partiallySignTransactionMessageWithSigners]: returns a transaction with a lifetime when the input message has a durable nonce lifetime const transactionMessage = null as unknown as BaseTransactionMessage & TransactionMessageWithDurableNonceLifetime & TransactionMessageWithFeePayer & TransactionMessageWithSigners; partiallySignTransactionMessageWithSigners(transactionMessage) satisfies Promise< - Readonly + Readonly >; } { - // [partiallySignTransactionMessageWithSigners]: returns a transaction with an unknown lifetime + // [partiallySignTransactionMessageWithSigners]: returns a transaction with an unknown lifetime when the input message has an unknown lifetime const transactionMessage = null as unknown as BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithLifetime & @@ -59,12 +57,11 @@ import { TransactionMessageWithSingleSendingSigner } from '../transaction-with-s } { - // [partiallySignTransactionMessageWithSigners]: returns a transaction with no lifetime constraint + // [partiallySignTransactionMessageWithSigners]: returns a transaction with a lifetime when the input message has no lifetime const transactionMessage = null as unknown as BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners; partiallySignTransactionMessageWithSigners(transactionMessage) satisfies Promise>; - // @ts-expect-error Expects no lifetime constraint partiallySignTransactionMessageWithSigners(transactionMessage) satisfies Promise< Readonly >; @@ -83,29 +80,29 @@ import { TransactionMessageWithSingleSendingSigner } from '../transaction-with-s } { - // [signTransactionMessageWithSigners]: returns a fully signed transaction with a blockhash lifetime + // [signTransactionMessageWithSigners]: returns a fully signed transaction with a lifetime when the input message has a blockhash lifetime const transactionMessage = null as unknown as BaseTransactionMessage & TransactionMessageWithBlockhashLifetime & TransactionMessageWithFeePayer & TransactionMessageWithSigners; signTransactionMessageWithSigners(transactionMessage) satisfies Promise< - Readonly + Readonly >; } { - // [signTransactionMessageWithSigners]: returns a fully signed transaction with a durable nonce lifetime + // [signTransactionMessageWithSigners]: returns a fully signed transaction with a lifetime when the input message has a durable nonce lifetime const transactionMessage = null as unknown as BaseTransactionMessage & TransactionMessageWithDurableNonceLifetime & TransactionMessageWithFeePayer & TransactionMessageWithSigners; signTransactionMessageWithSigners(transactionMessage) satisfies Promise< - Readonly + Readonly >; } { - // [signTransactionMessageWithSigners]: returns a fully signed transaction with an unknown lifetime + // [signTransactionMessageWithSigners]: returns a fully signed transaction with an unknown lifetime when the input message has an unknown lifetime const transactionMessage = null as unknown as BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithLifetime & @@ -116,12 +113,11 @@ import { TransactionMessageWithSingleSendingSigner } from '../transaction-with-s } { - // [signTransactionMessageWithSigners]: returns a transaction with no lifetime constraint + // [signTransactionMessageWithSigners]: returns a transaction with a lifetime when the input message has no lifetime const transactionMessage = null as unknown as BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners; signTransactionMessageWithSigners(transactionMessage) satisfies Promise>; - // @ts-expect-error Expects no lifetime constraint signTransactionMessageWithSigners(transactionMessage) satisfies Promise< Readonly >; diff --git a/packages/signers/src/sign-transaction.ts b/packages/signers/src/sign-transaction.ts index 1b4134254..ead89ac41 100644 --- a/packages/signers/src/sign-transaction.ts +++ b/packages/signers/src/sign-transaction.ts @@ -4,9 +4,9 @@ import { BaseTransactionMessage, TransactionMessageWithFeePayer } from '@solana/ import { assertIsFullySignedTransaction, compileTransaction, - FullySignedTransaction, + SendableTransaction, Transaction, - TransactionFromTransactionMessage, + TransactionWithinSizeLimit, TransactionWithLifetime, } from '@solana/transactions'; @@ -41,8 +41,6 @@ import { assertIsTransactionMessageWithSingleSendingSigner } from './transaction * {@link TransactionModifyingSigner} if no other signer implements that interface. * Otherwise, it will be used as a {@link TransactionPartialSigner}. * - * @typeParam TTransactionMessage - The inferred type of the transaction message provided. - * * @example * ```ts * const signedTransaction = await partiallySignTransactionMessageWithSigners(transactionMessage); @@ -64,12 +62,10 @@ import { assertIsTransactionMessageWithSingleSendingSigner } from './transaction * @see {@link signTransactionMessageWithSigners} * @see {@link signAndSendTransactionMessageWithSigners} */ -export async function partiallySignTransactionMessageWithSigners< - TTransactionMessage extends BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners, ->( - transactionMessage: TTransactionMessage, +export async function partiallySignTransactionMessageWithSigners( + transactionMessage: BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners, config?: TransactionPartialSignerConfig, -): Promise> { +): Promise { const { partialSigners, modifyingSigners } = categorizeTransactionSigners( deduplicateSigners(getSignersFromTransactionMessage(transactionMessage).filter(isTransactionSigner)), { identifySendingSigner: false }, @@ -91,8 +87,6 @@ export async function partiallySignTransactionMessageWithSigners< * This function delegates to the {@link partiallySignTransactionMessageWithSigners} function * in order to extract signers from the transaction message and sign the transaction. * - * @typeParam TTransactionMessage - The inferred type of the transaction message provided. - * * @example * ```ts * const mySignedTransaction = await signTransactionMessageWithSigners(myTransactionMessage); @@ -109,12 +103,10 @@ export async function partiallySignTransactionMessageWithSigners< * @see {@link partiallySignTransactionMessageWithSigners} * @see {@link signAndSendTransactionMessageWithSigners} */ -export async function signTransactionMessageWithSigners< - TTransactionMessage extends BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners, ->( - transactionMessage: TTransactionMessage, +export async function signTransactionMessageWithSigners( + transactionMessage: BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners, config?: TransactionPartialSignerConfig, -): Promise> { +): Promise { const signedTransaction = await partiallySignTransactionMessageWithSigners(transactionMessage, config); assertIsFullySignedTransaction(signedTransaction); return signedTransaction; @@ -126,8 +118,6 @@ export async function signTransactionMessageWithSigners< * * It returns the signature of the sent transaction (i.e. its identifier) as bytes. * - * @typeParam TTransactionMessage - The inferred type of the transaction message provided. - * * @example * ```ts * import { signAndSendTransactionMessageWithSigners } from '@solana/signers'; @@ -170,9 +160,10 @@ export async function signTransactionMessageWithSigners< * @see {@link signTransactionMessageWithSigners} * */ -export async function signAndSendTransactionMessageWithSigners< - TTransactionMessage extends BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners, ->(transaction: TTransactionMessage, config?: TransactionSendingSignerConfig): Promise { +export async function signAndSendTransactionMessageWithSigners( + transaction: BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners, + config?: TransactionSendingSignerConfig, +): Promise { assertIsTransactionMessageWithSingleSendingSigner(transaction); const abortSignal = config?.abortSignal; @@ -276,28 +267,24 @@ function identifyTransactionModifyingSigners( * Signs a transaction using the provided TransactionModifyingSigners * sequentially followed by the TransactionPartialSigners in parallel. */ -async function signModifyingAndPartialTransactionSigners< - TTransactionMessage extends BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners, ->( - transactionMessage: TTransactionMessage, +async function signModifyingAndPartialTransactionSigners( + transactionMessage: BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners, modifyingSigners: readonly TransactionModifyingSigner[] = [], partialSigners: readonly TransactionPartialSigner[] = [], config?: TransactionModifyingSignerConfig, -): Promise> { - type ReturnType = TransactionFromTransactionMessage; - +): Promise { // serialize the transaction const transaction = compileTransaction(transactionMessage); // Handle modifying signers sequentially. - const modifiedTransaction = await modifyingSigners.reduce( + const modifiedTransaction = (await modifyingSigners.reduce( async (transaction, modifyingSigner) => { config?.abortSignal?.throwIfAborted(); const [tx] = await modifyingSigner.modifyAndSignTransactions([await transaction], config); return Object.freeze(tx); }, Promise.resolve(transaction) as Promise>, - ); + )) as Transaction & TransactionWithinSizeLimit & TransactionWithLifetime; // Handle partial signers in parallel. config?.abortSignal?.throwIfAborted(); @@ -315,5 +302,5 @@ async function signModifyingAndPartialTransactionSigners< return { ...signatures, ...signatureDictionary }; }, modifiedTransaction.signatures ?? {}), ), - }) as ReturnType; + }); } diff --git a/packages/signers/src/transaction-modifying-signer.ts b/packages/signers/src/transaction-modifying-signer.ts index ebdf485ce..4a4684c24 100644 --- a/packages/signers/src/transaction-modifying-signer.ts +++ b/packages/signers/src/transaction-modifying-signer.ts @@ -1,6 +1,6 @@ import { Address } from '@solana/addresses'; import { SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_MODIFYING_SIGNER, SolanaError } from '@solana/errors'; -import { Transaction } from '@solana/transactions'; +import { Transaction, TransactionWithinSizeLimit, TransactionWithLifetime } from '@solana/transactions'; import { BaseTransactionSignerConfig } from './types'; @@ -21,7 +21,8 @@ export type TransactionModifyingSignerConfig = BaseTransactionSignerConfig; * {@link SignatureDictionary}, its * {@link TransactionModifyingSigner#modifyAndSignTransactions | modifyAndSignTransactions} function * returns an updated {@link Transaction} with a potentially modified set of instructions and - * signature dictionary. + * signature dictionary. The returned transaction must be within the transaction size limit, + * and include a `lifetimeConstraint`. * * @typeParam TAddress - Supply a string literal to define a signer having a particular address. * @@ -29,9 +30,9 @@ export type TransactionModifyingSignerConfig = BaseTransactionSignerConfig; * ```ts * const signer: TransactionModifyingSigner<'1234..5678'> = { * address: address('1234..5678'), - * modifyAndSignTransactions: async ( - * transactions: T[] - * ): Promise => { + * modifyAndSignTransactions: async ( + * transactions: Transaction[] + * ): Promise<(Transaction & TransactionWithinSizeLimit & TransactionWithLifetime)[]> => { * // My custom signing logic. * }, * }; @@ -55,10 +56,10 @@ export type TransactionModifyingSignerConfig = BaseTransactionSignerConfig; */ export type TransactionModifyingSigner = Readonly<{ address: Address; - modifyAndSignTransactions( - transactions: readonly T[], + modifyAndSignTransactions( + transactions: readonly (Transaction | (Transaction & TransactionWithLifetime))[], config?: TransactionModifyingSignerConfig, - ): Promise; + ): Promise; }>; /** diff --git a/packages/signers/src/transaction-partial-signer.ts b/packages/signers/src/transaction-partial-signer.ts index 53e5b44df..141188aa9 100644 --- a/packages/signers/src/transaction-partial-signer.ts +++ b/packages/signers/src/transaction-partial-signer.ts @@ -1,6 +1,6 @@ import { Address } from '@solana/addresses'; import { SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_PARTIAL_SIGNER, SolanaError } from '@solana/errors'; -import { Transaction, TransactionWithLifetime } from '@solana/transactions'; +import { Transaction, TransactionWithinSizeLimit, TransactionWithLifetime } from '@solana/transactions'; import { BaseTransactionSignerConfig, SignatureDictionary } from './types'; @@ -49,7 +49,7 @@ export type TransactionPartialSignerConfig = BaseTransactionSignerConfig; export type TransactionPartialSigner = Readonly<{ address: Address; signTransactions( - transactions: readonly (Transaction & TransactionWithLifetime)[], + transactions: readonly (Transaction & TransactionWithinSizeLimit & TransactionWithLifetime)[], config?: TransactionPartialSignerConfig, ): Promise; }>; diff --git a/packages/signers/src/transaction-sending-signer.ts b/packages/signers/src/transaction-sending-signer.ts index 66903340d..6b4c50708 100644 --- a/packages/signers/src/transaction-sending-signer.ts +++ b/packages/signers/src/transaction-sending-signer.ts @@ -1,7 +1,7 @@ import { Address } from '@solana/addresses'; import { SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_SENDING_SIGNER, SolanaError } from '@solana/errors'; import { SignatureBytes } from '@solana/keys'; -import { Transaction } from '@solana/transactions'; +import { Transaction, TransactionWithLifetime } from '@solana/transactions'; import { BaseTransactionSignerConfig } from './types'; @@ -61,7 +61,7 @@ export type TransactionSendingSignerConfig = BaseTransactionSignerConfig; export type TransactionSendingSigner = Readonly<{ address: Address; signAndSendTransactions( - transactions: readonly Transaction[], + transactions: readonly (Transaction | (Transaction & TransactionWithLifetime))[], config?: TransactionSendingSignerConfig, ): Promise; }>; From 95bd0fc36623d8e031d0e5a9b00d4667df7e38d4 Mon Sep 17 00:00:00 2001 From: Callum Date: Wed, 1 Oct 2025 18:58:30 +0000 Subject: [PATCH 2/2] Add changeset with breaking change --- .changeset/public-showers-remain.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/public-showers-remain.md diff --git a/.changeset/public-showers-remain.md b/.changeset/public-showers-remain.md new file mode 100644 index 000000000..68f6d4665 --- /dev/null +++ b/.changeset/public-showers-remain.md @@ -0,0 +1,12 @@ +--- +'@solana/signers': major +'@solana/react': major +--- + +Update the signer API to return Transaction & TransactionWithLifetime + +The `modifyAndSignTransactions` function for a `TransactionModifyingSigner` must now return a `Transaction & TransactionWithLifetime & TransactionWithinSizeLimit`. Previously it technically needed to return a type derived from the input `TransactionMessage`, but this wasn't checked. + +If you have written a `TransactionModifyingSigner` then you should review the changes to `useWalletAccountTransactionSigner` in the React package for guidance. You may need to use the new `getTransactionLifetimeConstraintFromCompiledTransactionMessage` function to obtain a lifetime for the transaction being returned. + +If you are using a `TransactionModifyingSigner` such as `useWalletAccountTransactionSigner`, then you will now receive a transaction with `TransactionWithLifetime` when you would previously have received a type with a lifetime matching the input transaction message. This was never guaranteed to match at runtime, but we incorrectly returned a stronger type than can be guaranteed. You may need to use the new `isTransactionWithBlockhashLifetime` or `isTransactionWithDurableNonceLifetime` functions to check the lifetime type of the returned transaction. For example, if you want to pass it to a function returned by `sendAndConfirmTransactionFactory` then you must use `isTransactionWithBlockhashLifetime` or `assertIsTransactionWithBlockhashLifetime` to check its lifetime first.