Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

signAndSendTransaction + signAndSendAllTransactions API #889

Open
wants to merge 7 commits into
base: sign-and-send-all-transactions
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ This shouldn't happen if you're using one of the starter projects, since they se

### `[...] is not a function`

This can happen if you try to use `signTransaction`, `signAllTransactions`, or `signMessage` without checking if they are defined first.
This can happen if you try to use `signTransaction`, `signAllTransactions`, `signAndSendAllTransactions`, `signMessage`, or `signIn` without checking if they are defined first.

`sendTransaction` is the primary method that all wallets support, and it signs transactions.
The other methods are optional APIs, so you have to feature-detect them before using them.
Expand Down
2 changes: 1 addition & 1 deletion packages/core/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@solana/web3.js": "^1.77.3"
},
"dependencies": {
"@solana/wallet-standard-features": "^1.1.0",
"@solana/wallet-standard-features": "^1.2.0",
"@wallet-standard/base": "^1.0.1",
"@wallet-standard/features": "^1.0.3",
"eventemitter3": "^4.0.7"
Expand Down
38 changes: 6 additions & 32 deletions packages/core/base/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Commitment, Connection, PublicKey, Signer, Transaction, Transactio
import EventEmitter from 'eventemitter3';
import { WalletNotConnectedError, type WalletError } from './errors.js';
import type { SupportedTransactionVersions, TransactionOrVersionedTransaction } from './transaction.js';
import type { SolanaSignAndSendTransactionMode } from '@solana/wallet-standard-features';

export { EventEmitter };

Expand All @@ -18,23 +19,19 @@ export interface SignAndSendTransactionOptions extends SendOptions {

export interface SendOptions {
minContextSlot?: number;
/** @deprecated Wallets are not expected to support this option. */
/** @deprecated Wallets are not expected to support this option. */
skipPreflight?: boolean;
/** @deprecated Wallets are not expected to support this option. */
/** @deprecated Wallets are not expected to support this option. */
preflightCommitment?: Commitment;
/** @deprecated Wallets are not expected to support this option. */
/** @deprecated Wallets are not expected to support this option. */
maxRetries?: number;
/** Mode for signing and sending transactions. */
mode?: SolanaSignAndSendTransactionMode;
}

/** @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 extends string = string> = T & { __brand__: 'WalletName' };
Expand All @@ -57,11 +54,6 @@ export interface WalletAdapterProps<Name extends string = string> {
connection: Connection,
options?: SignAndSendTransactionOptions
): Promise<TransactionSignature>;
signAndSendAllTransactions(
transactions: TransactionOrVersionedTransaction<this['supportedTransactionVersions']>[],
connection: Connection,
options?: SignAndSendTransactionOptions
): Promise<(TransactionSignature | SignAndSendAllTransactionsError)[]>;
/** @deprecated Use `signAndSendTransaction` instead. */
sendTransaction(
transaction: TransactionOrVersionedTransaction<this['supportedTransactionVersions']>,
Expand Down Expand Up @@ -130,24 +122,6 @@ export abstract class BaseWalletAdapter<Name extends string = string>
options?: SignAndSendTransactionOptions
): Promise<TransactionSignature>;

async signAndSendAllTransactions(
transactions: TransactionOrVersionedTransaction<this['supportedTransactionVersions']>[],
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<this['supportedTransactionVersions']>,
Expand Down
20 changes: 20 additions & 0 deletions packages/core/base/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,23 @@ export class WalletWindowBlockedError extends WalletError {
export class WalletWindowClosedError extends WalletError {
name = 'WalletWindowClosedError';
}

export class WalletSignAndSendAllTransactionsError extends WalletError {
name = 'WalletSignAndSendAllTransactionsError';

// Holds the original error object or details
error: any;

// Type of the error (if available)
type?: string;

// Error code (if available)
code?: number;

constructor(error: any) {
super(error?.reason?.message, error);
this.error = error;
this.type = error?.type || error?.name;
this.code = error?.code;
}
}
20 changes: 20 additions & 0 deletions packages/core/base/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type WalletAdapter,
type WalletAdapterProps,
} from './adapter.js';
import { WalletSignAndSendAllTransactionsError } from './errors.js';
import { WalletSendTransactionError, WalletSignTransactionError } from './errors.js';
import { isVersionedTransaction, type TransactionOrVersionedTransaction } from './transaction.js';

Expand All @@ -16,6 +17,11 @@ export interface SignerWalletAdapterProps<Name extends string = string> extends
signAllTransactions<T extends TransactionOrVersionedTransaction<this['supportedTransactionVersions']>>(
transactions: T[]
): Promise<T[]>;
signAndSendAllTransactions(
transactions: TransactionOrVersionedTransaction<this['supportedTransactionVersions']>[],
connection: Connection,
options?: SignAndSendTransactionOptions
): Promise<(TransactionSignature | WalletSignAndSendAllTransactionsError)[]>;
}

export type SignerWalletAdapter<Name extends string = string> = WalletAdapter<Name> & SignerWalletAdapterProps<Name>;
Expand Down Expand Up @@ -86,6 +92,20 @@ export abstract class BaseSignerWalletAdapter<Name extends string = string>
}
}

