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..9c526af3a 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'; @@ -24,10 +24,10 @@ export abstract class BaseSignerWalletAdapter extends BaseWalletAdapter implements SignerWalletAdapter { - async sendTransaction( + async signAndSendTransaction( transaction: TransactionOrVersionedTransaction, connection: Connection, - options: SendTransactionOptions = {} + options: SignAndSendTransactionOptions = {} ): Promise { let emit = true; try { 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 (