Skip to content
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
12 changes: 12 additions & 0 deletions .changeset/public-showers-remain.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions examples/signers/src/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createLogger } from '@solana/example-utils/createLogger.js';
import {
address,
appendTransactionMessageInstruction,
assertIsTransactionWithinSizeLimit,
Blockhash,
compileTransaction,
createKeyPairSignerFromBytes,
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions examples/transfer-lamports/src/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
address,
appendTransactionMessageInstruction,
assertIsSendableTransaction,
assertIsTransactionWithBlockhashLifetime,
createKeyPairSignerFromBytes,
createSolanaRpc,
createSolanaRpcSubscriptions,
Expand Down Expand Up @@ -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');
Expand Down
9 changes: 8 additions & 1 deletion packages/react/src/useWalletAccountTransactionSigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -74,7 +77,9 @@ export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalle
throw new SolanaError(SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED);
}
if (transactions.length === 0) {
return transactions;
return transactions as readonly (Transaction &
TransactionWithinSizeLimit &
TransactionWithLifetime)[];
}
const [transaction] = transactions;
const wireTransactionBytes = transactionCodec.encode(transaction);
Expand All @@ -87,6 +92,8 @@ export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalle
signedTransaction,
) as (typeof transactions)[number];

assertIsTransactionWithinSizeLimit(decodedSignedTransaction);

const existingLifetime =
'lifetimeConstraint' in transaction
? (transaction as TransactionWithLifetime).lifetimeConstraint
Expand Down
11 changes: 8 additions & 3 deletions packages/signers/src/__tests__/keypair-signer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import '@solana/test-matchers/toBeFrozenObject';
import { address, getAddressFromPublicKey } from '@solana/addresses';
import { SOLANA_ERROR__SIGNER__EXPECTED_KEY_PAIR_SIGNER, SolanaError } from '@solana/errors';
import { generateKeyPair, SignatureBytes, signBytes } from '@solana/keys';
import { partiallySignTransaction, Transaction, TransactionWithLifetime } from '@solana/transactions';
import {
partiallySignTransaction,
Transaction,
TransactionWithinSizeLimit,
TransactionWithLifetime,
} from '@solana/transactions';

import {
assertIsKeyPairSigner,
Expand Down Expand Up @@ -145,8 +150,8 @@ describe('createSignerFromKeyPair', () => {

// 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.
Expand Down
10 changes: 5 additions & 5 deletions packages/signers/src/__tests__/noop-signer-test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
28 changes: 12 additions & 16 deletions packages/signers/src/__typetests__/sign-transaction-typetest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import {
import {
FullySignedTransaction,
Transaction,
TransactionWithBlockhashLifetime,
TransactionWithDurableNonceLifetime,
TransactionWithinSizeLimit,
TransactionWithLifetime,
} from '@solana/transactions';
Expand All @@ -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<Transaction & TransactionWithBlockhashLifetime>
Readonly<Transaction & TransactionWithLifetime>
>;
}

{
// [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<Transaction & TransactionWithDurableNonceLifetime>
Readonly<Transaction & TransactionWithLifetime>
>;
}

{
// [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 &
Expand All @@ -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<Readonly<Transaction>>;
// @ts-expect-error Expects no lifetime constraint
partiallySignTransactionMessageWithSigners(transactionMessage) satisfies Promise<
Readonly<Transaction & TransactionWithLifetime>
>;
Expand All @@ -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<FullySignedTransaction & Transaction & TransactionWithBlockhashLifetime>
Readonly<FullySignedTransaction & Transaction & TransactionWithLifetime>
>;
}

{
// [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<FullySignedTransaction & Transaction & TransactionWithDurableNonceLifetime>
Readonly<FullySignedTransaction & Transaction & TransactionWithLifetime>
>;
}

{
// [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 &
Expand All @@ -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<Readonly<Transaction>>;
// @ts-expect-error Expects no lifetime constraint
signTransactionMessageWithSigners(transactionMessage) satisfies Promise<
Readonly<Transaction & TransactionWithLifetime>
>;
Expand Down
49 changes: 18 additions & 31 deletions packages/signers/src/sign-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { BaseTransactionMessage, TransactionMessageWithFeePayer } from '@solana/
import {
assertIsFullySignedTransaction,
compileTransaction,
FullySignedTransaction,
SendableTransaction,
Transaction,
TransactionFromTransactionMessage,
TransactionWithinSizeLimit,
TransactionWithLifetime,
} from '@solana/transactions';

Expand Down Expand Up @@ -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);
Expand All @@ -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<TransactionFromTransactionMessage<TTransactionMessage>> {
): Promise<Transaction & TransactionWithinSizeLimit & TransactionWithLifetime> {
const { partialSigners, modifyingSigners } = categorizeTransactionSigners(
deduplicateSigners(getSignersFromTransactionMessage(transactionMessage).filter(isTransactionSigner)),
{ identifySendingSigner: false },
Expand All @@ -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);
Expand All @@ -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<FullySignedTransaction & TransactionFromTransactionMessage<TTransactionMessage>> {
): Promise<SendableTransaction & Transaction & TransactionWithLifetime> {
const signedTransaction = await partiallySignTransactionMessageWithSigners(transactionMessage, config);
assertIsFullySignedTransaction(signedTransaction);
return signedTransaction;
Expand All @@ -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';
Expand Down Expand Up @@ -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<SignatureBytes> {
export async function signAndSendTransactionMessageWithSigners(
transaction: BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners,
config?: TransactionSendingSignerConfig,
): Promise<SignatureBytes> {
assertIsTransactionMessageWithSingleSendingSigner(transaction);

const abortSignal = config?.abortSignal;
Expand Down Expand Up @@ -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<TransactionFromTransactionMessage<TTransactionMessage>> {
type ReturnType = TransactionFromTransactionMessage<TTransactionMessage>;

): Promise<Transaction & TransactionWithinSizeLimit & TransactionWithLifetime> {
// 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<Readonly<Transaction & TransactionWithLifetime>>,
);
)) as Transaction & TransactionWithinSizeLimit & TransactionWithLifetime;

// Handle partial signers in parallel.
config?.abortSignal?.throwIfAborted();
Expand All @@ -315,5 +302,5 @@ async function signModifyingAndPartialTransactionSigners<
return { ...signatures, ...signatureDictionary };
}, modifiedTransaction.signatures ?? {}),
),
}) as ReturnType;
});
}
Loading