async signAndSendAllTransactions(
transactions: TransactionOrVersionedTransaction<this['supportedTransactionVersions']>[],
connection: Connection,
options: SignAndSendTransactionOptions = {}
): Promise<(TransactionSignature | WalletSignAndSendAllTransactionsError)[]> {
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 new WalletSignAndSendAllTransactionsError(result.reason);
});
}

abstract signTransaction<T extends TransactionOrVersionedTransaction<this['supportedTransactionVersions']>>(
transaction: T
): Promise<T>;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"dependencies": {
"@solana-mobile/wallet-adapter-mobile": "^2.0.0",
"@solana/wallet-adapter-base": "workspace:^",
"@solana/wallet-standard-wallet-adapter-react": "^1.1.0"
"@solana/wallet-standard-wallet-adapter-react": "^1.1.2"
},
"devDependencies": {
"@solana/web3.js": "^1.77.3",
Expand Down
17 changes: 10 additions & 7 deletions packages/core/react/src/WalletProviderBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,16 @@ export function WalletProviderBase({
[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);
},
// Sign and send multiple transactions using the provided connection if the wallet supports it
const signAndSendAllTransactions: SignerWalletAdapterProps['signAndSendAllTransactions'] | undefined = useMemo(
() =>
adapter && 'signAndSendAllTransactions' in adapter
? 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);
}
: undefined,
[adapter, connected]
);

