From 51f99a46edf2d776fad3158c4600762288dbd068 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 24 Oct 2023 16:55:56 -0600 Subject: [PATCH 1/4] WIP of signAndSendTransaction + signAndSendAllTransactions API --- packages/core/base/src/adapter.ts | 69 ++++++++++++++++++++++++++++--- packages/core/base/src/signer.ts | 4 +- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/packages/core/base/src/adapter.ts b/packages/core/base/src/adapter.ts index a3525a4c1..37ae98308 100644 --- a/packages/core/base/src/adapter.ts +++ b/packages/core/base/src/adapter.ts @@ -1,6 +1,6 @@ -import type { Connection, PublicKey, SendOptions, Signer, Transaction, TransactionSignature } from '@solana/web3.js'; +import type { Commitment, Connection, PublicKey, Signer, Transaction, TransactionSignature } from '@solana/web3.js'; import EventEmitter from 'eventemitter3'; -import { type WalletError, WalletNotConnectedError } from './errors.js'; +import { WalletNotConnectedError, type WalletError } from './errors.js'; import type { SupportedTransactionVersions, TransactionOrVersionedTransaction } from './transaction.js'; export { EventEmitter }; @@ -12,10 +12,29 @@ export interface WalletAdapterEvents { readyStateChange(readyState: WalletReadyState): void; } -export interface SendTransactionOptions extends SendOptions { +export interface SignAndSendTransactionOptions extends SendOptions { signers?: Signer[]; } +export interface SendOptions { + minContextSlot?: number; + /** @deprecated Wallets are not expected to support this option. */ + skipPreflight?: boolean; + /** @deprecated Wallets are not expected to support this option. */ + preflightCommitment?: Commitment; + /** @deprecated Wallets are not expected to support this option. */ + maxRetries?: number; +} + +/** @deprecated Use `SignAndSendTransactionOptions` instead. */ +export type SendTransactionOptions = SignAndSendTransactionOptions; + +export interface SignAndSendAllTransactionsError { + type: string; + code: number; + message: string; +} + // WalletName is a nominal type that wallet adapters should use, e.g. `'MyCryptoWallet' as WalletName<'MyCryptoWallet'>` // https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d export type WalletName = T & { __brand__: 'WalletName' }; @@ -33,10 +52,21 @@ export interface WalletAdapterProps { autoConnect(): Promise; connect(): Promise; disconnect(): Promise; + signAndSendTransaction( + transaction: TransactionOrVersionedTransaction, + connection: Connection, + options?: SignAndSendTransactionOptions + ): Promise; + signAndSendAllTransactions( + transactions: TransactionOrVersionedTransaction[], + connection: Connection, + options?: SignAndSendTransactionOptions + ): Promise<(TransactionSignature | SignAndSendAllTransactionsError)[]>; + /** @deprecated Use `signAndSendTransaction` instead. */ sendTransaction( transaction: TransactionOrVersionedTransaction, connection: Connection, - options?: SendTransactionOptions + options?: SignAndSendTransactionOptions ): Promise; } @@ -94,12 +124,39 @@ export abstract class BaseWalletAdapter abstract connect(): Promise; abstract disconnect(): Promise; - abstract sendTransaction( + abstract signAndSendTransaction( transaction: TransactionOrVersionedTransaction, connection: Connection, - options?: SendTransactionOptions + options?: SignAndSendTransactionOptions ): Promise; + async signAndSendAllTransactions( + transactions: TransactionOrVersionedTransaction[], + connection: Connection, + options?: SignAndSendTransactionOptions | undefined + ): Promise<(TransactionSignature | SignAndSendAllTransactionsError)[]> { + const results = await Promise.allSettled( + transactions.map((transaction) => this.signAndSendTransaction(transaction, connection, options)) + ); + return results.map((result) => { + if (result.status === 'fulfilled') return result.value; + return { + type: result.reason.type || result.reason.name, + code: result.reason.code, + message: result.reason.message, + }; + }); + } + + /** @deprecated Use `signAndSendTransaction` instead. */ + sendTransaction( + transaction: TransactionOrVersionedTransaction, + connection: Connection, + options?: SignAndSendTransactionOptions + ): Promise { + return this.signAndSendTransaction(transaction, connection, options); + } + protected async prepareTransaction( transaction: Transaction, connection: Connection, diff --git a/packages/core/base/src/signer.ts b/packages/core/base/src/signer.ts index 76f147b00..7ec6d869e 100644 --- a/packages/core/base/src/signer.ts +++ b/packages/core/base/src/signer.ts @@ -2,7 +2,7 @@ import type { SolanaSignInInput, SolanaSignInOutput } from '@solana/wallet-stand import type { Connection, TransactionSignature } from '@solana/web3.js'; import { BaseWalletAdapter, - type SendTransactionOptions, + type SignAndSendTransactionOptions, type WalletAdapter, type WalletAdapterProps, } from './adapter.js'; @@ -27,7 +27,7 @@ export abstract class BaseSignerWalletAdapter async sendTransaction( transaction: TransactionOrVersionedTransaction, connection: Connection, - options: SendTransactionOptions = {} + options: SignAndSendTransactionOptions = {} ): Promise { let emit = true; try { From fd34bbb49bd06072cc649526442d7d5358a0fb36 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 24 Oct 2023 17:01:48 -0600 Subject: [PATCH 2/4] fix method name --- packages/core/base/src/signer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/base/src/signer.ts b/packages/core/base/src/signer.ts index 7ec6d869e..9c526af3a 100644 --- a/packages/core/base/src/signer.ts +++ b/packages/core/base/src/signer.ts @@ -24,7 +24,7 @@ export abstract class BaseSignerWalletAdapter extends BaseWalletAdapter implements SignerWalletAdapter { - async sendTransaction( + async signAndSendTransaction( transaction: TransactionOrVersionedTransaction, connection: Connection, options: SignAndSendTransactionOptions = {} From 47b052304d6e086d582be69ae4378594ab66f3e9 Mon Sep 17 00:00:00 2001 From: Jordan Sexton Date: Tue, 24 Oct 2023 17:22:50 -0600 Subject: [PATCH 3/4] fix react context, hooks, adapters --- .../core/react/src/WalletProviderBase.tsx | 23 +++++++++++++++---- .../react/src/__mocks__/MockWalletAdapter.ts | 2 +- .../src/__tests__/WalletProviderBase-test.tsx | 2 +- .../__tests__/WalletProviderMobile-test.tsx | 2 +- packages/core/react/src/useWallet.ts | 15 +++++++++--- .../src/components/SendLegacyTransaction.tsx | 6 ++--- .../src/components/SendTransaction.tsx | 6 ++--- .../src/components/SendV0Transaction.tsx | 6 ++--- packages/wallets/alpha/src/adapter.ts | 6 ++--- packages/wallets/avana/src/adapter.ts | 6 ++--- packages/wallets/censo/src/adapter.ts | 6 ++--- packages/wallets/coinbase/src/adapter.ts | 6 ++--- packages/wallets/nufi/src/adapter.ts | 6 ++--- packages/wallets/phantom/src/adapter.ts | 6 ++--- packages/wallets/saifu/src/adapter.ts | 8 +++---- packages/wallets/sky/src/adapter.ts | 6 ++--- packages/wallets/solflare/src/adapter.ts | 6 ++--- packages/wallets/tokenary/src/adapter.ts | 6 ++--- packages/wallets/torus/src/adapter.ts | 6 ++--- packages/wallets/trust/src/adapter.ts | 6 ++--- 20 files changed, 80 insertions(+), 56 deletions(-) diff --git a/packages/core/react/src/WalletProviderBase.tsx b/packages/core/react/src/WalletProviderBase.tsx index 642ea43c0..243172ba7 100644 --- a/packages/core/react/src/WalletProviderBase.tsx +++ b/packages/core/react/src/WalletProviderBase.tsx @@ -194,12 +194,22 @@ export function WalletProviderBase({ })(); }, [connected, onAutoConnectRequest, onConnectError, wallet]); - // Send a transaction using the provided connection - const sendTransaction: WalletAdapterProps['sendTransaction'] = useCallback( + // Sign and send a transaction using the provided connection + const signAndSendTransaction: WalletAdapterProps['signAndSendTransaction'] = useCallback( async (transaction, connection, options) => { if (!adapter) throw handleErrorRef.current(new WalletNotSelectedError()); if (!connected) throw handleErrorRef.current(new WalletNotConnectedError(), adapter); - return await adapter.sendTransaction(transaction, connection, options); + return await adapter.signAndSendTransaction(transaction, connection, options); + }, + [adapter, connected] + ); + + // Sign and send multiple transactions using the provided connection + const signAndSendAllTransactions: WalletAdapterProps['signAndSendAllTransactions'] = useCallback( + async (transactions, connection, options) => { + if (!adapter) throw handleErrorRef.current(new WalletNotSelectedError()); + if (!connected) throw handleErrorRef.current(new WalletNotConnectedError(), adapter); + return await adapter.signAndSendAllTransactions(transactions, connection, options); }, [adapter, connected] ); @@ -251,6 +261,9 @@ export function WalletProviderBase({ [adapter] ); + // Deprecated alias for `signAndSendTransaction`. + const sendTransaction: WalletAdapterProps['sendTransaction'] = signAndSendTransaction; + const handleConnect = useCallback(async () => { if (isConnectingRef.current || isDisconnectingRef.current || wallet?.adapter.connected) return; if (!wallet) throw handleErrorRef.current(new WalletNotSelectedError()); @@ -296,11 +309,13 @@ export function WalletProviderBase({ select: onSelectWallet, connect: handleConnect, disconnect: handleDisconnect, - sendTransaction, + signAndSendTransaction, + signAndSendAllTransactions, signTransaction, signAllTransactions, signMessage, signIn, + sendTransaction, }} > {children} diff --git a/packages/core/react/src/__mocks__/MockWalletAdapter.ts b/packages/core/react/src/__mocks__/MockWalletAdapter.ts index 4f0489b78..676c341ef 100644 --- a/packages/core/react/src/__mocks__/MockWalletAdapter.ts +++ b/packages/core/react/src/__mocks__/MockWalletAdapter.ts @@ -27,7 +27,7 @@ export abstract class MockWalletAdapter extends BaseWalletAdapter { this.emit('disconnect'); }); }); - sendTransaction = jest.fn(); + signAndSendTransaction = jest.fn(); supportedTransactionVersions = null; autoConnect = jest.fn(); } diff --git a/packages/core/react/src/__tests__/WalletProviderBase-test.tsx b/packages/core/react/src/__tests__/WalletProviderBase-test.tsx index 6a89c3621..234bf1d53 100644 --- a/packages/core/react/src/__tests__/WalletProviderBase-test.tsx +++ b/packages/core/react/src/__tests__/WalletProviderBase-test.tsx @@ -102,7 +102,7 @@ describe('WalletProviderBase', () => { this.emit('disconnect'); }); }); - sendTransaction = jest.fn(); + signAndSendTransaction = jest.fn(); supportedTransactionVersions = null; } class FooWalletAdapter extends MockWalletAdapter { diff --git a/packages/core/react/src/__tests__/WalletProviderMobile-test.tsx b/packages/core/react/src/__tests__/WalletProviderMobile-test.tsx index 33507cf33..c478603b0 100644 --- a/packages/core/react/src/__tests__/WalletProviderMobile-test.tsx +++ b/packages/core/react/src/__tests__/WalletProviderMobile-test.tsx @@ -116,7 +116,7 @@ describe('WalletProvider when the environment is `MOBILE_WEB`', () => { this.emit('disconnect'); }); }); - sendTransaction = jest.fn(); + signAndSendTransaction = jest.fn(); supportedTransactionVersions = null; autoConnect = jest.fn(); } diff --git a/packages/core/react/src/useWallet.ts b/packages/core/react/src/useWallet.ts index 87a199606..86a6cfb29 100644 --- a/packages/core/react/src/useWallet.ts +++ b/packages/core/react/src/useWallet.ts @@ -28,11 +28,14 @@ export interface WalletContextState { connect(): Promise; disconnect(): Promise; - sendTransaction: WalletAdapterProps['sendTransaction']; + signAndSendTransaction: WalletAdapterProps['signAndSendTransaction']; + signAndSendAllTransactions: WalletAdapterProps['signAndSendAllTransactions']; signTransaction: SignerWalletAdapterProps['signTransaction'] | undefined; signAllTransactions: SignerWalletAdapterProps['signAllTransactions'] | undefined; signMessage: MessageSignerWalletAdapterProps['signMessage'] | undefined; signIn: SignInMessageSignerWalletAdapterProps['signIn'] | undefined; + /** @deprecated Use `signAndSendTransaction` instead. */ + sendTransaction: WalletAdapterProps['sendTransaction']; } const EMPTY_ARRAY: ReadonlyArray = []; @@ -51,8 +54,11 @@ const DEFAULT_CONTEXT: Partial = { disconnect() { return Promise.reject(logMissingProviderError('call', 'disconnect')); }, - sendTransaction() { - return Promise.reject(logMissingProviderError('call', 'sendTransaction')); + signAndSendTransaction() { + return Promise.reject(logMissingProviderError('call', 'signAndSendTransaction')); + }, + signAndSendAllTransactions() { + return Promise.reject(logMissingProviderError('call', 'signAndSendAllTransactions')); }, signTransaction() { return Promise.reject(logMissingProviderError('call', 'signTransaction')); @@ -66,6 +72,9 @@ const DEFAULT_CONTEXT: Partial = { signIn() { return Promise.reject(logMissingProviderError('call', 'signIn')); }, + sendTransaction() { + return Promise.reject(logMissingProviderError('call', 'sendTransaction')); + }, }; Object.defineProperty(DEFAULT_CONTEXT, 'wallets', { get() { diff --git a/packages/starter/example/src/components/SendLegacyTransaction.tsx b/packages/starter/example/src/components/SendLegacyTransaction.tsx index 8238d6ec7..b860509d0 100644 --- a/packages/starter/example/src/components/SendLegacyTransaction.tsx +++ b/packages/starter/example/src/components/SendLegacyTransaction.tsx @@ -8,7 +8,7 @@ import { useNotify } from './notify'; export const SendLegacyTransaction: FC = () => { const { connection } = useConnection(); - const { publicKey, sendTransaction, wallet } = useWallet(); + const { publicKey, signAndSendTransaction, wallet } = useWallet(); const notify = useNotify(); const supportedTransactionVersions = wallet?.adapter.supportedTransactionVersions; @@ -38,7 +38,7 @@ export const SendLegacyTransaction: FC = () => { }); const transaction = new VersionedTransaction(message.compileToLegacyMessage()); - signature = await sendTransaction(transaction, connection, { minContextSlot }); + signature = await signAndSendTransaction(transaction, connection, { minContextSlot }); notify('info', 'Transaction sent:', signature); await connection.confirmTransaction({ blockhash, lastValidBlockHeight, signature }); @@ -46,7 +46,7 @@ export const SendLegacyTransaction: FC = () => { } catch (error: any) { notify('error', `Transaction failed! ${error?.message}`, signature); } - }, [publicKey, supportedTransactionVersions, connection, sendTransaction, notify]); + }, [publicKey, supportedTransactionVersions, connection, signAndSendTransaction, notify]); return (