Skip to content

Commit 9ee37a8

Browse files
committed
Add minContextSlot as an option to Transaction{Sending}SignerConfig
1 parent f66e3dd commit 9ee37a8

9 files changed

+60
-108
lines changed

packages/react/src/__tests__/useWalletAccountTransactionSendingSigner-test.ts

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -144,36 +144,10 @@ describe('useWalletAccountTransactionSendingSigner', () => {
144144
await expect(signAndSendTransactions([inputTransaction])).resolves.toEqual([mockSignatureResult]);
145145
}
146146
});
147-
it('calls `getOptions` with the transaction', () => {
148-
const mockGetOptions = jest.fn();
149-
const { result } = renderHook(() =>
150-
useWalletAccountTransactionSendingSigner(mockUiWalletAccount, 'solana:danknet', {
151-
getOptions: mockGetOptions,
152-
}),
153-
);
154-
// eslint-disable-next-line jest/no-conditional-in-test
155-
if (result.__type === 'error' || !result.current) {
156-
throw result.current;
157-
} else {
158-
const { signAndSendTransactions } = result.current;
159-
const inputTransaction = {
160-
messageBytes: new Uint8Array([1, 2, 3]) as unknown as TransactionMessageBytes,
161-
signatures: {
162-
'11111111111111111111111111111114': new Uint8Array(64).fill(2) as SignatureBytes,
163-
},
164-
};
165-
signAndSendTransactions([inputTransaction]);
166-
// eslint-disable-next-line jest/no-conditional-expect
167-
expect(mockGetOptions).toHaveBeenCalledWith(inputTransaction);
168-
}
169-
});
170-
it('adds the options returned by `getOptions` to the call to `signTransaction`', () => {
147+
it('calls `signAndSendTransaction` with all options except the `abortSignal`', () => {
171148
const mockOptions = { minContextSlot: 123n };
172-
const mockGetOptions = jest.fn().mockReturnValue(mockOptions);
173149
const { result } = renderHook(() =>
174-
useWalletAccountTransactionSendingSigner(mockUiWalletAccount, 'solana:danknet', {
175-
getOptions: mockGetOptions,
176-
}),
150+
useWalletAccountTransactionSendingSigner(mockUiWalletAccount, 'solana:danknet'),
177151
);
178152
// eslint-disable-next-line jest/no-conditional-in-test
179153
if (result.__type === 'error' || !result.current) {
@@ -186,9 +160,9 @@ describe('useWalletAccountTransactionSendingSigner', () => {
186160
'11111111111111111111111111111114': new Uint8Array(64).fill(2) as SignatureBytes,
187161
},
188162
};
189-
signAndSendTransactions([inputTransaction]);
163+
signAndSendTransactions([inputTransaction], { abortSignal: AbortSignal.any([]), ...mockOptions });
190164
// eslint-disable-next-line jest/no-conditional-expect
191-
expect(mockSignAndSendTransaction).toHaveBeenCalledWith({ options: mockOptions });
165+
expect(mockSignAndSendTransaction).toHaveBeenCalledWith(mockOptions);
192166
}
193167
});
194168
it('rejects when aborted', async () => {

packages/react/src/__tests__/useWalletAccountTransactionSigner-test.ts

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -144,33 +144,9 @@ describe('useWalletAccountTransactionSigner', () => {
144144
await expect(signPromise).resolves.toEqual([mockDecodedTransaction]);
145145
}
146146
});
147-
it('calls `getOptions` with the transaction', () => {
148-
const mockGetOptions = jest.fn();
149-
const { result } = renderHook(() =>
150-
useWalletAccountTransactionSigner(mockUiWalletAccount, 'solana:danknet', { getOptions: mockGetOptions }),
151-
);
152-
// eslint-disable-next-line jest/no-conditional-in-test
153-
if (result.__type === 'error' || !result.current) {
154-
throw result.current;
155-
} else {
156-
const { modifyAndSignTransactions } = result.current;
157-
const inputTransaction = {
158-
messageBytes: new Uint8Array([1, 2, 3]) as unknown as TransactionMessageBytes,
159-
signatures: {
160-
'11111111111111111111111111111114': new Uint8Array(64).fill(2) as SignatureBytes,
161-
},
162-
};
163-
modifyAndSignTransactions([inputTransaction]);
164-
// eslint-disable-next-line jest/no-conditional-expect
165-
expect(mockGetOptions).toHaveBeenCalledWith(inputTransaction);
166-
}
167-
});
168-
it('adds the options returned by `getOptions` to the call to `signTransaction`', () => {
147+
it('calls `signTransaction` with all options except the `abortSignal`', () => {
169148
const mockOptions = { minContextSlot: 123n };
170-
const mockGetOptions = jest.fn().mockReturnValue(mockOptions);
171-
const { result } = renderHook(() =>
172-
useWalletAccountTransactionSigner(mockUiWalletAccount, 'solana:danknet', { getOptions: mockGetOptions }),
173-
);
149+
const { result } = renderHook(() => useWalletAccountTransactionSigner(mockUiWalletAccount, 'solana:danknet'));
174150
// eslint-disable-next-line jest/no-conditional-in-test
175151
if (result.__type === 'error' || !result.current) {
176152
throw result.current;
@@ -182,9 +158,9 @@ describe('useWalletAccountTransactionSigner', () => {
182158
'11111111111111111111111111111114': new Uint8Array(64).fill(2) as SignatureBytes,
183159
},
184160
};
185-
modifyAndSignTransactions([inputTransaction]);
161+
modifyAndSignTransactions([inputTransaction], { abortSignal: AbortSignal.any([]), ...mockOptions });
186162
// eslint-disable-next-line jest/no-conditional-expect
187-
expect(mockSignTransaction).toHaveBeenCalledWith({ options: mockOptions });
163+
expect(mockSignTransaction).toHaveBeenCalledWith(mockOptions);
188164
}
189165
});
190166
it('rejects when aborted', async () => {

packages/react/src/useWalletAccountTransactionSendingSigner.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,31 @@ import { address } from '@solana/addresses';
22
import { SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED, SolanaError } from '@solana/errors';
33
import { SignatureBytes } from '@solana/keys';
44
import { TransactionSendingSigner } from '@solana/signers';
5-
import { getTransactionEncoder, Transaction } from '@solana/transactions';
5+
import { getTransactionEncoder } from '@solana/transactions';
66
import { UiWalletAccount } from '@wallet-standard/ui';
77
import { useMemo, useRef } from 'react';
88

99
import { getAbortablePromise } from './abortable-promise';
1010
import { OnlySolanaChains } from './chain';
1111
import { useSignAndSendTransaction } from './useSignAndSendTransaction';
1212

13-
type ExtraConfig = Readonly<{
14-
getOptions?(transaction: Transaction): Readonly<{ minContextSlot?: bigint }> | undefined;
15-
}>;
16-
1713
/**
1814
* Returns an object that conforms to the `TransactionSendingSigner` interface of `@solana/signers`.
1915
*/
2016
export function useWalletAccountTransactionSendingSigner<TWalletAccount extends UiWalletAccount>(
2117
uiWalletAccount: TWalletAccount,
2218
chain: OnlySolanaChains<TWalletAccount['chains']>,
23-
extraConfig?: ExtraConfig,
2419
): TransactionSendingSigner<TWalletAccount['address']>;
2520
export function useWalletAccountTransactionSendingSigner<TWalletAccount extends UiWalletAccount>(
2621
uiWalletAccount: TWalletAccount,
2722
chain: `solana:${string}`,
28-
extraConfig?: ExtraConfig,
2923
): TransactionSendingSigner<TWalletAccount['address']>;
3024
export function useWalletAccountTransactionSendingSigner<TWalletAccount extends UiWalletAccount>(
3125
uiWalletAccount: TWalletAccount,
3226
chain: `solana:${string}`,
33-
extraConfig?: ExtraConfig,
3427
): TransactionSendingSigner<TWalletAccount['address']> {
3528
const encoderRef = useRef<ReturnType<typeof getTransactionEncoder>>();
3629
const signAndSendTransaction = useSignAndSendTransaction(uiWalletAccount, chain);
37-
const getOptions = extraConfig?.getOptions;
3830
return useMemo(
3931
() => ({
4032
address: address(uiWalletAccount.address),
@@ -49,9 +41,8 @@ export function useWalletAccountTransactionSendingSigner<TWalletAccount extends
4941
}
5042
const [transaction] = transactions;
5143
const wireTransactionBytes = transactionEncoder.encode(transaction);
52-
const options = getOptions ? getOptions(transaction) : undefined;
5344
const inputWithOptions = {
54-
...(options ? { options } : null),
45+
...(config?.minContextSlot ? { minContextSlot: config.minContextSlot } : null),
5546
transaction: wireTransactionBytes as Uint8Array,
5647
};
5748
const { signature } = await getAbortablePromise(
@@ -61,6 +52,6 @@ export function useWalletAccountTransactionSendingSigner<TWalletAccount extends
6152
return Object.freeze([signature as SignatureBytes]);
6253
},
6354
}),
64-
[getOptions, signAndSendTransaction, uiWalletAccount.address],
55+
[signAndSendTransaction, uiWalletAccount.address],
6556
);
6657
}
Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,38 @@
11
import { address } from '@solana/addresses';
22
import { SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED, SolanaError } from '@solana/errors';
33
import { TransactionModifyingSigner } from '@solana/signers';
4-
import { getTransactionCodec, Transaction } from '@solana/transactions';
4+
import { getTransactionCodec } from '@solana/transactions';
55
import { UiWalletAccount } from '@wallet-standard/ui';
66
import { useMemo, useRef } from 'react';
77

88
import { getAbortablePromise } from './abortable-promise';
99
import { OnlySolanaChains } from './chain';
1010
import { useSignTransaction } from './useSignTransaction';
1111

12-
type ExtraConfig = Readonly<{
13-
getOptions?(transaction: Transaction): Readonly<{ minContextSlot?: bigint }> | undefined;
14-
}>;
15-
1612
/**
1713
* Returns an object that conforms to the `TransactionModifyingSigner` interface of
1814
* `@solana/signers`.
1915
*/
2016
export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalletAccount>(
2117
uiWalletAccount: TWalletAccount,
2218
chain: OnlySolanaChains<TWalletAccount['chains']>,
23-
extraConfig?: ExtraConfig,
2419
): TransactionModifyingSigner<TWalletAccount['address']>;
2520
export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalletAccount>(
2621
uiWalletAccount: TWalletAccount,
2722
chain: `solana:${string}`,
28-
extraConfig?: ExtraConfig,
2923
): TransactionModifyingSigner<TWalletAccount['address']>;
3024
export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalletAccount>(
3125
uiWalletAccount: TWalletAccount,
3226
chain: `solana:${string}`,
33-
extraConfig?: ExtraConfig,
3427
): TransactionModifyingSigner<TWalletAccount['address']> {
3528
const encoderRef = useRef<ReturnType<typeof getTransactionCodec>>();
3629
const signTransaction = useSignTransaction(uiWalletAccount, chain);
37-
const getOptions = extraConfig?.getOptions;
3830
return useMemo(
3931
() => ({
4032
address: address(uiWalletAccount.address),
41-
async modifyAndSignTransactions(transactions, config) {
42-
config?.abortSignal?.throwIfAborted();
33+
async modifyAndSignTransactions(transactions, config = {}) {
34+
const { abortSignal, ...options } = config;
35+
abortSignal?.throwIfAborted();
4336
const transactionCodec = (encoderRef.current ||= getTransactionCodec());
4437
if (transactions.length > 1) {
4538
throw new SolanaError(SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED);
@@ -49,21 +42,17 @@ export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalle
4942
}
5043
const [transaction] = transactions;
5144
const wireTransactionBytes = transactionCodec.encode(transaction);
52-
const options = getOptions ? getOptions(transaction) : undefined;
5345
const inputWithOptions = {
54-
...(options ? { options } : null),
46+
...options,
5547
transaction: wireTransactionBytes as Uint8Array,
5648
};
57-
const { signedTransaction } = await getAbortablePromise(
58-
signTransaction(inputWithOptions),
59-
config?.abortSignal,
60-
);
49+
const { signedTransaction } = await getAbortablePromise(signTransaction(inputWithOptions), abortSignal);
6150
const decodedSignedTransaction = transactionCodec.decode(
6251
signedTransaction,
6352
) as (typeof transactions)[number];
6453
return Object.freeze([decodedSignedTransaction]);
6554
},
6655
}),
67-
[uiWalletAccount.address, getOptions, signTransaction],
56+
[uiWalletAccount.address, signTransaction],
6857
);
6958
}

packages/signers/src/sign-transaction.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,21 @@ import {
1717

1818
import { getSignersFromTransactionMessage, ITransactionMessageWithSigners } from './account-signer-meta';
1919
import { deduplicateSigners } from './deduplicate-signers';
20-
import { isTransactionModifyingSigner, TransactionModifyingSigner } from './transaction-modifying-signer';
21-
import { isTransactionPartialSigner, TransactionPartialSigner } from './transaction-partial-signer';
22-
import { isTransactionSendingSigner, TransactionSendingSigner } from './transaction-sending-signer';
20+
import {
21+
isTransactionModifyingSigner,
22+
TransactionModifyingSigner,
23+
TransactionModifyingSignerConfig,
24+
} from './transaction-modifying-signer';
25+
import {
26+
isTransactionPartialSigner,
27+
TransactionPartialSigner,
28+
TransactionPartialSignerConfig,
29+
} from './transaction-partial-signer';
30+
import {
31+
isTransactionSendingSigner,
32+
TransactionSendingSigner,
33+
TransactionSendingSignerConfig,
34+
} from './transaction-sending-signer';
2335
import { isTransactionSigner, TransactionSigner } from './transaction-signer';
2436
import { assertIsTransactionMessageWithSingleSendingSigner } from './transaction-with-single-sending-signer';
2537

@@ -59,7 +71,7 @@ export async function partiallySignTransactionMessageWithSigners<
5971
TTransactionMessage extends CompilableTransactionMessageWithSigners = CompilableTransactionMessageWithSigners,
6072
>(
6173
transactionMessage: TTransactionMessage,
62-
config: { abortSignal?: AbortSignal } = {},
74+
config: TransactionPartialSignerConfig = {},
6375
): Promise<Readonly<Transaction & TransactionWithLifetime>> {
6476
const { partialSigners, modifyingSigners } = categorizeTransactionSigners(
6577
deduplicateSigners(getSignersFromTransactionMessage(transactionMessage).filter(isTransactionSigner)),
@@ -70,7 +82,7 @@ export async function partiallySignTransactionMessageWithSigners<
7082
transactionMessage,
7183
modifyingSigners,
7284
partialSigners,
73-
config.abortSignal,
85+
config,
7486
);
7587
}
7688

@@ -124,7 +136,7 @@ export async function signTransactionMessageWithSigners<
124136
*/
125137
export async function signAndSendTransactionMessageWithSigners<
126138
TTransactionMessage extends CompilableTransactionMessageWithSigners = CompilableTransactionMessageWithSigners,
127-
>(transaction: TTransactionMessage, config: { abortSignal?: AbortSignal } = {}): Promise<SignatureBytes> {
139+
>(transaction: TTransactionMessage, config: TransactionSendingSignerConfig = {}): Promise<SignatureBytes> {
128140
assertIsTransactionMessageWithSingleSendingSigner(transaction);
129141

130142
const abortSignal = config.abortSignal;
@@ -137,15 +149,15 @@ export async function signAndSendTransactionMessageWithSigners<
137149
transaction,
138150
modifyingSigners,
139151
partialSigners,
140-
abortSignal,
152+
config,
141153
);
142154

143155
if (!sendingSigner) {
144156
throw new SolanaError(SOLANA_ERROR__SIGNER__TRANSACTION_SENDING_SIGNER_MISSING);
145157
}
146158

147159
abortSignal?.throwIfAborted();
148-
const [signature] = await sendingSigner.signAndSendTransactions([signedTransaction], { abortSignal });
160+
const [signature] = await sendingSigner.signAndSendTransactions([signedTransaction], config);
149161
abortSignal?.throwIfAborted();
150162

151163
return signature;
@@ -234,26 +246,26 @@ async function signModifyingAndPartialTransactionSigners<
234246
transactionMessage: TTransactionMessage,
235247
modifyingSigners: readonly TransactionModifyingSigner[] = [],
236248
partialSigners: readonly TransactionPartialSigner[] = [],
237-
abortSignal?: AbortSignal,
249+
config: TransactionModifyingSignerConfig = {},
238250
): Promise<Readonly<Transaction & TransactionWithLifetime>> {
239251
// serialize the transaction
240252
const transaction = compileTransaction(transactionMessage);
241253

242254
// Handle modifying signers sequentially.
243255
const modifiedTransaction = await modifyingSigners.reduce(
244256
async (transaction, modifyingSigner) => {
245-
abortSignal?.throwIfAborted();
246-
const [tx] = await modifyingSigner.modifyAndSignTransactions([await transaction], { abortSignal });
257+
config.abortSignal?.throwIfAborted();
258+
const [tx] = await modifyingSigner.modifyAndSignTransactions([await transaction], config);
247259
return Object.freeze(tx);
248260
},
249261
Promise.resolve(transaction) as Promise<Readonly<Transaction & TransactionWithLifetime>>,
250262
);
251263

252264
// Handle partial signers in parallel.
253-
abortSignal?.throwIfAborted();
265+
config.abortSignal?.throwIfAborted();
254266
const signatureDictionaries = await Promise.all(
255267
partialSigners.map(async partialSigner => {
256-
const [signatures] = await partialSigner.signTransactions([modifiedTransaction], { abortSignal });
268+
const [signatures] = await partialSigner.signTransactions([modifiedTransaction], config);
257269
return signatures;
258270
}),
259271
);

packages/signers/src/transaction-modifying-signer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { Address } from '@solana/addresses';
22
import { SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_MODIFYING_SIGNER, SolanaError } from '@solana/errors';
33
import { Transaction } from '@solana/transactions';
44

5-
import { BaseSignerConfig } from './types';
5+
import { BaseTransactionSignerConfig } from './types';
66

7-
export type TransactionModifyingSignerConfig = BaseSignerConfig;
7+
export type TransactionModifyingSignerConfig = BaseTransactionSignerConfig;
88

99
/** Defines a signer capable of signing transactions. */
1010
export type TransactionModifyingSigner<TAddress extends string = string> = Readonly<{

packages/signers/src/transaction-partial-signer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { Address } from '@solana/addresses';
22
import { SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_PARTIAL_SIGNER, SolanaError } from '@solana/errors';
33
import { Transaction } from '@solana/transactions';
44

5-
import { BaseSignerConfig, SignatureDictionary } from './types';
5+
import { BaseTransactionSignerConfig, SignatureDictionary } from './types';
66

7-
export type TransactionPartialSignerConfig = BaseSignerConfig;
7+
export type TransactionPartialSignerConfig = BaseTransactionSignerConfig;
88

99
/** Defines a signer capable of signing transactions. */
1010
export type TransactionPartialSigner<TAddress extends string = string> = Readonly<{

packages/signers/src/transaction-sending-signer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { SOLANA_ERROR__SIGNER__EXPECTED_TRANSACTION_SENDING_SIGNER, SolanaError
33
import { SignatureBytes } from '@solana/keys';
44
import { Transaction } from '@solana/transactions';
55

6-
import { BaseSignerConfig } from './types';
6+
import { BaseTransactionSignerConfig } from './types';
77

8-
export type TransactionSendingSignerConfig = BaseSignerConfig;
8+
export type TransactionSendingSignerConfig = BaseTransactionSignerConfig;
99

1010
/** Defines a signer capable of signing and sending transactions simultaneously. */
1111
export type TransactionSendingSigner<TAddress extends string = string> = Readonly<{

packages/signers/src/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { Address } from '@solana/addresses';
22
import { SignatureBytes } from '@solana/keys';
3+
import { Slot } from '@solana/rpc-types';
34

45
export type SignatureDictionary = Readonly<Record<Address, SignatureBytes>>;
56

6-
export type BaseSignerConfig = Readonly<{ abortSignal?: AbortSignal }>;
7+
export type BaseSignerConfig = Readonly<{
8+
abortSignal?: AbortSignal;
9+
}>;
10+
11+
export interface BaseTransactionSignerConfig extends BaseSignerConfig {
12+
// Signers that simulate transactions (eg. wallets) might be interested in knowing which slot
13+
// was current when the transaction was prepared. They can use this information to ensure that
14+
// they don't run the simulation at too early a slot.
15+
minContextSlot?: Slot;
16+
}

0 commit comments

Comments
 (0)