Skip to content

Commit

Permalink
Merge pull request #558 from jstarry/versioned-tx
Browse files Browse the repository at this point in the history
Add versioned transaction methods to adapter interface
  • Loading branch information
jordaaash authored Sep 15, 2022
2 parents 0400302 + ddf04df commit b109484
Show file tree
Hide file tree
Showing 49 changed files with 329 additions and 206 deletions.
16 changes: 12 additions & 4 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 { Connection, PublicKey, SendOptions, Signer, Transaction, Transacti
import EventEmitter from 'eventemitter3';
import type { WalletError } from './errors.js';
import { WalletNotConnectedError } from './errors.js';
import type { SupportedTransactionVersions, TransactionOrVersionedTransaction } from './types.js';

export { EventEmitter };

Expand All @@ -28,11 +29,13 @@ export interface WalletAdapterProps<Name extends string = string> {
publicKey: PublicKey | null;
connecting: boolean;
connected: boolean;
supportedTransactionVersions: SupportedTransactionVersions;

connect(): Promise<void>;
disconnect(): Promise<void>;

sendTransaction(
transaction: Transaction,
transaction: TransactionOrVersionedTransaction<this['supportedTransactionVersions']>,
connection: Connection,
options?: SendTransactionOptions
): Promise<TransactionSignature>;
Expand Down Expand Up @@ -69,22 +72,27 @@ export enum WalletReadyState {
Unsupported = 'Unsupported',
}

export abstract class BaseWalletAdapter extends EventEmitter<WalletAdapterEvents> implements WalletAdapter {
abstract name: WalletName;
export abstract class BaseWalletAdapter<Name extends string = string>
extends EventEmitter<WalletAdapterEvents>
implements WalletAdapter<Name>
{
abstract name: WalletName<Name>;
abstract url: string;
abstract icon: string;
abstract readyState: WalletReadyState;
abstract publicKey: PublicKey | null;
abstract connecting: boolean;
abstract supportedTransactionVersions: SupportedTransactionVersions;

get connected() {
return !!this.publicKey;
}

abstract connect(): Promise<void>;
abstract disconnect(): Promise<void>;

abstract sendTransaction(
transaction: Transaction,
transaction: TransactionOrVersionedTransaction<this['supportedTransactionVersions']>,
connection: Connection,
options?: SendTransactionOptions
): Promise<TransactionSignature>;
Expand Down
114 changes: 85 additions & 29 deletions packages/core/base/src/signer.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,78 @@
import type { Connection, Transaction, TransactionSignature } from '@solana/web3.js';
import type { SendTransactionOptions, WalletAdapter } from './adapter.js';
import type { Connection, TransactionSignature } from '@solana/web3.js';
import type { SendTransactionOptions, WalletAdapter, WalletAdapterProps } from './adapter.js';
import { BaseWalletAdapter } from './adapter.js';
import { WalletSendTransactionError, WalletSignTransactionError } from './errors.js';
import type { TransactionOrVersionedTransaction } from './types.js';

export interface SignerWalletAdapterProps {
signTransaction(transaction: Transaction): Promise<Transaction>;
signAllTransactions(transaction: Transaction[]): Promise<Transaction[]>;
export interface SignerWalletAdapterProps<Name extends string = string> extends WalletAdapterProps<Name> {
signTransaction<T extends TransactionOrVersionedTransaction<this['supportedTransactionVersions']>>(
transaction: T
): Promise<T>;
signAllTransactions<T extends TransactionOrVersionedTransaction<this['supportedTransactionVersions']>>(
transactions: T[]
): Promise<T[]>;
}

export type SignerWalletAdapter = WalletAdapter & SignerWalletAdapterProps;
export type SignerWalletAdapter<Name extends string = string> = WalletAdapter<Name> & SignerWalletAdapterProps<Name>;

export abstract class BaseSignerWalletAdapter extends BaseWalletAdapter implements SignerWalletAdapter {
export abstract class BaseSignerWalletAdapter<Name extends string = string>
extends BaseWalletAdapter<Name>
implements SignerWalletAdapter<Name>
{
async sendTransaction(
transaction: Transaction,
transaction: TransactionOrVersionedTransaction<this['supportedTransactionVersions']>,
connection: Connection,
options: SendTransactionOptions = {}
): Promise<TransactionSignature> {
let emit = true;
try {
try {
const { signers, ...sendOptions } = options;
if ('message' in transaction) {
if (!this.supportedTransactionVersions)
throw new WalletSendTransactionError(
`Sending versioned transactions isn't supported by this wallet`
);

const { version } = transaction.message;
if (!this.supportedTransactionVersions.has(version))
throw new WalletSendTransactionError(
`Sending transaction version ${version} isn't supported by this wallet`
);

try {
transaction = await this.signTransaction(transaction);

const rawTransaction = transaction.serialize();

return await connection.sendRawTransaction(rawTransaction, options);
} catch (error: any) {
// If the error was thrown by `signTransaction`, rethrow it and don't emit a duplicate event
if (error instanceof WalletSignTransactionError) {
emit = false;
throw error;
}
throw new WalletSendTransactionError(error?.message, error);
}
} else {
try {
const { signers, ...sendOptions } = options;

transaction = await this.prepareTransaction(transaction, connection, sendOptions);
transaction = await this.prepareTransaction(transaction, connection, sendOptions);

signers?.length && transaction.partialSign(...signers);
signers?.length && transaction.partialSign(...signers);

transaction = await this.signTransaction(transaction);
transaction = await this.signTransaction(transaction);

const rawTransaction = transaction.serialize();
const rawTransaction = transaction.serialize();

return await connection.sendRawTransaction(rawTransaction, sendOptions);
} catch (error: any) {
// If the error was thrown by `signTransaction`, rethrow it and don't emit a duplicate event
if (error instanceof WalletSignTransactionError) {
emit = false;
throw error;
return await connection.sendRawTransaction(rawTransaction, sendOptions);
} catch (error: any) {
// If the error was thrown by `signTransaction`, rethrow it and don't emit a duplicate event
if (error instanceof WalletSignTransactionError) {
emit = false;
throw error;
}
throw new WalletSendTransactionError(error?.message, error);
}
throw new WalletSendTransactionError(error?.message, error);
}
} catch (error: any) {
if (emit) {
Expand All @@ -46,26 +82,46 @@ export abstract class BaseSignerWalletAdapter extends BaseWalletAdapter implemen
}
}

abstract signTransaction(transaction: Transaction): Promise<Transaction>;
abstract signTransaction<T extends TransactionOrVersionedTransaction<this['supportedTransactionVersions']>>(
transaction: T
): Promise<T>;

async signAllTransactions<T extends TransactionOrVersionedTransaction<this['supportedTransactionVersions']>>(
transactions: T[]
): Promise<T[]> {
for (const transaction of transactions) {
if ('message' in transaction) {
if (!this.supportedTransactionVersions)
throw new WalletSignTransactionError(
`Signing versioned transactions isn't supported by this wallet`
);

const { version } = transaction.message;
if (!this.supportedTransactionVersions.has(version))
throw new WalletSignTransactionError(
`Signing transaction version ${version} isn't supported by this wallet`
);
}
}

async signAllTransactions(transactions: Transaction[]): Promise<Transaction[]> {
const signedTransactions: Transaction[] = [];
const signedTransactions: T[] = [];
for (const transaction of transactions) {
signedTransactions.push(await this.signTransaction(transaction));
}
return signedTransactions;
}
}

export interface MessageSignerWalletAdapterProps {
export interface MessageSignerWalletAdapterProps<Name extends string = string> extends WalletAdapterProps<Name> {
signMessage(message: Uint8Array): Promise<Uint8Array>;
}

export type MessageSignerWalletAdapter = WalletAdapter & MessageSignerWalletAdapterProps;
export type MessageSignerWalletAdapter<Name extends string = string> = WalletAdapter<Name> &
MessageSignerWalletAdapterProps<Name>;

export abstract class BaseMessageSignerWalletAdapter
extends BaseSignerWalletAdapter
implements MessageSignerWalletAdapter
export abstract class BaseMessageSignerWalletAdapter<Name extends string = string>
extends BaseSignerWalletAdapter<Name>
implements MessageSignerWalletAdapter<Name>
{
abstract signMessage(message: Uint8Array): Promise<Uint8Array>;
}
7 changes: 7 additions & 0 deletions packages/core/base/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Transaction, TransactionVersion, VersionedTransaction } from '@solana/web3.js';
import type { WalletAdapter } from './adapter.js';
import type { MessageSignerWalletAdapter, SignerWalletAdapter } from './signer.js';

Expand All @@ -8,3 +9,9 @@ export enum WalletAdapterNetwork {
Testnet = 'testnet',
Devnet = 'devnet',
}

export type SupportedTransactionVersions = Set<TransactionVersion> | null;

export type TransactionOrVersionedTransaction<S extends SupportedTransactionVersions> = S extends null
? Transaction
: Transaction | VersionedTransaction;
27 changes: 17 additions & 10 deletions packages/core/react/src/WalletProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type { Adapter, SendTransactionOptions, WalletError, WalletName } from '@solana/wallet-adapter-base';
import type {
Adapter,
MessageSignerWalletAdapterProps,
SignerWalletAdapterProps,
WalletAdapterProps,
WalletError,
WalletName,
} from '@solana/wallet-adapter-base';
import { WalletNotConnectedError, WalletNotReadyError, WalletReadyState } from '@solana/wallet-adapter-base';
import type { Connection, PublicKey, Transaction } from '@solana/web3.js';
import type { PublicKey } from '@solana/web3.js';
import type { FC, ReactNode } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { WalletNotSelectedError } from './errors.js';
Expand Down Expand Up @@ -230,8 +237,8 @@ export const WalletProvider: FC<WalletProviderProps> = ({
}, [isDisconnecting, adapter, setName]);

// Send a transaction using the provided connection
const sendTransaction = useCallback(
async (transaction: Transaction, connection: Connection, options?: SendTransactionOptions) => {
const sendTransaction: WalletAdapterProps['sendTransaction'] = useCallback(
async (transaction, connection, options) => {
if (!adapter) throw handleError(new WalletNotSelectedError());
if (!connected) throw handleError(new WalletNotConnectedError());
return await adapter.sendTransaction(transaction, connection, options);
Expand All @@ -240,10 +247,10 @@ export const WalletProvider: FC<WalletProviderProps> = ({
);

// Sign a transaction if the wallet supports it
const signTransaction = useMemo(
const signTransaction: SignerWalletAdapterProps['signTransaction'] | undefined = useMemo(
() =>
adapter && 'signTransaction' in adapter
? async (transaction: Transaction): Promise<Transaction> => {
? async (transaction) => {
if (!connected) throw handleError(new WalletNotConnectedError());
return await adapter.signTransaction(transaction);
}
Expand All @@ -252,10 +259,10 @@ export const WalletProvider: FC<WalletProviderProps> = ({
);

// Sign multiple transactions if the wallet supports it
const signAllTransactions = useMemo(
const signAllTransactions: SignerWalletAdapterProps['signAllTransactions'] | undefined = useMemo(
() =>
adapter && 'signAllTransactions' in adapter
? async (transactions: Transaction[]): Promise<Transaction[]> => {
? async (transactions) => {
if (!connected) throw handleError(new WalletNotConnectedError());
return await adapter.signAllTransactions(transactions);
}
Expand All @@ -264,10 +271,10 @@ export const WalletProvider: FC<WalletProviderProps> = ({
);

// Sign an arbitrary message if the wallet supports it
const signMessage = useMemo(
const signMessage: MessageSignerWalletAdapterProps['signMessage'] | undefined = useMemo(
() =>
adapter && 'signMessage' in adapter
? async (message: Uint8Array): Promise<Uint8Array> => {
? async (message) => {
if (!connected) throw handleError(new WalletNotConnectedError());
return await adapter.signMessage(message);
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/react/src/__tests__/WalletProvider-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ describe('WalletProvider', () => {
});
});
sendTransaction = jest.fn();
supportedTransactionVersions = null;
}
class FooWalletAdapter extends MockWalletAdapter {
name = 'FooWallet' as WalletName<'FooWallet'>;
Expand Down
15 changes: 8 additions & 7 deletions packages/core/react/src/useWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import type {
MessageSignerWalletAdapterProps,
SendTransactionOptions,
SignerWalletAdapterProps,
WalletAdapterProps,
WalletName,
WalletReadyState,
} from '@solana/wallet-adapter-base';
import type { Connection, PublicKey, Transaction, TransactionSignature } from '@solana/web3.js';
import type { Connection, PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js';
import { createContext, useContext } from 'react';

export interface Wallet {
Expand All @@ -26,12 +27,8 @@ export interface WalletContextState {
select(walletName: WalletName): void;
connect(): Promise<void>;
disconnect(): Promise<void>;
sendTransaction(
transaction: Transaction,
connection: Connection,
options?: SendTransactionOptions
): Promise<TransactionSignature>;

sendTransaction: WalletAdapterProps['sendTransaction'];
signTransaction: SignerWalletAdapterProps['signTransaction'] | undefined;
signAllTransactions: SignerWalletAdapterProps['signAllTransactions'] | undefined;
signMessage: MessageSignerWalletAdapterProps['signMessage'] | undefined;
Expand All @@ -53,7 +50,11 @@ const DEFAULT_CONTEXT = {
disconnect() {
return Promise.reject(console.error(constructMissingProviderErrorMessage('get', 'disconnect')));
},
sendTransaction(_transaction: Transaction, _connection: Connection, _options?: SendTransactionOptions) {
sendTransaction(
_transaction: VersionedTransaction | Transaction,
_connection: Connection,
_options?: SendTransactionOptions
) {
return Promise.reject(console.error(constructMissingProviderErrorMessage('get', 'sendTransaction')));
},
signTransaction(_transaction: Transaction) {
Expand Down
9 changes: 5 additions & 4 deletions packages/wallets/alpha/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class AlphaWalletAdapter extends BaseMessageSignerWalletAdapter {
url = 'https://github.com/babilu-online/alpha-wallet';
icon =
'data:image/svg+xml;base64,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';
readonly supportedTransactionVersions = null;

private _connecting: boolean;
private _wallet: AlphaWallet | null;
Expand Down Expand Up @@ -185,13 +186,13 @@ export class AlphaWalletAdapter extends BaseMessageSignerWalletAdapter {
}
}

async signTransaction(transaction: Transaction): Promise<Transaction> {
async signTransaction<T extends Transaction>(transaction: T): Promise<T> {
try {
const wallet = this._wallet;
if (!wallet) throw new WalletNotConnectedError();

try {
return (await wallet.signTransaction(transaction)) || transaction;
return ((await wallet.signTransaction(transaction)) as T) || transaction;
} catch (error: any) {
throw new WalletSignTransactionError(error?.message, error);
}
Expand All @@ -201,13 +202,13 @@ export class AlphaWalletAdapter extends BaseMessageSignerWalletAdapter {
}
}

async signAllTransactions(transactions: Transaction[]): Promise<Transaction[]> {
async signAllTransactions<T extends Transaction>(transactions: T[]): Promise<T[]> {
try {
const wallet = this._wallet;
if (!wallet) throw new WalletNotConnectedError();

try {
return (await wallet.signAllTransactions(transactions)) || transactions;
return ((await wallet.signAllTransactions(transactions)) as T[]) || transactions;
} catch (error: any) {
throw new WalletSignTransactionError(error?.message, error);
}
Expand Down
Loading

0 comments on commit b109484

Please sign in to comment.