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
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