@solana/react 6.3.1 → 6.3.2-canary-20260313112147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solana/react",
3
- "version": "6.3.1",
3
+ "version": "6.3.2-canary-20260313112147",
4
4
  "description": "React hooks for building Solana apps",
5
5
  "homepage": "https://www.solanakit.com/api#solanareact",
6
6
  "exports": {
@@ -33,7 +33,8 @@
33
33
  "types": "./dist/types/index.d.ts",
34
34
  "type": "commonjs",
35
35
  "files": [
36
- "./dist/"
36
+ "./dist/",
37
+ "./src/"
37
38
  ],
38
39
  "sideEffects": false,
39
40
  "keywords": [
@@ -61,13 +62,13 @@
61
62
  "@wallet-standard/react": "^1.0.1",
62
63
  "@wallet-standard/ui": "^1.0.1",
63
64
  "@wallet-standard/ui-registry": "^1.0.1",
64
- "@solana/addresses": "6.3.1",
65
- "@solana/promises": "6.3.1",
66
- "@solana/signers": "6.3.1",
67
- "@solana/transaction-messages": "6.3.1",
68
- "@solana/errors": "6.3.1",
69
- "@solana/keys": "6.3.1",
70
- "@solana/transactions": "6.3.1"
65
+ "@solana/errors": "6.3.2-canary-20260313112147",
66
+ "@solana/keys": "6.3.2-canary-20260313112147",
67
+ "@solana/addresses": "6.3.2-canary-20260313112147",
68
+ "@solana/signers": "6.3.2-canary-20260313112147",
69
+ "@solana/promises": "6.3.2-canary-20260313112147",
70
+ "@solana/transaction-messages": "6.3.2-canary-20260313112147",
71
+ "@solana/transactions": "6.3.2-canary-20260313112147"
71
72
  },
72
73
  "peerDependencies": {
73
74
  "react": ">=18"
@@ -0,0 +1,147 @@
1
+ import type { UiWallet, UiWalletAccount } from '@wallet-standard/react';
2
+ import {
3
+ getUiWalletAccountStorageKey,
4
+ uiWalletAccountBelongsToUiWallet,
5
+ uiWalletAccountsAreSame,
6
+ useWallets,
7
+ } from '@wallet-standard/react';
8
+ import React from 'react';
9
+
10
+ import { SelectedWalletAccountContext, SelectedWalletAccountState } from './selectedWalletAccountContext';
11
+
12
+ export type SelectedWalletAccountContextProviderProps = {
13
+ filterWallets: (wallet: UiWallet) => boolean;
14
+ stateSync: {
15
+ deleteSelectedWallet: () => void;
16
+ getSelectedWallet: () => string | null;
17
+ storeSelectedWallet: (accountKey: string) => void;
18
+ };
19
+ } & { children: React.ReactNode };
20
+
21
+ /**
22
+ * Returns the saved wallet account when its corresponding wallet, and account is available.
23
+ * @param wallets All wallets available to select in the app
24
+ * @param savedWalletKey The saved wallet account storage key
25
+ * @returns The saved wallet account, or undefined if not found
26
+ */
27
+ function findSavedWalletAccount(
28
+ wallets: readonly UiWallet[],
29
+ savedWalletKey: string | null,
30
+ ): UiWalletAccount | undefined {
31
+ if (!savedWalletKey) {
32
+ return;
33
+ }
34
+ const [savedWalletName, savedWalletAddress] = savedWalletKey.split(':');
35
+ if (!savedWalletName || !savedWalletAddress) {
36
+ return;
37
+ }
38
+ for (const wallet of wallets) {
39
+ if (wallet.name !== savedWalletName) continue;
40
+ for (const account of wallet.accounts) {
41
+ if (account.address === savedWalletAddress) {
42
+ return account;
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Saves the selected wallet account's storage key to a persistant storage. In future
50
+ * sessions it will try to return that same wallet account, or at least one from the same brand of
51
+ * wallet if the wallet from which it came is still in the Wallet Standard registry.
52
+ * @param children The child components that will have access to the selected wallet account context
53
+ * @param filterWallets A function to filter which wallets are available in the app
54
+ * @param stateSync An object with methods to synchronize the selected wallet account state with persistent storage
55
+ * @returns A React component that provides the selected wallet account context to its children
56
+ */
57
+ export function SelectedWalletAccountContextProvider({
58
+ children,
59
+ filterWallets,
60
+ stateSync,
61
+ }: SelectedWalletAccountContextProviderProps) {
62
+ const wallets = useWallets();
63
+ const filteredWallets = React.useMemo(() => wallets.filter(filterWallets), [wallets, filterWallets]);
64
+ const wasSetterInvokedRef = React.useRef(false);
65
+
66
+ const [selectedWalletAccount, setSelectedWalletAccountInternal] = React.useState<SelectedWalletAccountState>(() => {
67
+ const savedWalletKey = stateSync.getSelectedWallet();
68
+ const savedWalletAccount = findSavedWalletAccount(filteredWallets, savedWalletKey);
69
+ return savedWalletAccount;
70
+ });
71
+
72
+ // Public setter: mark the per-instance ref synchronously to avoid races, then schedule state update.
73
+ // useCallback stabilises the setter for consumers.
74
+ const setSelectedWalletAccount: React.Dispatch<React.SetStateAction<SelectedWalletAccountState>> =
75
+ React.useCallback(
76
+ setStateAction => {
77
+ wasSetterInvokedRef.current = true;
78
+ setSelectedWalletAccountInternal(prevSelectedWalletAccount => {
79
+ const nextWalletAccount =
80
+ typeof setStateAction === 'function'
81
+ ? setStateAction(prevSelectedWalletAccount)
82
+ : setStateAction;
83
+ return nextWalletAccount;
84
+ });
85
+ },
86
+ [setSelectedWalletAccountInternal],
87
+ );
88
+
89
+ //Sync to persistant storage when selectedWalletAccount changes
90
+ React.useEffect(() => {
91
+ if (!wasSetterInvokedRef.current) return;
92
+
93
+ const accountKey = selectedWalletAccount ? getUiWalletAccountStorageKey(selectedWalletAccount) : undefined;
94
+
95
+ if (accountKey) {
96
+ stateSync.storeSelectedWallet(accountKey);
97
+ } else {
98
+ stateSync.deleteSelectedWallet();
99
+ }
100
+ }, [selectedWalletAccount, stateSync]);
101
+
102
+ //Auto-restore saved wallet account if it appears later,
103
+ //and if the user hasn't made an explicit choice yet.
104
+ React.useEffect(() => {
105
+ if (wasSetterInvokedRef.current) return;
106
+ const savedWalletKey = stateSync.getSelectedWallet();
107
+ const savedAccount = findSavedWalletAccount(filteredWallets, savedWalletKey);
108
+ if (savedAccount && selectedWalletAccount && uiWalletAccountsAreSame(savedAccount, selectedWalletAccount)) {
109
+ return;
110
+ }
111
+ if (savedAccount) {
112
+ setSelectedWalletAccountInternal(savedAccount);
113
+ }
114
+ }, [filteredWallets, stateSync, selectedWalletAccount]);
115
+
116
+ const walletAccount = React.useMemo(() => {
117
+ if (!selectedWalletAccount) return;
118
+ for (const wallet of filteredWallets) {
119
+ for (const account of wallet.accounts) {
120
+ if (uiWalletAccountsAreSame(account, selectedWalletAccount)) {
121
+ return account;
122
+ }
123
+ }
124
+ if (uiWalletAccountBelongsToUiWallet(selectedWalletAccount, wallet) && wallet.accounts[0]) {
125
+ return wallet.accounts[0];
126
+ }
127
+ }
128
+ }, [selectedWalletAccount, filteredWallets]);
129
+
130
+ React.useEffect(() => {
131
+ // If there is a selected wallet account but the wallet to which it belongs has since
132
+ // disconnected, clear the selected wallet. This is an automatic cleanup and should not
133
+ // mark the 'wasSetterInvoked' ref (so we use the internal setter).
134
+ // Cleanup shouldn't be run if user has made a selection or selectedWalletAccount/walletAccount are loading or undefined
135
+ if (!selectedWalletAccount) return; //still loading ...
136
+ if (wasSetterInvokedRef.current) return; //user made a selection
137
+ if (!walletAccount) {
138
+ setSelectedWalletAccountInternal(undefined);
139
+ }
140
+ }, [selectedWalletAccount, walletAccount]);
141
+
142
+ return (
143
+ <SelectedWalletAccountContext.Provider value={[walletAccount, setSelectedWalletAccount, filteredWallets]}>
144
+ {children}
145
+ </SelectedWalletAccountContext.Provider>
146
+ );
147
+ }
package/src/chain.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { IdentifierArray } from '@wallet-standard/base';
2
+
3
+ type AssertSolanaChain<T> = T extends `solana:${string}` ? T : never;
4
+
5
+ export type OnlySolanaChains<T extends IdentifierArray> = T extends IdentifierArray
6
+ ? AssertSolanaChain<T[number]>
7
+ : never;
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * This package contains React hooks for building Solana apps.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+ export * from './useSignAndSendTransaction';
7
+ export * from './useSignIn';
8
+ export * from './useSignMessage';
9
+ export * from './useSignTransaction';
10
+ export * from './useWalletAccountMessageSigner';
11
+ export * from './useWalletAccountTransactionSigner';
12
+ export * from './useWalletAccountTransactionSendingSigner';
13
+ export * from './SelectedWalletAccountContextProvider';
14
+ export * from './selectedWalletAccountContext';
@@ -0,0 +1,21 @@
1
+ import { UiWallet, UiWalletAccount } from '@wallet-standard/react';
2
+ import React, { createContext } from 'react';
3
+
4
+ export type SelectedWalletAccountState = UiWalletAccount | undefined;
5
+
6
+ export type SelectedWalletAccountContextValue = readonly [
7
+ selectedWalletAccount: SelectedWalletAccountState,
8
+ setSelectedWalletAccount: React.Dispatch<React.SetStateAction<SelectedWalletAccountState>>,
9
+ filteredWallets: UiWallet[],
10
+ ];
11
+
12
+ export const SelectedWalletAccountContext = /*#__PURE__*/ createContext<SelectedWalletAccountContextValue>([
13
+ undefined,
14
+ () => {},
15
+ [],
16
+ ]);
17
+
18
+ export function useSelectedWalletAccount() {
19
+ const ctx = React.useContext(SelectedWalletAccountContext);
20
+ return ctx;
21
+ }
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { ErrorBoundary } from 'react-error-boundary';
3
+ import { act, create, ReactTestRenderer } from 'react-test-renderer';
4
+
5
+ type Result<T> =
6
+ | {
7
+ __type: 'error';
8
+ current: Error;
9
+ reset: () => void;
10
+ }
11
+ | {
12
+ __type: 'result';
13
+ current?: T;
14
+ };
15
+
16
+ type TestComponentProps<THookReturn> = {
17
+ executor(): THookReturn;
18
+ resultRef: Result<THookReturn>;
19
+ };
20
+
21
+ function TestComponentHookRenderer<THookFn extends (...args: never) => unknown>({
22
+ executor,
23
+ resultRef,
24
+ }: TestComponentProps<ReturnType<THookFn>>) {
25
+ resultRef.current = executor();
26
+ return null;
27
+ }
28
+
29
+ function TestComponent<THookFn extends (...args: never) => unknown>({
30
+ executor,
31
+ resultRef,
32
+ }: TestComponentProps<ReturnType<THookFn>>) {
33
+ return (
34
+ <ErrorBoundary
35
+ fallbackRender={({ error, resetErrorBoundary }) => {
36
+ resultRef.__type = 'error';
37
+ resultRef.current = error;
38
+ (resultRef as Extract<typeof resultRef, { __type: 'error' }>).reset = resetErrorBoundary;
39
+ return null;
40
+ }}
41
+ onReset={() => {
42
+ resultRef.__type = 'result';
43
+ delete resultRef.current;
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ delete (resultRef as any).reset;
46
+ }}
47
+ >
48
+ <TestComponentHookRenderer executor={executor} resultRef={resultRef} />
49
+ </ErrorBoundary>
50
+ );
51
+ }
52
+
53
+ export function renderHook<THookReturn>(executor: () => THookReturn): {
54
+ rerenderHook(nextExecutor: () => THookReturn): void;
55
+ result: Readonly<Result<THookReturn>>;
56
+ } {
57
+ const result = { __type: 'result' } as Result<THookReturn>;
58
+ let testRenderer: ReactTestRenderer;
59
+ void act(() => {
60
+ testRenderer = create(<TestComponent executor={executor} resultRef={result} />);
61
+ });
62
+ return {
63
+ rerenderHook(nextExecutor) {
64
+ void act(() => {
65
+ testRenderer.update(<TestComponent executor={nextExecutor} resultRef={result} />);
66
+ });
67
+ },
68
+ result,
69
+ };
70
+ }
@@ -0,0 +1,168 @@
1
+ import {
2
+ SolanaSignAndSendTransaction,
3
+ SolanaSignAndSendTransactionFeature,
4
+ SolanaSignAndSendTransactionInput,
5
+ SolanaSignAndSendTransactionOutput,
6
+ } from '@solana/wallet-standard-features';
7
+ import {
8
+ WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED,
9
+ WalletStandardError,
10
+ } from '@wallet-standard/errors';
11
+ import { getWalletAccountFeature, type UiWalletAccount } from '@wallet-standard/ui';
12
+ import { getWalletAccountForUiWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '@wallet-standard/ui-registry';
13
+ import { useCallback } from 'react';
14
+
15
+ import { OnlySolanaChains } from './chain';
16
+
17
+ type Input = Readonly<
18
+ Omit<SolanaSignAndSendTransactionInput, 'account' | 'chain' | 'options'> & {
19
+ options?: Readonly<{
20
+ minContextSlot?: bigint;
21
+ }>;
22
+ }
23
+ >;
24
+ type Output = SolanaSignAndSendTransactionOutput;
25
+
26
+ /**
27
+ * Use this to get a function capable of signing a serialized transaction with the private key of a
28
+ * {@link UiWalletAccount} and sending it to the network for processing.
29
+ *
30
+ * @param chain The identifier of the chain the transaction is destined for. Wallets may use this to
31
+ * simulate the transaction for the user.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * import { getBase58Decoder } from '@solana/codecs-strings';
36
+ * import { useSignAndSendTransaction } from '@solana/react';
37
+ *
38
+ * function SignAndSendTransactionButton({ account, transactionBytes }) {
39
+ * const signAndSendTransaction = useSignAndSendTransaction(account, 'solana:devnet');
40
+ * return (
41
+ * <button
42
+ * onClick={async () => {
43
+ * try {
44
+ * const { signature } = await signAndSendTransaction({
45
+ * transaction: transactionBytes,
46
+ * });
47
+ * const base58TransactionSignature = getBase58Decoder().decode(signature);
48
+ * window.alert(
49
+ * `View transaction: https://explorer.solana.com/tx/${base58TransactionSignature}?cluster=devnet`,
50
+ * );
51
+ * } catch (e) {
52
+ * console.error('Failed to send transaction', e);
53
+ * }
54
+ * }}
55
+ * >
56
+ * Sign and Send Transaction
57
+ * </button>
58
+ * );
59
+ * }
60
+ * ```
61
+ */
62
+ export function useSignAndSendTransaction<TWalletAccount extends UiWalletAccount>(
63
+ uiWalletAccount: TWalletAccount,
64
+ chain: OnlySolanaChains<TWalletAccount['chains']>,
65
+ ): (input: Input) => Promise<Output>;
66
+ export function useSignAndSendTransaction<TWalletAccount extends UiWalletAccount>(
67
+ uiWalletAccount: TWalletAccount,
68
+ chain: `solana:${string}`,
69
+ ): (input: Input) => Promise<Output>;
70
+ export function useSignAndSendTransaction<TWalletAccount extends UiWalletAccount>(
71
+ uiWalletAccount: TWalletAccount,
72
+ chain: `solana:${string}`,
73
+ ): (input: Input) => Promise<Output> {
74
+ const signAndSendTransactions = useSignAndSendTransactions(uiWalletAccount, chain);
75
+ return useCallback(
76
+ async input => {
77
+ const [result] = await signAndSendTransactions(input);
78
+ return result;
79
+ },
80
+ [signAndSendTransactions],
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Use this to get a function capable of signing one or more serialized transactions with the private
86
+ * key of a {@link UiWalletAccount} and sending them to the network for processing. This supports
87
+ * wallets that allow batching multiple transactions in a single request.
88
+ *
89
+ * @example
90
+ * ```tsx
91
+ * import { useSignAndSendTransactions } from '@solana/react';
92
+ *
93
+ * function SignAndSendTransactionsButton({ account, transactionBytes1, transactionBytes2 }) {
94
+ * const signAndSendTransactions = useSignAndSendTransactions(account, 'solana:devnet');
95
+ * return (
96
+ * <button
97
+ * onClick={async () => {
98
+ * try {
99
+ * const [first, second] = await signAndSendTransactions(
100
+ * { transaction: transactionBytes1 },
101
+ * { transaction: transactionBytes2 },
102
+ * );
103
+ * window.alert(
104
+ * `Transaction signatures: ${first.signature.toString()}, ${second.signature.toString()}`,
105
+ * );
106
+ * } catch (e) {
107
+ * console.error('Failed to send transactions', e);
108
+ * }
109
+ * }}
110
+ * >
111
+ * Sign and Send Transactions
112
+ * </button>
113
+ * );
114
+ * }
115
+ * ```
116
+ */
117
+ export function useSignAndSendTransactions<TWalletAccount extends UiWalletAccount>(
118
+ uiWalletAccount: TWalletAccount,
119
+ chain: OnlySolanaChains<TWalletAccount['chains']>,
120
+ ): (...inputs: readonly Input[]) => Promise<readonly Output[]>;
121
+ export function useSignAndSendTransactions<TWalletAccount extends UiWalletAccount>(
122
+ uiWalletAccount: TWalletAccount,
123
+ chain: `solana:${string}`,
124
+ ): (...inputs: readonly Input[]) => Promise<readonly Output[]>;
125
+ export function useSignAndSendTransactions<TWalletAccount extends UiWalletAccount>(
126
+ uiWalletAccount: TWalletAccount,
127
+ chain: `solana:${string}`,
128
+ ): (...inputs: readonly Input[]) => Promise<readonly Output[]> {
129
+ if (!uiWalletAccount.chains.includes(chain)) {
130
+ throw new WalletStandardError(WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED, {
131
+ address: uiWalletAccount.address,
132
+ chain,
133
+ featureName: SolanaSignAndSendTransaction,
134
+ supportedChains: [...uiWalletAccount.chains],
135
+ supportedFeatures: [...uiWalletAccount.features],
136
+ });
137
+ }
138
+ const signAndSendTransactionFeature = getWalletAccountFeature(
139
+ uiWalletAccount,
140
+ SolanaSignAndSendTransaction,
141
+ ) as SolanaSignAndSendTransactionFeature[typeof SolanaSignAndSendTransaction];
142
+ const account = getWalletAccountForUiWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWalletAccount);
143
+ return useCallback(
144
+ async (...inputs) => {
145
+ if (inputs.length === 0) {
146
+ return [];
147
+ }
148
+ const inputsWithChainAndAccount = inputs.map(({ options, ...rest }) => {
149
+ const minContextSlot = options?.minContextSlot;
150
+ return {
151
+ ...rest,
152
+ account,
153
+ chain,
154
+ ...(minContextSlot != null
155
+ ? {
156
+ options: {
157
+ minContextSlot: Number(minContextSlot),
158
+ },
159
+ }
160
+ : null),
161
+ };
162
+ });
163
+ const results = await signAndSendTransactionFeature.signAndSendTransaction(...inputsWithChainAndAccount);
164
+ return results;
165
+ },
166
+ [account, chain, signAndSendTransactionFeature],
167
+ );
168
+ }
@@ -0,0 +1,124 @@
1
+ import { Address } from '@solana/addresses';
2
+ import {
3
+ SolanaSignIn,
4
+ SolanaSignInFeature,
5
+ SolanaSignInInput,
6
+ SolanaSignInOutput,
7
+ } from '@solana/wallet-standard-features';
8
+ import {
9
+ getWalletAccountFeature,
10
+ getWalletFeature,
11
+ UiWallet,
12
+ UiWalletAccount,
13
+ UiWalletHandle,
14
+ } from '@wallet-standard/ui';
15
+ import {
16
+ getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
17
+ getWalletAccountForUiWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
18
+ getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
19
+ } from '@wallet-standard/ui-registry';
20
+ import { useCallback } from 'react';
21
+
22
+ type Input = SolanaSignInInput;
23
+ type Output = Omit<SolanaSignInOutput, 'account' | 'signatureType'> &
24
+ Readonly<{
25
+ account: UiWalletAccount;
26
+ }>;
27
+
28
+ /**
29
+ * Use the ['Sign In With Solana'](https://phantom.app/learn/developers/sign-in-with-solana) feature
30
+ * of a {@link UiWallet} or {@link UiWalletAccount}.
31
+ *
32
+ * @returns A function that you can call to sign in with the particular wallet and address specified
33
+ * by the supplied {@link UiWalletAccount}
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * import { useSignIn } from '@solana/react';
38
+ *
39
+ * function SignInButton({ wallet }) {
40
+ * const csrfToken = useCsrfToken();
41
+ * const signIn = useSignIn(wallet);
42
+ * return (
43
+ * <button
44
+ * onClick={async () => {
45
+ * try {
46
+ * const { account, signedMessage, signature } = await signIn({
47
+ * requestId: csrfToken,
48
+ * });
49
+ * // Authenticate the user, typically on the server, by verifying that
50
+ * // `signedMessage` was signed by the person who holds the private key for
51
+ * // `account.publicKey`.
52
+ * //
53
+ * // Authorize the user, also on the server, by decoding `signedMessage` as the
54
+ * // text of a Sign In With Solana message, verifying that it was not modified
55
+ * // from the values your application expects, and that its content is sufficient
56
+ * // to grant them access.
57
+ * window.alert(`You are now signed in with the address ${account.address}`);
58
+ * } catch (e) {
59
+ * console.error('Failed to sign in', e);
60
+ * }
61
+ * }}
62
+ * >
63
+ * Sign In
64
+ * </button>
65
+ * );
66
+ * }
67
+ * ```
68
+ */
69
+ export function useSignIn(uiWalletAccount: UiWalletAccount): (input?: Omit<Input, 'address'>) => Promise<Output>;
70
+ /**
71
+ * @returns A function that you can call to sign in with the supplied {@link UiWallet}
72
+ */
73
+ export function useSignIn(uiWallet: UiWallet): (input?: Input) => Promise<Output>;
74
+ export function useSignIn(uiWalletHandle: UiWalletHandle): (input?: Input) => Promise<Output> {
75
+ const signIns = useSignIns(uiWalletHandle);
76
+ return useCallback(
77
+ async input => {
78
+ const [result] = await signIns(input);
79
+ return result;
80
+ },
81
+ [signIns],
82
+ );
83
+ }
84
+
85
+ function useSignIns(
86
+ uiWalletHandle: UiWalletHandle,
87
+ ): (...inputs: readonly (Input | undefined)[]) => Promise<readonly Output[]> {
88
+ let signMessageFeature: SolanaSignInFeature[typeof SolanaSignIn];
89
+ if ('address' in uiWalletHandle && typeof uiWalletHandle.address === 'string') {
90
+ getWalletAccountForUiWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWalletHandle as UiWalletAccount);
91
+ signMessageFeature = getWalletAccountFeature(
92
+ uiWalletHandle as UiWalletAccount,
93
+ SolanaSignIn,
94
+ ) as SolanaSignInFeature[typeof SolanaSignIn];
95
+ } else {
96
+ signMessageFeature = getWalletFeature(uiWalletHandle, SolanaSignIn) as SolanaSignInFeature[typeof SolanaSignIn];
97
+ }
98
+ const wallet = getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWalletHandle);
99
+ return useCallback(
100
+ async (...inputs) => {
101
+ const inputsWithAddressAndChainId = inputs.map(input => ({
102
+ ...input,
103
+ // Prioritize the `UiWalletAccount` address if it exists.
104
+ ...('address' in uiWalletHandle ? { address: uiWalletHandle.address as Address } : null),
105
+ }));
106
+ const results = await signMessageFeature.signIn(...inputsWithAddressAndChainId);
107
+ const resultsWithoutSignatureType = results.map(
108
+ ({
109
+ account,
110
+ signatureType: _, // Solana signatures are always of type `ed25519` so drop this property.
111
+ ...rest
112
+ }) => ({
113
+ ...rest,
114
+ account: getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(
115
+ wallet,
116
+ account,
117
+ ),
118
+ }),
119
+ );
120
+ return resultsWithoutSignatureType;
121
+ },
122
+ [signMessageFeature, uiWalletHandle, wallet],
123
+ );
124
+ }
@@ -0,0 +1,78 @@
1
+ import {
2
+ SolanaSignMessage,
3
+ SolanaSignMessageFeature,
4
+ SolanaSignMessageInput,
5
+ SolanaSignMessageOutput,
6
+ } from '@solana/wallet-standard-features';
7
+ import { getWalletAccountFeature, UiWalletAccount } from '@wallet-standard/ui';
8
+ import { getWalletAccountForUiWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '@wallet-standard/ui-registry';
9
+ import { useCallback } from 'react';
10
+
11
+ type Input = Omit<SolanaSignMessageInput, 'account'>;
12
+ type Output = Omit<SolanaSignMessageOutput, 'signatureType'>;
13
+
14
+ /**
15
+ * Use this to get a function capable of signing a message with the private key of a
16
+ * {@link UiWalletAccount}
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * import { useSignMessage } from '@solana/react';
21
+ *
22
+ * function SignMessageButton({ account, messageBytes }) {
23
+ * const signMessage = useSignMessage(account);
24
+ * return (
25
+ * <button
26
+ * onClick={async () => {
27
+ * try {
28
+ * const { signature } = await signMessage({
29
+ * message: messageBytes,
30
+ * });
31
+ * window.alert(`Signature bytes: ${signature.toString()}`);
32
+ * } catch (e) {
33
+ * console.error('Failed to sign message', e);
34
+ * }
35
+ * }}
36
+ * >
37
+ * Sign Message
38
+ * </button>
39
+ * );
40
+ * }
41
+ * ```
42
+ */
43
+ export function useSignMessage<TWalletAccount extends UiWalletAccount>(
44
+ ...config: Parameters<typeof useSignMessages<TWalletAccount>>
45
+ ): (input: Input) => Promise<Output> {
46
+ const signMessages = useSignMessages(...config);
47
+ return useCallback(
48
+ async input => {
49
+ const [result] = await signMessages(input);
50
+ return result;
51
+ },
52
+ [signMessages],
53
+ );
54
+ }
55
+
56
+ function useSignMessages<TWalletAccount extends UiWalletAccount>(
57
+ uiWalletAccount: TWalletAccount,
58
+ ): (...inputs: readonly Input[]) => Promise<readonly Output[]> {
59
+ const signMessageFeature = getWalletAccountFeature(
60
+ uiWalletAccount,
61
+ SolanaSignMessage,
62
+ ) as SolanaSignMessageFeature[typeof SolanaSignMessage];
63
+ const account = getWalletAccountForUiWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWalletAccount);
64
+ return useCallback(
65
+ async (...inputs) => {
66
+ const inputsWithAccount = inputs.map(input => ({ ...input, account }));
67
+ const results = await signMessageFeature.signMessage(...inputsWithAccount);
68
+ const resultsWithoutSignatureType = results.map(
69
+ ({
70
+ signatureType: _, // Solana signatures are always of type `ed25519` so drop this property.
71
+ ...rest
72
+ }) => rest,
73
+ );
74
+ return resultsWithoutSignatureType;
75
+ },
76
+ [signMessageFeature, account],
77
+ );
78
+ }
@@ -0,0 +1,164 @@
1
+ import {
2
+ SolanaSignTransaction,
3
+ SolanaSignTransactionFeature,
4
+ SolanaSignTransactionInput,
5
+ SolanaSignTransactionOutput,
6
+ } from '@solana/wallet-standard-features';
7
+ import {
8
+ WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED,
9
+ WalletStandardError,
10
+ } from '@wallet-standard/errors';
11
+ import { getWalletAccountFeature, UiWalletAccount } from '@wallet-standard/ui';
12
+ import { getWalletAccountForUiWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '@wallet-standard/ui-registry';
13
+ import { useCallback } from 'react';
14
+
15
+ import { OnlySolanaChains } from './chain';
16
+
17
+ type Input = Readonly<
18
+ Omit<SolanaSignTransactionInput, 'account' | 'chain' | 'options'> & {
19
+ options?: Readonly<{
20
+ minContextSlot?: bigint;
21
+ }>;
22
+ }
23
+ >;
24
+ type Output = SolanaSignTransactionOutput;
25
+
26
+ /**
27
+ * Use this to get a function capable of signing a serialized transaction with the private key of a
28
+ * {@link UiWalletAccount}
29
+ *
30
+ * @param chain The identifier of the chain the transaction is destined for. Wallets may use this to
31
+ * simulate the transaction for the user.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * import { useSignTransaction } from '@solana/react';
36
+ *
37
+ * function SignTransactionButton({ account, transactionBytes }) {
38
+ * const signTransaction = useSignTransaction(account, 'solana:devnet');
39
+ * return (
40
+ * <button
41
+ * onClick={async () => {
42
+ * try {
43
+ * const { signedTransaction } = await signTransaction({
44
+ * transaction: transactionBytes,
45
+ * });
46
+ * window.alert(`Signed transaction bytes: ${signedTransaction.toString()}`);
47
+ * } catch (e) {
48
+ * console.error('Failed to sign transaction', e);
49
+ * }
50
+ * }}
51
+ * >
52
+ * Sign Transaction
53
+ * </button>
54
+ * );
55
+ * }
56
+ * ```
57
+ */
58
+ export function useSignTransaction<TWalletAccount extends UiWalletAccount>(
59
+ uiWalletAccount: TWalletAccount,
60
+ chain: OnlySolanaChains<TWalletAccount['chains']>,
61
+ ): (input: Input) => Promise<Output>;
62
+ export function useSignTransaction<TWalletAccount extends UiWalletAccount>(
63
+ uiWalletAccount: TWalletAccount,
64
+ chain: `solana:${string}`,
65
+ ): (input: Input) => Promise<Output>;
66
+ export function useSignTransaction<TWalletAccount extends UiWalletAccount>(
67
+ uiWalletAccount: TWalletAccount,
68
+ chain: `solana:${string}`,
69
+ ): (input: Input) => Promise<Output> {
70
+ const signTransactions = useSignTransactions(uiWalletAccount, chain);
71
+ return useCallback(
72
+ async input => {
73
+ const [result] = await signTransactions(input);
74
+ return result;
75
+ },
76
+ [signTransactions],
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Use this to get a function capable of signing one or more serialized transactions with the private
82
+ * key of a {@link UiWalletAccount}. This supports batching multiple transactions in a single wallet
83
+ * prompt when the wallet implementation allows it.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * import { useSignTransactions } from '@solana/react';
88
+ *
89
+ * function SignTransactionsButton({ account, transactionBytes1, transactionBytes2 }) {
90
+ * const signTransactions = useSignTransactions(account, 'solana:devnet');
91
+ * return (
92
+ * <button
93
+ * onClick={async () => {
94
+ * try {
95
+ * const [first, second] = await signTransactions(
96
+ * { transaction: transactionBytes1 },
97
+ * { transaction: transactionBytes2 },
98
+ * );
99
+ * window.alert(
100
+ * `First transaction bytes: ${first.signedTransaction.toString()}, second transaction bytes: ${second.signedTransaction.toString()}`,
101
+ * );
102
+ * } catch (e) {
103
+ * console.error('Failed to sign transactions', e);
104
+ * }
105
+ * }}
106
+ * >
107
+ * Sign Transactions
108
+ * </button>
109
+ * );
110
+ * }
111
+ * ```
112
+ */
113
+ export function useSignTransactions<TWalletAccount extends UiWalletAccount>(
114
+ uiWalletAccount: TWalletAccount,
115
+ chain: OnlySolanaChains<TWalletAccount['chains']>,
116
+ ): (...inputs: readonly Input[]) => Promise<readonly Output[]>;
117
+ export function useSignTransactions<TWalletAccount extends UiWalletAccount>(
118
+ uiWalletAccount: TWalletAccount,
119
+ chain: `solana:${string}`,
120
+ ): (...inputs: readonly Input[]) => Promise<readonly Output[]>;
121
+ export function useSignTransactions<TWalletAccount extends UiWalletAccount>(
122
+ uiWalletAccount: TWalletAccount,
123
+ chain: `solana:${string}`,
124
+ ): (...inputs: readonly Input[]) => Promise<readonly Output[]> {
125
+ if (!uiWalletAccount.chains.includes(chain)) {
126
+ throw new WalletStandardError(WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED, {
127
+ address: uiWalletAccount.address,
128
+ chain,
129
+ featureName: SolanaSignTransaction,
130
+ supportedChains: [...uiWalletAccount.chains],
131
+ supportedFeatures: [...uiWalletAccount.features],
132
+ });
133
+ }
134
+ const signTransactionFeature = getWalletAccountFeature(
135
+ uiWalletAccount,
136
+ SolanaSignTransaction,
137
+ ) as SolanaSignTransactionFeature[typeof SolanaSignTransaction];
138
+ const account = getWalletAccountForUiWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWalletAccount);
139
+ return useCallback(
140
+ async (...inputs) => {
141
+ if (inputs.length === 0) {
142
+ return [];
143
+ }
144
+ const inputsWithAccountAndChain = inputs.map(({ options, ...rest }) => {
145
+ const minContextSlot = options?.minContextSlot;
146
+ return {
147
+ ...rest,
148
+ account,
149
+ chain,
150
+ ...(minContextSlot != null
151
+ ? {
152
+ options: {
153
+ minContextSlot: Number(minContextSlot),
154
+ },
155
+ }
156
+ : null),
157
+ };
158
+ });
159
+ const results = await signTransactionFeature.signTransaction(...inputsWithAccountAndChain);
160
+ return results;
161
+ },
162
+ [signTransactionFeature, account, chain],
163
+ );
164
+ }
@@ -0,0 +1,98 @@
1
+ import { Address, address } from '@solana/addresses';
2
+ import { bytesEqual } from '@solana/codecs-core';
3
+ import { SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED, SolanaError } from '@solana/errors';
4
+ import { SignatureBytes } from '@solana/keys';
5
+ import { getAbortablePromise } from '@solana/promises';
6
+ import { MessageModifyingSigner, SignableMessage } from '@solana/signers';
7
+ import type { UiWalletAccount } from '@wallet-standard/ui';
8
+ import { useMemo } from 'react';
9
+
10
+ import { useSignMessage } from './useSignMessage';
11
+
12
+ /**
13
+ * Use this to get a {@link MessageSigner} capable of signing messages with the private key of a
14
+ * {@link UiWalletAccount}
15
+ *
16
+ * @returns A {@link MessageModifyingSigner}. This is a conservative assumption based on the fact
17
+ * that your application can not control whether or not the wallet will modify the message before
18
+ * signing it. Otherwise this method could more specifically return a {@link MessageSigner} or a
19
+ * {@link MessagePartialSigner}.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * import { useWalletAccountMessageSigner } from '@solana/react';
24
+ * import { createSignableMessage } from '@solana/signers';
25
+ *
26
+ * function SignMessageButton({ account, text }) {
27
+ * const messageSigner = useWalletAccountMessageSigner(account);
28
+ * return (
29
+ * <button
30
+ * onClick={async () => {
31
+ * try {
32
+ * const signableMessage = createSignableMessage(text);
33
+ * const [signedMessage] = await messageSigner.modifyAndSignMessages([signableMessage]);
34
+ * const messageWasModified = signableMessage.content !== signedMessage.content;
35
+ * const signatureBytes = signedMessage.signatures[messageSigner.address];
36
+ * window.alert(
37
+ * `Signature bytes: ${signatureBytes.toString()}${
38
+ * messageWasModified ? ' (message was modified)' : ''
39
+ * }`,
40
+ * );
41
+ * } catch (e) {
42
+ * console.error('Failed to sign message', e);
43
+ * }
44
+ * }}
45
+ * >
46
+ * Sign Message: {text}
47
+ * </button>
48
+ * );
49
+ * }
50
+ * ```
51
+ */
52
+ export function useWalletAccountMessageSigner<TWalletAccount extends UiWalletAccount>(
53
+ uiWalletAccount: TWalletAccount,
54
+ ): MessageModifyingSigner<TWalletAccount['address']> {
55
+ const signMessage = useSignMessage(uiWalletAccount);
56
+ return useMemo(
57
+ () => ({
58
+ address: address(uiWalletAccount.address),
59
+ async modifyAndSignMessages(messages, config) {
60
+ config?.abortSignal?.throwIfAborted();
61
+ if (messages.length > 1) {
62
+ throw new SolanaError(SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED);
63
+ }
64
+ if (messages.length === 0) {
65
+ return messages;
66
+ }
67
+ const { content: originalMessage, signatures: originalSignatureMap } = messages[0];
68
+ const input = {
69
+ message: originalMessage,
70
+ };
71
+ const { signedMessage, signature } = await getAbortablePromise(signMessage(input), config?.abortSignal);
72
+ const messageWasModified =
73
+ originalMessage.length !== signedMessage.length ||
74
+ originalMessage.some((originalByte, ii) => originalByte !== signedMessage[ii]);
75
+ const originalSignature = originalSignatureMap[uiWalletAccount.address as Address<string>] as
76
+ | SignatureBytes
77
+ | undefined;
78
+ const signatureIsNew = originalSignature === undefined || !bytesEqual(originalSignature, signature);
79
+ if (!signatureIsNew && !messageWasModified) {
80
+ // We already had this exact signature, and the message wasn't modified.
81
+ // Don't replace the existing message object.
82
+ return messages;
83
+ }
84
+ const nextSignatureMap = messageWasModified
85
+ ? { [uiWalletAccount.address]: signature }
86
+ : { ...originalSignatureMap, [uiWalletAccount.address]: signature };
87
+ const outputMessages = Object.freeze([
88
+ Object.freeze({
89
+ content: signedMessage,
90
+ signatures: Object.freeze(nextSignatureMap),
91
+ }) as SignableMessage,
92
+ ]);
93
+ return outputMessages;
94
+ },
95
+ }),
96
+ [uiWalletAccount, signMessage],
97
+ );
98
+ }
@@ -0,0 +1,101 @@
1
+ import { address } from '@solana/addresses';
2
+ import { SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED, SolanaError } from '@solana/errors';
3
+ import { SignatureBytes } from '@solana/keys';
4
+ import { getAbortablePromise } from '@solana/promises';
5
+ import { TransactionSendingSigner } from '@solana/signers';
6
+ import { getTransactionEncoder } from '@solana/transactions';
7
+ import { UiWalletAccount } from '@wallet-standard/ui';
8
+ import { useMemo, useRef } from 'react';
9
+
10
+ import { OnlySolanaChains } from './chain';
11
+ import { useSignAndSendTransaction } from './useSignAndSendTransaction';
12
+
13
+ /**
14
+ * Use this to get a {@link TransactionSendingSigner} capable of signing a serialized transaction
15
+ * with the private key of a {@link UiWalletAccount} and sending it to the network for processing.
16
+ *
17
+ * @param chain The identifier of the chain the transaction is destined for. Wallets may use this to
18
+ * simulate the transaction for the user.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * import { useWalletAccountTransactionSendingSigner } from '@solana/react';
23
+ * import {
24
+ * appendTransactionMessageInstruction,
25
+ * createSolanaRpc,
26
+ * getBase58Decoder,
27
+ * pipe,
28
+ * setTransactionMessageFeePayerSigner,
29
+ * setTransactionMessageLifetimeUsingBlockhash,
30
+ * signAndSendTransactionMessageWithSigners,
31
+ * } from '@solana/kit';
32
+ *
33
+ * function RecordMemoButton({ account, rpc, text }) {
34
+ * const signer = useWalletAccountTransactionSendingSigner(account, 'solana:devnet');
35
+ * return (
36
+ * <button
37
+ * onClick={async () => {
38
+ * try {
39
+ * const { value: latestBlockhash } = await createSolanaRpc('https://api.devnet.solana.com')
40
+ * .getLatestBlockhash()
41
+ * .send();
42
+ * const message = pipe(
43
+ * createTransactionMessage({ version: 'legacy' }),
44
+ * m => setTransactionMessageFeePayerSigner(signer, m),
45
+ * m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
46
+ * m => appendTransactionMessageInstruction(getAddMemoInstruction({ memo: text }), m),
47
+ * );
48
+ * const signatureBytes = await signAndSendTransactionMessageWithSigners(message);
49
+ * const base58Signature = getBase58Decoder().decode(signature);
50
+ * window.alert(`View transaction: https://explorer.solana.com/tx/${base58Signature}?cluster=devnet`);
51
+ * } catch (e) {
52
+ * console.error('Failed to record memo', e);
53
+ * }
54
+ * }}
55
+ * >
56
+ * Record Memo
57
+ * </button>
58
+ * );
59
+ * }
60
+ * ```
61
+ */
62
+ export function useWalletAccountTransactionSendingSigner<TWalletAccount extends UiWalletAccount>(
63
+ uiWalletAccount: TWalletAccount,
64
+ chain: OnlySolanaChains<TWalletAccount['chains']>,
65
+ ): TransactionSendingSigner<TWalletAccount['address']>;
66
+ export function useWalletAccountTransactionSendingSigner<TWalletAccount extends UiWalletAccount>(
67
+ uiWalletAccount: TWalletAccount,
68
+ chain: `solana:${string}`,
69
+ ): TransactionSendingSigner<TWalletAccount['address']>;
70
+ export function useWalletAccountTransactionSendingSigner<TWalletAccount extends UiWalletAccount>(
71
+ uiWalletAccount: TWalletAccount,
72
+ chain: `solana:${string}`,
73
+ ): TransactionSendingSigner<TWalletAccount['address']> {
74
+ const encoderRef = useRef<ReturnType<typeof getTransactionEncoder> | null>(null);
75
+ const signAndSendTransaction = useSignAndSendTransaction(uiWalletAccount, chain);
76
+ return useMemo(
77
+ () => ({
78
+ address: address(uiWalletAccount.address),
79
+ async signAndSendTransactions(transactions, config = {}) {
80
+ const { abortSignal, ...options } = config;
81
+ abortSignal?.throwIfAborted();
82
+ const transactionEncoder = (encoderRef.current ||= getTransactionEncoder());
83
+ if (transactions.length > 1) {
84
+ throw new SolanaError(SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED);
85
+ }
86
+ if (transactions.length === 0) {
87
+ return [];
88
+ }
89
+ const [transaction] = transactions;
90
+ const wireTransactionBytes = transactionEncoder.encode(transaction);
91
+ const inputWithOptions = {
92
+ ...options,
93
+ transaction: wireTransactionBytes as Uint8Array,
94
+ };
95
+ const { signature } = await getAbortablePromise(signAndSendTransaction(inputWithOptions), abortSignal);
96
+ return Object.freeze([signature as SignatureBytes]);
97
+ },
98
+ }),
99
+ [signAndSendTransaction, uiWalletAccount.address],
100
+ );
101
+ }
@@ -0,0 +1,146 @@
1
+ import { address } from '@solana/addresses';
2
+ import { bytesEqual } from '@solana/codecs-core';
3
+ import { SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED, SolanaError } from '@solana/errors';
4
+ import { getAbortablePromise } from '@solana/promises';
5
+ import { TransactionModifyingSigner } from '@solana/signers';
6
+ import { getCompiledTransactionMessageDecoder } from '@solana/transaction-messages';
7
+ import {
8
+ assertIsTransactionWithinSizeLimit,
9
+ getTransactionCodec,
10
+ getTransactionLifetimeConstraintFromCompiledTransactionMessage,
11
+ Transaction,
12
+ TransactionWithinSizeLimit,
13
+ TransactionWithLifetime,
14
+ } from '@solana/transactions';
15
+ import { UiWalletAccount } from '@wallet-standard/ui';
16
+ import { useMemo, useRef } from 'react';
17
+
18
+ import { OnlySolanaChains } from './chain';
19
+ import { useSignTransaction } from './useSignTransaction';
20
+
21
+ /**
22
+ * Use this to get a {@link TransactionSigner} capable of signing serialized transactions with the
23
+ * private key of a {@link UiWalletAccount}
24
+ *
25
+ * @returns A {@link TransactionModifyingSigner}. This is a conservative assumption based on the
26
+ * fact that your application can not control whether or not the wallet will modify the transaction
27
+ * before signing it (eg. to add guard instructions, or a priority fee budget). Otherwise this
28
+ * method could more specifically return a {@link TransactionSigner} or a
29
+ * {@link TransactionPartialSigner}.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * import { useWalletAccountTransactionSigner } from '@solana/react';
34
+ *
35
+ * function SignTransactionButton({ account, transaction }) {
36
+ * const transactionSigner = useWalletAccountTransactionSigner(account, 'solana:devnet');
37
+ * return (
38
+ * <button
39
+ * onClick={async () => {
40
+ * try {
41
+ * const [{ signatures }] = await transactionSigner.modifyAndSignTransactions([transaction]);
42
+ * const signatureBytes = signatures[transactionSigner.address];
43
+ * window.alert(`Signature bytes: ${signatureBytes.toString()}`);
44
+ * } catch (e) {
45
+ * console.error('Failed to sign transaction', e);
46
+ * }
47
+ * }}
48
+ * >
49
+ * Sign Transaction
50
+ * </button>
51
+ * );
52
+ * }
53
+ * ```
54
+ */
55
+ export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalletAccount>(
56
+ uiWalletAccount: TWalletAccount,
57
+ chain: OnlySolanaChains<TWalletAccount['chains']>,
58
+ ): TransactionModifyingSigner<TWalletAccount['address']>;
59
+ export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalletAccount>(
60
+ uiWalletAccount: TWalletAccount,
61
+ chain: `solana:${string}`,
62
+ ): TransactionModifyingSigner<TWalletAccount['address']>;
63
+ export function useWalletAccountTransactionSigner<TWalletAccount extends UiWalletAccount>(
64
+ uiWalletAccount: TWalletAccount,
65
+ chain: `solana:${string}`,
66
+ ): TransactionModifyingSigner<TWalletAccount['address']> {
67
+ const encoderRef = useRef<ReturnType<typeof getTransactionCodec> | null>(null);
68
+ const signTransaction = useSignTransaction(uiWalletAccount, chain);
69
+ return useMemo(
70
+ () => ({
71
+ address: address(uiWalletAccount.address),
72
+ async modifyAndSignTransactions(transactions, config = {}) {
73
+ const { abortSignal, ...options } = config;
74
+ abortSignal?.throwIfAborted();
75
+ const transactionCodec = (encoderRef.current ||= getTransactionCodec());
76
+ if (transactions.length > 1) {
77
+ throw new SolanaError(SOLANA_ERROR__SIGNER__WALLET_MULTISIGN_UNIMPLEMENTED);
78
+ }
79
+ if (transactions.length === 0) {
80
+ return transactions as readonly (Transaction &
81
+ TransactionWithinSizeLimit &
82
+ TransactionWithLifetime)[];
83
+ }
84
+ const [transaction] = transactions;
85
+ const wireTransactionBytes = transactionCodec.encode(transaction);
86
+ const inputWithOptions = {
87
+ ...options,
88
+ transaction: wireTransactionBytes as Uint8Array,
89
+ };
90
+ const { signedTransaction } = await getAbortablePromise(signTransaction(inputWithOptions), abortSignal);
91
+ const decodedSignedTransaction = transactionCodec.decode(
92
+ signedTransaction,
93
+ ) as (typeof transactions)[number];
94
+
95
+ assertIsTransactionWithinSizeLimit(decodedSignedTransaction);
96
+
97
+ const existingLifetime =
98
+ 'lifetimeConstraint' in transaction
99
+ ? (transaction as TransactionWithLifetime).lifetimeConstraint
100
+ : undefined;
101
+
102
+ if (existingLifetime) {
103
+ if (bytesEqual(decodedSignedTransaction.messageBytes, transaction.messageBytes)) {
104
+ // If the transaction has identical bytes, the lifetime won't have changed
105
+ return Object.freeze([
106
+ {
107
+ ...decodedSignedTransaction,
108
+ lifetimeConstraint: existingLifetime,
109
+ },
110
+ ]);
111
+ }
112
+
113
+ // If the transaction has changed, check the lifetime constraint field
114
+ const compiledTransactionMessage = getCompiledTransactionMessageDecoder().decode(
115
+ decodedSignedTransaction.messageBytes,
116
+ );
117
+ const currentToken =
118
+ 'blockhash' in existingLifetime ? existingLifetime.blockhash : existingLifetime.nonce;
119
+
120
+ if (compiledTransactionMessage.lifetimeToken === currentToken) {
121
+ return Object.freeze([
122
+ {
123
+ ...decodedSignedTransaction,
124
+ lifetimeConstraint: existingLifetime,
125
+ },
126
+ ]);
127
+ }
128
+ }
129
+
130
+ // If we get here then there is no existing lifetime, or the lifetime has changed. We need to attach a new lifetime
131
+ const compiledTransactionMessage = getCompiledTransactionMessageDecoder().decode(
132
+ decodedSignedTransaction.messageBytes,
133
+ );
134
+ const lifetimeConstraint =
135
+ await getTransactionLifetimeConstraintFromCompiledTransactionMessage(compiledTransactionMessage);
136
+ return Object.freeze([
137
+ {
138
+ ...decodedSignedTransaction,
139
+ lifetimeConstraint,
140
+ },
141
+ ]);
142
+ },
143
+ }),
144
+ [uiWalletAccount.address, signTransaction],
145
+ );
146
+ }