Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
Add minContextSlot as an option to `Transaction{Sending}SignerConfi…
Browse files Browse the repository at this point in the history
…g` (#2858)
  • Loading branch information
steveluscher authored Jun 26, 2024
1 parent f66e3dd commit 22a34aa
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 156 deletions.
5 changes: 5 additions & 0 deletions .changeset/wicked-cougars-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/signers': minor
---

Transaction signers' methods now take `minContextSlot` as an option. This is important for signers that simulate transactions, like wallets. They might be interested in knowing the slot at which the transaction was prepared, lest they run simulation at too early a slot.
Original file line number Diff line number Diff line change
Expand Up @@ -144,36 +144,10 @@ describe('useWalletAccountTransactionSendingSigner', () => {
await expect(signAndSendTransactions([inputTransaction])).resolves.toEqual([mockSignatureResult]);
}
});
it('calls `getOptions` with the transaction', () => {
const mockGetOptions = jest.fn();
const { result } = renderHook(() =>
useWalletAccountTransactionSendingSigner(mockUiWalletAccount, 'solana:danknet', {
getOptions: mockGetOptions,
}),
);
// eslint-disable-next-line jest/no-conditional-in-test
if (result.__type === 'error' || !result.current) {
throw result.current;
} else {
const { signAndSendTransactions } = result.current;
const inputTransaction = {
messageBytes: new Uint8Array([1, 2, 3]) as unknown as TransactionMessageBytes,
signatures: {
'11111111111111111111111111111114': new Uint8Array(64).fill(2) as SignatureBytes,
},
};
signAndSendTransactions([inputTransaction]);
// eslint-disable-next-line jest/no-conditional-expect
expect(mockGetOptions).toHaveBeenCalledWith(inputTransaction);
}
});
it('adds the options returned by `getOptions` to the call to `signTransaction`', () => {
it('calls `signAndSendTransaction` with all options except the `abortSignal`', () => {
const mockOptions = { minContextSlot: 123n };
const mockGetOptions = jest.fn().mockReturnValue(mockOptions);
const { result } = renderHook(() =>
useWalletAccountTransactionSendingSigner(mockUiWalletAccount, 'solana:danknet', {
getOptions: mockGetOptions,
}),
useWalletAccountTransactionSendingSigner(mockUiWalletAccount, 'solana:danknet'),
);
// eslint-disable-next-line jest/no-conditional-in-test
if (result.__type === 'error' || !result.current) {
Expand All @@ -186,9 +160,12 @@ describe('useWalletAccountTransactionSendingSigner', () => {
'11111111111111111111111111111114': new Uint8Array(64).fill(2) as SignatureBytes,
},
};
signAndSendTransactions([inputTransaction]);
signAndSendTransactions([inputTransaction], {
abortSignal: AbortSignal.timeout(1_000_000),
...mockOptions,
});
// eslint-disable-next-line jest/no-conditional-expect
expect(mockSignAndSendTransaction).toHaveBeenCalledWith({ options: mockOptions });
expect(mockSignAndSendTransaction).toHaveBeenCalledWith(mockOptions);
}
});
it('rejects when aborted', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,33 +144,9 @@ describe('useWalletAccountTransactionSigner', () => {
await expect(signPromise).resolves.toEqual([mockDecodedTransaction]);
}
});
it('calls `getOptions` with the transaction', () => {
const mockGetOptions = jest.fn();
const { result } = renderHook(() =>
useWalletAccountTransactionSigner(mockUiWalletAccount, 'solana:danknet', { getOptions: mockGetOptions }),
);
// eslint-disable-next-line jest/no-conditional-in-test
if (result.__type === 'error' || !result.current) {
throw result.current;
} else {
const { modifyAndSignTransactions } = result.current;
const inputTransaction = {
messageBytes: new Uint8Array([1, 2, 3]) as unknown as TransactionMessageBytes,
signatures: {
'11111111111111111111111111111114': new Uint8Array(64).fill(2) as SignatureBytes,
},
};
modifyAndSignTransactions([inputTransaction]);
// eslint-disable-next-line jest/no-conditional-expect
expect(mockGetOptions).toHaveBeenCalledWith(inputTransaction);
}
});
it('adds the options returned by `getOptions` to the call to `signTransaction`', () => {
it('calls `signTransaction` with all options except the `abortSignal`', () => {
const mockOptions = { minContextSlot: 123n };
const mockGetOptions = jest.fn().mockReturnValue(mockOptions);
const { result } = renderHook(() =>
useWalletAccountTransactionSigner(mockUiWalletAccount, 'solana:danknet', { getOptions: mockGetOptions }),
);
const { result } = renderHook(() => useWalletAccountTransactionSigner(mockUiWalletAccount, 'solana:danknet'));
// eslint-disable-next-line jest/no-conditional-in-test
if (result.__type === 'error' || !result.current) {
throw result.current;
Expand All @@ -182,9 +158,12 @@ describe('useWalletAccountTransactionSigner', () => {
'11111111111111111111111111111114': new Uint8Array(64).fill(2) as SignatureBytes,
},
};
modifyAndSignTransactions([inputTransaction]);
modifyAndSignTransactions([inputTransaction], {
abortSignal: AbortSignal.timeout(1_000_000),
...mockOptions,
});
// eslint-disable-next-line jest/no-conditional-expect
expect(mockSignTransaction).toHaveBeenCalledWith({ options: mockOptions });
expect(mockSignTransaction).toHaveBeenCalledWith(mockOptions);
}
});
it('rejects when aborted', async () => {
Expand Down
25 changes: 7 additions & 18 deletions packages/react/src/useWalletAccountTransactionSendingSigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,37 @@ import { address } from '@solana/addresses';
import { SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED, SolanaError } from '@solana/errors';
import { SignatureBytes } from '@solana/keys';
import { TransactionSendingSigner } from '@solana/signers';
import { getTransactionEncoder, Transaction } from '@solana/transactions';
import { getTransactionEncoder } from '@solana/transactions';
import { UiWalletAccount } from '@wallet-standard/ui';
import { useMemo, useRef } from 'react';

import { getAbortablePromise } from './abortable-promise';
import { OnlySolanaChains } from './chain';
import { useSignAndSendTransaction } from './useSignAndSendTransaction';

type ExtraConfig = Readonly<{
getOptions?(transaction: Transaction): Readonly<{ minContextSlot?: bigint }> | undefined;
}>;

/**
* Returns an object that conforms to the `TransactionSendingSigner` interface of `@solana/signers`.
*/
export function useWalletAccountTransactionSendingSigner<TWalletAccount extends UiWalletAccount>(
uiWalletAccount: TWalletAccount,
chain: OnlySolanaChains<TWalletAccount['chains']>,
extraConfig?: ExtraConfig,
): TransactionSendingSigner<TWalletAccount['address']>;
export function useWalletAccountTransactionSendingSigner<TWalletAccount extends UiWalletAccount>(
uiWalletAccount: TWalletAccount,
chain: `solana:${string}`,
extraConfig?: ExtraConfig,
): TransactionSendingSigner<TWalletAccount['address']>;
export function useWalletAccountTransactionSendingSigner<TWalletAccount extends UiWalletAccount>(
uiWalletAccount: TWalletAccount,
chain: `solana:${string}`,
extraConfig?: ExtraConfig,
): TransactionSendingSigner<TWalletAccount['address']> {
const encoderRef = useRef<ReturnType<typeof getTransactionEncoder>>();
const signAndSendTransaction = useSignAndSendTransaction(uiWalletAccount, chain);
const getOptions = extraConfig?.getOptions;
return useMemo(
() => ({
address: address(uiWalletAccount.address),
async signAndSendTransactions(transactions, config) {
config?.abortSignal?.throwIfAborted();
async signAndSendTransactions(transactions, config = {}) {
const { abortSignal, ...options } = config;
abortSignal?.throwIfAborted();
const transactionEncoder = (encoderRef.current ||= getTransactionEncoder());
if (transactions.length > 1) {
throw new SolanaError(SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED);
Expand All @@ -49,18 +42,14 @@ export function useWalletAccountTransactionSendingSigner<TWalletAccount extends
}
const [transaction] = transactions;
const wireTransactionBytes = transactionEncoder.encode(transaction);
const options = getOptions ? getOptions(transaction) : undefined;
const inputWithOptions = {
...(options ? { options } : null),
...options,
transaction: wireTransactionBytes as Uint8Array,
};
const { signature } = await getAbortablePromise(
signAndSendTransaction(inputWithOptions),
config?.abortSignal,
);
const { signature } = await getAbortablePromise(signAndSendTransaction(inputWithOptions), abortSignal);
return Object.freeze([signature as SignatureBytes]);
},
}),
[getOptions, signAndSendTransaction, uiWalletAccount.address],
[signAndSendTransaction, uiWalletAccount.address],
);
}
25 changes: 7 additions & 18 deletions packages/react/src/useWalletAccountTransactionSigner.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,38 @@
import { address } from '@solana/addresses';
import { SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED, SolanaError } from '@solana/errors';
import { TransactionModifyingSigner } from '@solana/signers';
import { getTransactionCodec, Transaction } from '@solana/transactions';
import { getTransactionCodec } from '@solana/transactions';
import { UiWalletAccount } from '@wallet-standard/ui';
import { useMemo, useRef } from 'react';

import { getAbortablePromise } from './abortable-promise';
import { OnlySolanaChains } from './chain';
import { useSignTransaction } from './useSignTransaction';

type ExtraConfig = Readonly<{
getOptions?(transaction: Transaction): Readonly<{ minContextSlot?: bigint }> | undefined;
}>;

/**
* Returns an object that conforms to the `TransactionModifyingSigner` interface of
* `@solana/signers`.
*/
export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalletAccount>(
uiWalletAccount: TWalletAccount,
chain: OnlySolanaChains<TWalletAccount['chains']>,
extraConfig?: ExtraConfig,
): TransactionModifyingSigner<TWalletAccount['address']>;
export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalletAccount>(
uiWalletAccount: TWalletAccount,
chain: `solana:${string}`,
extraConfig?: ExtraConfig,
): TransactionModifyingSigner<TWalletAccount['address']>;
export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalletAccount>(
uiWalletAccount: TWalletAccount,
chain: `solana:${string}`,
extraConfig?: ExtraConfig,
): TransactionModifyingSigner<TWalletAccount['address']> {
const encoderRef = useRef<ReturnType<typeof getTransactionCodec>>();
const signTransaction = useSignTransaction(uiWalletAccount, chain);
const getOptions = extraConfig?.getOptions;
return useMemo(
() => ({
address: address(uiWalletAccount.address),
async modifyAndSignTransactions(transactions, config) {
config?.abortSignal?.throwIfAborted();
async modifyAndSignTransactions(transactions, config = {}) {
const { abortSignal, ...options } = config;
abortSignal?.throwIfAborted();
const transactionCodec = (encoderRef.current ||= getTransactionCodec());
if (transactions.length > 1) {
throw new SolanaError(SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED);
Expand All @@ -49,21 +42,17 @@ export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalle
}
const [transaction] = transactions;
const wireTransactionBytes = transactionCodec.encode(transaction);
const options = getOptions ? getOptions(transaction) : undefined;
const inputWithOptions = {
...(options ? { options } : null),
...options,
transaction: wireTransactionBytes as Uint8Array,
};
const { signedTransaction } = await getAbortablePromise(
signTransaction(inputWithOptions),
config?.abortSignal,
);
const { signedTransaction } = await getAbortablePromise(signTransaction(inputWithOptions), abortSignal);
const decodedSignedTransaction = transactionCodec.decode(
signedTransaction,
) as (typeof transactions)[number];
return Object.freeze([decodedSignedTransaction]);
},
}),
[uiWalletAccount.address, getOptions, signTransaction],
[uiWalletAccount.address, signTransaction],
);
}
Loading

0 comments on commit 22a34aa

Please sign in to comment.