Skip to content

Commit 9c23a11

Browse files
committed
Add SendableTransaction type
1 parent b433163 commit 9c23a11

File tree

10 files changed

+163
-14
lines changed

10 files changed

+163
-14
lines changed

examples/transfer-lamports/src/example.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import pressAnyKeyPrompt from '@solana/example-utils/pressAnyKeyPrompt.js';
1212
import {
1313
address,
1414
appendTransactionMessageInstruction,
15+
assertIsSendableTransaction,
1516
createKeyPairSignerFromBytes,
1617
createSolanaRpc,
1718
createSolanaRpcSubscriptions,
@@ -183,6 +184,7 @@ log.warn(
183184
'connections. Use Chrome.',
184185
);
185186
try {
187+
assertIsSendableTransaction(signedTransaction);
186188
await sendAndConfirmTransaction(signedTransaction, { commitment: 'confirmed' });
187189
log.info('[success] Transfer confirmed');
188190
await pressAnyKeyPrompt('Press any key to quit');

packages/kit/src/__tests__/send-transaction-internal-test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import type { Rpc, SendTransactionApi } from '@solana/rpc';
33
import type { Commitment } from '@solana/rpc-types';
44
import {
55
Base64EncodedWireTransaction,
6-
FullySignedTransaction,
76
getBase64EncodedWireTransaction,
7+
SendableTransaction,
88
Transaction,
99
TransactionWithBlockhashLifetime,
1010
TransactionWithDurableNonceLifetime,
@@ -22,7 +22,7 @@ const FOREVER_PROMISE = new Promise(() => {
2222
});
2323

2424
describe('sendAndConfirmTransaction', () => {
25-
const MOCK_TRANSACTION = {} as FullySignedTransaction & Transaction & TransactionWithBlockhashLifetime;
25+
const MOCK_TRANSACTION = {} as SendableTransaction & Transaction & TransactionWithBlockhashLifetime;
2626
let confirmRecentTransaction: jest.Mock;
2727
let createPendingRequest: jest.Mock;
2828
let rpc: Rpc<SendTransactionApi>;
@@ -176,7 +176,7 @@ describe('sendAndConfirmTransaction', () => {
176176
});
177177

178178
describe('sendAndConfirmDurableNonceTransaction', () => {
179-
const MOCK_DURABLE_NONCE_TRANSACTION = {} as unknown as FullySignedTransaction &
179+
const MOCK_DURABLE_NONCE_TRANSACTION = {} as unknown as SendableTransaction &
180180
Transaction &
181181
TransactionWithDurableNonceLifetime;
182182
let confirmDurableNonceTransaction: jest.Mock;

packages/kit/src/send-and-confirm-durable-nonce-transaction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import {
55
createRecentSignatureConfirmationPromiseFactory,
66
waitForDurableNonceTransactionConfirmation,
77
} from '@solana/transaction-confirmation';
8-
import { FullySignedTransaction, Transaction, TransactionWithDurableNonceLifetime } from '@solana/transactions';
8+
import { SendableTransaction, Transaction, TransactionWithDurableNonceLifetime } from '@solana/transactions';
99

1010
import { sendAndConfirmDurableNonceTransaction_INTERNAL_ONLY_DO_NOT_EXPORT } from './send-transaction-internal';
1111

1212
type SendAndConfirmDurableNonceTransactionFunction = (
13-
transaction: FullySignedTransaction & Transaction & TransactionWithDurableNonceLifetime,
13+
transaction: SendableTransaction & Transaction & TransactionWithDurableNonceLifetime,
1414
config: Omit<
1515
Parameters<typeof sendAndConfirmDurableNonceTransaction_INTERNAL_ONLY_DO_NOT_EXPORT>[0],
1616
'confirmDurableNonceTransaction' | 'rpc' | 'transaction'

packages/kit/src/send-and-confirm-transaction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import {
66
TransactionWithLastValidBlockHeight,
77
waitForRecentTransactionConfirmation,
88
} from '@solana/transaction-confirmation';
9-
import { FullySignedTransaction, Transaction } from '@solana/transactions';
9+
import { SendableTransaction, Transaction } from '@solana/transactions';
1010

1111
import { sendAndConfirmTransactionWithBlockhashLifetime_INTERNAL_ONLY_DO_NOT_EXPORT } from './send-transaction-internal';
1212

1313
type SendAndConfirmTransactionWithBlockhashLifetimeFunction = (
14-
transaction: FullySignedTransaction & Transaction & TransactionWithLastValidBlockHeight,
14+
transaction: SendableTransaction & Transaction & TransactionWithLastValidBlockHeight,
1515
config: Omit<
1616
Parameters<typeof sendAndConfirmTransactionWithBlockhashLifetime_INTERNAL_ONLY_DO_NOT_EXPORT>[0],
1717
'confirmRecentTransaction' | 'rpc' | 'transaction'

packages/kit/src/send-transaction-internal.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {
77
waitForRecentTransactionConfirmation,
88
} from '@solana/transaction-confirmation';
99
import {
10-
FullySignedTransaction,
1110
getBase64EncodedWireTransaction,
11+
SendableTransaction,
1212
Transaction,
1313
TransactionWithDurableNonceLifetime,
1414
} from '@solana/transactions';
@@ -22,7 +22,7 @@ interface SendAndConfirmDurableNonceTransactionConfig
2222
'getNonceInvalidationPromise' | 'getRecentSignatureConfirmationPromise'
2323
>,
2424
) => Promise<void>;
25-
transaction: FullySignedTransaction & Transaction & TransactionWithDurableNonceLifetime;
25+
transaction: SendableTransaction & Transaction & TransactionWithDurableNonceLifetime;
2626
}
2727

2828
interface SendAndConfirmTransactionWithBlockhashLifetimeConfig
@@ -34,14 +34,14 @@ interface SendAndConfirmTransactionWithBlockhashLifetimeConfig
3434
'getBlockHeightExceedencePromise' | 'getRecentSignatureConfirmationPromise'
3535
>,
3636
) => Promise<void>;
37-
transaction: FullySignedTransaction & Transaction & TransactionWithLastValidBlockHeight;
37+
transaction: SendableTransaction & Transaction & TransactionWithLastValidBlockHeight;
3838
}
3939

4040
interface SendTransactionBaseConfig extends SendTransactionConfigWithoutEncoding {
4141
abortSignal?: AbortSignal;
4242
commitment: Commitment;
4343
rpc: Rpc<SendTransactionApi>;
44-
transaction: FullySignedTransaction & Transaction;
44+
transaction: SendableTransaction & Transaction;
4545
}
4646

4747
type SendTransactionConfigWithoutEncoding = Omit<

packages/kit/src/send-transaction-without-confirming.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { Rpc, SendTransactionApi } from '@solana/rpc';
2-
import { FullySignedTransaction, Transaction } from '@solana/transactions';
2+
import { SendableTransaction, Transaction } from '@solana/transactions';
33

44
import { sendTransaction_INTERNAL_ONLY_DO_NOT_EXPORT } from './send-transaction-internal';
55

66
type SendTransactionWithoutConfirmingFunction = (
7-
transaction: FullySignedTransaction & Transaction,
7+
transaction: SendableTransaction & Transaction,
88
config: Omit<Parameters<typeof sendTransaction_INTERNAL_ONLY_DO_NOT_EXPORT>[0], 'rpc' | 'transaction'>,
99
) => Promise<void>;
1010

packages/transactions/README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,16 @@ In order to be landed on the network, a transaction must be signed by all of the
3737

3838
#### `FullySignedTransaction`
3939

40-
This type represents a transaction that is signed by all of its required signers. Being fully signed is a prerequisite of functions designed to land transactions on the network.
40+
This type represents a transaction that is signed by all of its required signers.
41+
42+
#### `SendableTransaction`
43+
44+
This type represents a transaction that has all the required conditions to be sent to the network. Namely:
45+
46+
- It must be fully signed (ie. conform to `FullySignedTransaction`)
47+
- It must be within size limit (ie. conform to `TransactionWithSizeLimit`)
48+
49+
The `SendableTransaction` type is a prerequisite of functions designed to land transactions on the network.
4150

4251
### Functions
4352

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { assertIsSendableTransaction, isSendableTransaction, SendableTransaction } from '../sendable-transaction';
2+
import { FullySignedTransaction } from '../signatures';
3+
import { Transaction } from '../transaction';
4+
import { TransactionWithinSizeLimit } from '../transaction-size';
5+
6+
// [DESCRIBE] SendableTransaction.
7+
{
8+
// It must have all the required conditions.
9+
{
10+
null as unknown as FullySignedTransaction &
11+
Transaction &
12+
TransactionWithinSizeLimit satisfies SendableTransaction;
13+
}
14+
15+
// It does not satify Transaction alone.
16+
{
17+
// @ts-expect-error No required conditions.
18+
null as unknown as Transaction satisfies SendableTransaction;
19+
}
20+
21+
// It does not satify Transaction with missing required conditions.
22+
{
23+
// @ts-expect-error Not enough required conditions.
24+
null as unknown as FullySignedTransaction & Transaction satisfies SendableTransaction;
25+
// @ts-expect-error Not enough required conditions.
26+
null as unknown as Transaction & TransactionWithinSizeLimit satisfies SendableTransaction;
27+
}
28+
}
29+
30+
// [DESCRIBE] isSendableTransaction.
31+
{
32+
// It narrows the type to a SendableTransaction.
33+
{
34+
const transaction = null as unknown as Transaction;
35+
if (isSendableTransaction(transaction)) {
36+
transaction satisfies SendableTransaction;
37+
transaction satisfies FullySignedTransaction;
38+
transaction satisfies TransactionWithinSizeLimit;
39+
} else {
40+
// @ts-expect-error Not sendable.
41+
transaction satisfies SendableTransaction;
42+
// @ts-expect-error Not fully signed.
43+
transaction satisfies FullySignedTransaction;
44+
// @ts-expect-error Not within size limit.
45+
transaction satisfies TransactionWithinSizeLimit;
46+
}
47+
}
48+
}
49+
50+
// [DESCRIBE] assertIsSendableTransaction.
51+
{
52+
// It narrows the type to a SendableTransaction.
53+
{
54+
const transaction = null as unknown as Transaction;
55+
assertIsSendableTransaction(transaction);
56+
transaction satisfies SendableTransaction;
57+
transaction satisfies FullySignedTransaction;
58+
transaction satisfies TransactionWithinSizeLimit;
59+
}
60+
}

packages/transactions/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export * from './lifetime';
1313
export * from './compile-transaction';
1414
export * from './signatures';
1515
export * from './wire-transaction';
16+
export * from './sendable-transaction';
1617
export * from './transaction-message-size';
1718
export * from './transaction-size';
1819
export * from './transaction';
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { assertIsFullySignedTransaction, FullySignedTransaction, isFullySignedTransaction } from './signatures';
2+
import { Transaction } from './transaction';
3+
import {
4+
assertIsTransactionWithinSizeLimit,
5+
isTransactionWithinSizeLimit,
6+
TransactionWithinSizeLimit,
7+
} from './transaction-size';
8+
9+
/**
10+
* Helper type that includes all transaction types required
11+
* for the transaction to be sent to the network.
12+
*
13+
* @see {@link isSendableTransaction}
14+
* @see {@link assertIsSendableTransaction}
15+
*/
16+
export type SendableTransaction = FullySignedTransaction & TransactionWithinSizeLimit;
17+
18+
/**
19+
* Checks if a transaction has all the required
20+
* conditions to be sent to the network.
21+
*
22+
* @example
23+
* ```ts
24+
* import { isSendableTransaction } from '@solana/transactions';
25+
*
26+
* const transaction = getTransactionDecoder().decode(transactionBytes);
27+
* if (isSendableTransaction(transaction)) {
28+
* // At this point we know that the transaction can be sent to the network.
29+
* }
30+
* ```
31+
*
32+
* @see {@link assertIsSendableTransaction}
33+
*/
34+
export function isSendableTransaction<TTransaction extends Transaction>(
35+
transaction: TTransaction,
36+
): transaction is SendableTransaction & TTransaction {
37+
return isFullySignedTransaction(transaction) && isTransactionWithinSizeLimit(transaction);
38+
}
39+
40+
/**
41+
* Asserts that a given transaction has all the
42+
* required conditions to be sent to the network.
43+
*
44+
* From time to time you might acquire a {@link Transaction}
45+
* from an untrusted network API or user input and you are not sure
46+
* that it has all the required conditions to be sent to the network
47+
* — such as being fully signed and within the size limit.
48+
* This function can be used to assert that such a transaction
49+
* is in fact sendable.
50+
*
51+
* @example
52+
* ```ts
53+
* import { assertIsSendableTransaction } from '@solana/transactions';
54+
*
55+
* const transaction = getTransactionDecoder().decode(transactionBytes);
56+
* try {
57+
* // If this type assertion function doesn't throw, then Typescript will upcast `transaction`
58+
* // to `SendableTransaction`.
59+
* assertIsSendableTransaction(transaction);
60+
* // At this point we know that the transaction can be sent to the network.
61+
* await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' });
62+
* } catch(e) {
63+
* if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING)) {
64+
* setError(`Missing signatures for ${e.context.addresses.join(', ')}`);
65+
* } else if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT)) {
66+
* setError(`Transaction exceeds size limit of ${e.context.transactionSizeLimit} bytes`);
67+
* }
68+
* throw;
69+
* }
70+
* ```
71+
*/
72+
export function assertIsSendableTransaction<TTransaction extends Transaction>(
73+
transaction: TTransaction,
74+
): asserts transaction is SendableTransaction & TTransaction {
75+
assertIsFullySignedTransaction(transaction);
76+
assertIsTransactionWithinSizeLimit(transaction);
77+
}

0 commit comments

Comments
 (0)