Expand Down
2 changes: 1 addition & 1 deletion packages/core/react/src/useWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export interface WalletContextState {
disconnect(): Promise<void>;

signAndSendTransaction: WalletAdapterProps['signAndSendTransaction'];
signAndSendAllTransactions: WalletAdapterProps['signAndSendAllTransactions'];
signAndSendAllTransactions: SignerWalletAdapterProps['signAndSendAllTransactions'] | undefined;
signTransaction: SignerWalletAdapterProps['signTransaction'] | undefined;
signAllTransactions: SignerWalletAdapterProps['signAllTransactions'] | undefined;
signMessage: MessageSignerWalletAdapterProps['signMessage'] | undefined;
Expand Down
4 changes: 2 additions & 2 deletions packages/starter/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
"@solana/wallet-adapter-react": "workspace:^",
"@solana/wallet-adapter-react-ui": "workspace:^",
"@solana/wallet-adapter-wallets": "workspace:^",
"@solana/wallet-standard-features": "^1.1.0",
"@solana/wallet-standard-util": "^1.1.0",
"@solana/wallet-standard-features": "^1.2.0",
"@solana/wallet-standard-util": "^1.1.1",
"@solana/web3.js": "^1.77.3",
"antd": "^4.24.10",
"bs58": "^4.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Button } from '@mui/material';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js';
import type { FC } from 'react';
import React, { useCallback } from 'react';
import { useNotify } from './notify';
import { WalletSignAndSendAllTransactionsError } from '@solana/wallet-adapter-base';

export const SignAndSendAllTransactions: FC = () => {
const { connection } = useConnection();
const { publicKey, signAndSendAllTransactions } = useWallet();
const notify = useNotify();

const onClick = useCallback(async () => {
try {
if (!publicKey) throw new Error('Wallet not connected!');
if (!signAndSendAllTransactions) throw new Error('Wallet does not support signAndSendAllTransactions!');

const {
context: { slot: minContextSlot },
value: { blockhash, lastValidBlockHeight },
} = await connection.getLatestBlockhashAndContext();

const transactions = [];
for (let i = 0; i < 5; i++) {
const transaction = new Transaction({
feePayer: publicKey,
recentBlockhash: blockhash,
}).add(
new TransactionInstruction({
data: Buffer.from(`transaction number ${i}`),
keys: [],
programId: new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'),
})
);
transactions.push(transaction);
}

const results = await signAndSendAllTransactions(transactions, connection, { minContextSlot });

// Confirm all transactions
(async () => {
await Promise.all(
results.map(async (result) => {
if (result instanceof WalletSignAndSendAllTransactionsError) {
// If the result is an instance of WalletSignAndSendAllTransactionsError, notify about the error
notify('error', `Transaction failed: ${result.message}`);
} else {
try {
await connection.confirmTransaction({
blockhash,
lastValidBlockHeight,
signature: result,
});
notify('success', 'Transaction successful!', result);
} catch (error: any) {
notify('error', `Transaction failed! ${error?.message}`, result);
}
}
})
);
})();
} catch (error: any) {
notify('error', `Transactions failed! ${error?.message}`);
}
}, [publicKey, connection, signAndSendAllTransactions, notify]);

return (
<Button variant="contained" color="secondary" onClick={onClick} disabled={!publicKey}>
Sign and Send All Transactions (devnet)
</Button>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { FC } from 'react';
import React, { useCallback } from 'react';
import { useNotify } from './notify';

export const SendLegacyTransaction: FC = () => {
export const SignAndSendLegacyTransaction: FC = () => {
const { connection } = useConnection();
const { publicKey, signAndSendTransaction, wallet } = useWallet();
const notify = useNotify();
Expand Down Expand Up @@ -55,7 +55,7 @@ export const SendLegacyTransaction: FC = () => {
onClick={onClick}
disabled={!publicKey || !supportedTransactionVersions?.has('legacy')}
>
Send Legacy Transaction (devnet)
Sign and Send Legacy Transaction (devnet)
</Button>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { FC } from 'react';
import React, { useCallback } from 'react';
import { useNotify } from './notify';

export const SendTransaction: FC = () => {
export const SignAndSendTransaction: FC = () => {
const { connection } = useConnection();
const { publicKey, signAndSendTransaction } = useWallet();
const notify = useNotify();
Expand Down Expand Up @@ -44,7 +44,7 @@ export const SendTransaction: FC = () => {

return (
<Button variant="contained" color="secondary" onClick={onClick} disabled={!publicKey}>
Send Transaction (devnet)
Sign and Send Transaction (devnet)
</Button>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { FC } from 'react';
import React, { useCallback } from 'react';
import { useNotify } from './notify';

export const SendV0Transaction: FC = () => {
export const SignAndSendV0Transaction: FC = () => {
const { connection } = useConnection();
const { publicKey, signAndSendTransaction, wallet } = useWallet();
const notify = useNotify();
Expand Down Expand Up @@ -68,7 +68,7 @@ export const SendV0Transaction: FC = () => {
onClick={onClick}
disabled={!publicKey || !supportedTransactionVersions?.has(0)}
>
Send V0 Transaction using Address Lookup Table (devnet)
Sign and Send V0 Transaction using Address Lookup Table (devnet)
</Button>
);
};
32 changes: 22 additions & 10 deletions packages/starter/example/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,24 @@ const ReactUIWalletModalButtonDynamic = dynamic(
const RequestAirdropDynamic = dynamic(async () => (await import('../components/RequestAirdrop')).RequestAirdrop, {
ssr: false,
});
const SendLegacyTransactionDynamic = dynamic(
async () => (await import('../components/SendLegacyTransaction')).SendLegacyTransaction,
const SignAndSendLegacyTransactionDynamic = dynamic(
async () => (await import('../components/SignAndSendLegacyTransaction')).SignAndSendLegacyTransaction,
{ ssr: false }
);
const SendTransactionDynamic = dynamic(async () => (await import('../components/SendTransaction')).SendTransaction, {
ssr: false,
});
const SendV0TransactionDynamic = dynamic(
async () => (await import('../components/SendV0Transaction')).SendV0Transaction,
const SignAndSendTransactionDynamic = dynamic(
async () => (await import('../components/SignAndSendTransaction')).SignAndSendTransaction,
{
ssr: false,
}
);
const SignAndSendAllTransactionsDynamic = dynamic(
async () => (await import('../components/SignAndSendAllTransactions')).SignAndSendAllTransactions,
{
ssr: false,
}
);
const SignAndSendV0TransactionDynamic = dynamic(
async () => (await import('../components/SignAndSendV0Transaction')).SignAndSendV0Transaction,
{ ssr: false }
);
const SignInDynamic = dynamic(async () => (await import('../components/SignIn')).SignIn, { ssr: false });
Expand Down Expand Up @@ -191,13 +200,16 @@ const Index: NextPage = () => {
<TableRow>
<TableCell></TableCell>
<TableCell>
<SendTransactionDynamic />
<SignAndSendTransactionDynamic />
</TableCell>
<TableCell>
<SignAndSendAllTransactionsDynamic />
</TableCell>
<TableCell>
<SendLegacyTransactionDynamic />
<SignAndSendLegacyTransactionDynamic />
</TableCell>
<TableCell>
<SendV0TransactionDynamic />
<SignAndSendV0TransactionDynamic />
</TableCell>
<TableCell></TableCell>
</TableRow>
Expand Down
Loading
Loading