Skip to content

Refactor FullySignedTransaction helpers #479

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
5 changes: 5 additions & 0 deletions .changeset/fresh-ghosts-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/transactions': minor
---

Adds `isFullySignedTransaction` helper and renames `assertTransactionIsFullySigned` to `assertIsFullySignedTransaction`. The old name was kept as an alias but marked as deprecated.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1138,7 +1138,7 @@ const signedTransaction = await signTransaction([], transactionMessage);

// Asserts the transaction is a `FullySignedTransaction`
// Throws an error if any signatures are missing!
assertTransactionIsFullySigned(signedTransaction);
assertIsFullySignedTransaction(signedTransaction);

await sendAndConfirmTransaction(signedTransaction);
```
Expand Down
4 changes: 2 additions & 2 deletions packages/errors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ import {
SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING,
isSolanaError,
} from '@solana/errors';
import { assertTransactionIsFullySigned, getSignatureFromTransaction } from '@solana/transactions';
import { assertIsFullySignedTransaction, getSignatureFromTransaction } from '@solana/transactions';

try {
const transactionSignature = getSignatureFromTransaction(tx);
assertTransactionIsFullySigned(tx);
assertIsFullySignedTransaction(tx);
/* ... */
} catch (e) {
if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/errors/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ import { getErrorMessage } from './message-formatter';
* SOLANA_ERROR__TRANSACTION__FEE_PAYER_SIGNATURE_MISSING,
* isSolanaError,
* } from '@solana/errors';
* import { assertTransactionIsFullySigned, getSignatureFromTransaction } from '@solana/transactions';
* import { assertIsFullySignedTransaction, getSignatureFromTransaction } from '@solana/transactions';
*
* try {
* const transactionSignature = getSignatureFromTransaction(tx);
* assertTransactionIsFullySigned(tx);
* assertIsFullySignedTransaction(tx);
* /* ... *\/
* } catch (e) {
* if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/signers/src/sign-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { SOLANA_ERROR__SIGNER__TRANSACTION_SENDING_SIGNER_MISSING, SolanaError }
import { SignatureBytes } from '@solana/keys';
import { CompilableTransactionMessage } from '@solana/transaction-messages';
import {
assertTransactionIsFullySigned,
assertIsFullySignedTransaction,
compileTransaction,
FullySignedTransaction,
Transaction,
Expand Down Expand Up @@ -118,7 +118,7 @@ export async function signTransactionMessageWithSigners<
config?: TransactionPartialSignerConfig,
): Promise<FullySignedTransaction & TransactionFromCompilableTransactionMessage<TTransactionMessage>> {
const signedTransaction = await partiallySignTransactionMessageWithSigners(transactionMessage, config);
assertTransactionIsFullySigned(signedTransaction);
assertIsFullySignedTransaction(signedTransaction);
return signedTransaction;
}

Expand Down
55 changes: 48 additions & 7 deletions packages/transactions/src/__tests__/signatures-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {
import { SignatureBytes, signBytes } from '@solana/keys';

import {
assertTransactionIsFullySigned,
assertIsFullySignedTransaction,
getSignatureFromTransaction,
isFullySignedTransaction,
partiallySignTransaction,
signTransaction,
} from '../signatures';
Expand Down Expand Up @@ -398,7 +399,47 @@ describe('signTransaction', () => {
});
});

describe('assertTransactionIsFullySigned', () => {
describe('isFullySignedTransaction', () => {
const mockPublicKeyAddressA = 'A' as Address;
const mockSignatureA = new Uint8Array(0) as SignatureBytes;
const mockPublicKeyAddressB = 'B' as Address;
const mockSignatureB = new Uint8Array(1) as SignatureBytes;

it('returns false if the transaction has missing signatures', () => {
const signatures: SignaturesMap = {};
signatures[mockPublicKeyAddressA] = null;
const transaction: Transaction = {
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures,
};

expect(isFullySignedTransaction(transaction)).toBe(false);
});

it('returns true if the transaction is signed by all its signers', () => {
const signatures: SignaturesMap = {};
signatures[mockPublicKeyAddressA] = mockSignatureA;
signatures[mockPublicKeyAddressB] = mockSignatureB;
const transaction: Transaction = {
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures,
};

expect(isFullySignedTransaction(transaction)).toBe(true);
});

it('return true if the transaction has no signatures', () => {
const signatures: SignaturesMap = {};
const transaction: Transaction = {
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures,
};

expect(isFullySignedTransaction(transaction)).toBe(true);
});
});

describe('assertIsFullySignedTransaction', () => {
const mockPublicKeyAddressA = 'A' as Address;
const mockSignatureA = new Uint8Array(0) as SignatureBytes;
const mockPublicKeyAddressB = 'B' as Address;
Expand All @@ -412,7 +453,7 @@ describe('assertTransactionIsFullySigned', () => {
signatures,
};

expect(() => assertTransactionIsFullySigned(transaction)).toThrow(
expect(() => assertIsFullySignedTransaction(transaction)).toThrow(
new SolanaError(SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING, {
addresses: [mockPublicKeyAddressA],
}),
Expand All @@ -428,7 +469,7 @@ describe('assertTransactionIsFullySigned', () => {
signatures,
};

expect(() => assertTransactionIsFullySigned(transaction)).toThrow(
expect(() => assertIsFullySignedTransaction(transaction)).toThrow(
new SolanaError(SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING, {
addresses: [mockPublicKeyAddressA, mockPublicKeyAddressB],
}),
Expand All @@ -443,7 +484,7 @@ describe('assertTransactionIsFullySigned', () => {
signatures,
};

expect(() => assertTransactionIsFullySigned(transaction)).not.toThrow();
expect(() => assertIsFullySignedTransaction(transaction)).not.toThrow();
});

it('does not throw if the transaction is signed by all its signers', () => {
Expand All @@ -455,7 +496,7 @@ describe('assertTransactionIsFullySigned', () => {
signatures,
};

expect(() => assertTransactionIsFullySigned(transaction)).not.toThrow();
expect(() => assertIsFullySignedTransaction(transaction)).not.toThrow();
});

it('does not throw if the transaction has no signatures', () => {
Expand All @@ -464,6 +505,6 @@ describe('assertTransactionIsFullySigned', () => {
messageBytes: new Uint8Array() as ReadonlyUint8Array as TransactionMessageBytes,
signatures,
};
expect(() => assertTransactionIsFullySigned(transaction)).not.toThrow();
expect(() => assertIsFullySignedTransaction(transaction)).not.toThrow();
});
});
17 changes: 13 additions & 4 deletions packages/transactions/src/__typetests__/signatures-typetest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import { Signature } from '@solana/keys';

import {
assertTransactionIsFullySigned,
assertIsFullySignedTransaction,
FullySignedTransaction,
getSignatureFromTransaction,
isFullySignedTransaction,
partiallySignTransaction,
signTransaction,
} from '..';
Expand All @@ -28,9 +29,17 @@ import { Transaction } from '../transaction';
signTransaction([], transaction) satisfies Promise<FullySignedTransaction & { some: 1 }>;
}

// assertTransactionIsFullySigned
// isFullySignedTransaction
{
const transaction = null as unknown as Transaction & { some: 1 };
assertTransactionIsFullySigned(transaction);
transaction satisfies FullySignedTransaction & { some: 1 };
if (isFullySignedTransaction(transaction)) {
transaction satisfies FullySignedTransaction & Transaction & { some: 1 };
}
}

// assertIsFullySignedTransaction
{
const transaction = null as unknown as Transaction & { some: 1 };
assertIsFullySignedTransaction(transaction);
transaction satisfies FullySignedTransaction & Transaction & { some: 1 };
}
29 changes: 29 additions & 0 deletions packages/transactions/src/deprecated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { assertIsFullySignedTransaction } from './signatures';

/**
* From time to time you might acquire a {@link Transaction}, that you expect to be fully signed,
* from an untrusted network API or user input. Use this function to assert that such a transaction
* is fully signed.
*
* @deprecated Use {@link assertIsFullySignedTransaction} instead. It was only renamed.
*
* @example
* ```ts
* import { assertTransactionIsFullySigned } from '@solana/transactions';
*
* const transaction = getTransactionDecoder().decode(transactionBytes);
* try {
* // If this type assertion function doesn't throw, then Typescript will upcast `transaction`
* // to `FullySignedTransaction`.
* assertTransactionIsFullySigned(transaction);
* // At this point we know that the transaction is signed and can be sent to the network.
* await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' });
* } catch(e) {
* if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING)) {
* setError(`Missing signatures for ${e.context.addresses.join(', ')}`);
* }
* throw;
* }
* ```
*/
export const assertTransactionIsFullySigned = assertIsFullySignedTransaction;
3 changes: 3 additions & 0 deletions packages/transactions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ export * from './wire-transaction';
export * from './transaction-message-size';
export * from './transaction-size';
export * from './transaction';

// Remove in the next major version.
export * from './deprecated';
32 changes: 26 additions & 6 deletions packages/transactions/src/signatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,25 +151,44 @@ export async function signTransaction<T extends Transaction>(
transaction: T,
): Promise<FullySignedTransaction & T> {
const out = await partiallySignTransaction(keyPairs, transaction);
assertTransactionIsFullySigned(out);
assertIsFullySignedTransaction(out);
Object.freeze(out);
return out;
}

/**
* Checks whether a given {@link Transaction} is fully signed.
*
* @example
* ```ts
* import { isFullySignedTransaction } from '@solana/transactions';
*
* const transaction = getTransactionDecoder().decode(transactionBytes);
* if (isFullySignedTransaction(transaction)) {
* // At this point we know that the transaction is signed and can be sent to the network.
* }
* ```
*/
export function isFullySignedTransaction<TTransaction extends Transaction>(
transaction: TTransaction,
): transaction is FullySignedTransaction & TTransaction {
return Object.entries(transaction.signatures).every(([_, signatureBytes]) => !!signatureBytes);
}

/**
* From time to time you might acquire a {@link Transaction}, that you expect to be fully signed,
* from an untrusted network API or user input. Use this function to assert that such a transaction
* is fully signed.
*
* @example
* ```ts
* import { assertTransactionIsFullySigned } from '@solana/transactions';
* import { assertIsFullySignedTransaction } from '@solana/transactions';
*
* const transaction = getTransactionDecoder().decode(transactionBytes);
* try {
* // If this type assertion function doesn't throw, then Typescript will upcast `transaction`
* // to `FullySignedTransaction`.
* assertTransactionIsFullySigned(transaction);
* assertIsFullySignedTransaction(transaction);
* // At this point we know that the transaction is signed and can be sent to the network.
* await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' });
* } catch(e) {
Expand All @@ -178,10 +197,11 @@ export async function signTransaction<T extends Transaction>(
* }
* throw;
* }
* ```
*/
export function assertTransactionIsFullySigned(
transaction: Transaction,
): asserts transaction is FullySignedTransaction {
export function assertIsFullySignedTransaction<TTransaction extends Transaction>(
transaction: TTransaction,
): asserts transaction is FullySignedTransaction & TTransaction {
const missingSigs: Address[] = [];
Object.entries(transaction.signatures).forEach(([address, signatureBytes]) => {
if (!signatureBytes) {
Expand Down