FRE-600: Fix code review blockers

- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-25 00:08:01 -04:00
parent 65b552bb08
commit 7c684a42cc
48450 changed files with 5679671 additions and 383 deletions

View File

@@ -0,0 +1,479 @@
import {
BaseWalletAdapter,
isVersionedTransaction,
type SendTransactionOptions,
type StandardWalletAdapter as StandardWalletAdapterType,
type SupportedTransactionVersions,
WalletAccountError,
type WalletAdapterCompatibleStandardWallet,
WalletConfigError,
WalletConnectionError,
WalletDisconnectedError,
WalletDisconnectionError,
WalletError,
type WalletName,
WalletNotConnectedError,
WalletNotReadyError,
WalletPublicKeyError,
WalletReadyState,
WalletSendTransactionError,
WalletSignInError,
WalletSignMessageError,
WalletSignTransactionError,
} from '@solana/wallet-adapter-base';
import {
SolanaSignAndSendTransaction,
type SolanaSignAndSendTransactionFeature,
SolanaSignIn,
type SolanaSignInInput,
type SolanaSignInOutput,
SolanaSignMessage,
SolanaSignTransaction,
type SolanaSignTransactionFeature,
} from '@solana/wallet-standard-features';
import { getChainForEndpoint, getCommitment } from '@solana/wallet-standard-util';
import type { Connection, TransactionSignature } from '@solana/web3.js';
import { PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js';
import type { WalletAccount } from '@wallet-standard/base';
import {
StandardConnect,
type StandardConnectInput,
StandardDisconnect,
StandardEvents,
type StandardEventsListeners,
} from '@wallet-standard/features';
import { arraysEqual } from '@wallet-standard/wallet';
import bs58 from 'bs58';
/** TODO: docs */
export interface StandardWalletAdapterConfig {
wallet: WalletAdapterCompatibleStandardWallet;
}
/** TODO: docs */
export class StandardWalletAdapter extends BaseWalletAdapter implements StandardWalletAdapterType {
#account: WalletAccount | null;
#publicKey: PublicKey | null;
#connecting: boolean;
#disconnecting: boolean;
#off: (() => void) | null;
#supportedTransactionVersions: SupportedTransactionVersions;
readonly #wallet: WalletAdapterCompatibleStandardWallet;
readonly #readyState: WalletReadyState =
typeof window === 'undefined' || typeof document === 'undefined'
? WalletReadyState.Unsupported
: WalletReadyState.Installed;
get name() {
return this.#wallet.name as WalletName;
}
get url() {
return 'https://github.com/solana-labs/wallet-standard';
}
get icon() {
return this.#wallet.icon;
}
get readyState() {
return this.#readyState;
}
get publicKey() {
return this.#publicKey;
}
get connecting() {
return this.#connecting;
}
get supportedTransactionVersions() {
return this.#supportedTransactionVersions;
}
get wallet(): WalletAdapterCompatibleStandardWallet {
return this.#wallet;
}
get standard() {
return true as const;
}
constructor({ wallet }: StandardWalletAdapterConfig) {
super();
this.#wallet = wallet;
this.#account = null;
this.#publicKey = null;
this.#connecting = false;
this.#disconnecting = false;
this.#off = this.#wallet.features[StandardEvents].on('change', this.#changed);
this.#reset();
}
destroy(): void {
this.#account = null;
this.#publicKey = null;
this.#connecting = false;
this.#disconnecting = false;
const off = this.#off;
if (off) {
this.#off = null;
off();
}
}
async autoConnect(): Promise<void> {
return this.#connect({ silent: true });
}
async connect(): Promise<void> {
return this.#connect();
}
async #connect(input?: StandardConnectInput): Promise<void> {
try {
if (this.connected || this.connecting) return;
if (this.#readyState !== WalletReadyState.Installed) throw new WalletNotReadyError();
this.#connecting = true;
if (!this.#wallet.accounts.length) {
try {
await this.#wallet.features[StandardConnect].connect(input);
} catch (error: any) {
throw new WalletConnectionError(error?.message, error);
}
}
const account = this.#wallet.accounts[0];
if (!account) throw new WalletAccountError();
this.#connected(account);
} catch (error: any) {
this.emit('error', error);
throw error;
} finally {
this.#connecting = false;
}
}
async disconnect(): Promise<void> {
if (StandardDisconnect in this.#wallet.features) {
try {
this.#disconnecting = true;
await this.#wallet.features[StandardDisconnect].disconnect();
} catch (error: any) {
this.emit('error', new WalletDisconnectionError(error?.message, error));
} finally {
this.#disconnecting = false;
}
}
this.#disconnected();
}
#connected(account: WalletAccount) {
let publicKey: PublicKey;
try {
// Use account.address instead of account.publicKey since address could be a PDA
publicKey = new PublicKey(account.address);
} catch (error: any) {
throw new WalletPublicKeyError(error?.message, error);
}
this.#account = account;
this.#publicKey = publicKey;
this.#reset();
this.emit('connect', publicKey);
}
#disconnected(): void {
this.#account = null;
this.#publicKey = null;
this.#reset();
this.emit('disconnect');
}
#reset() {
const supportedTransactionVersions =
SolanaSignAndSendTransaction in this.#wallet.features
? this.#wallet.features[SolanaSignAndSendTransaction].supportedTransactionVersions
: this.#wallet.features[SolanaSignTransaction].supportedTransactionVersions;
this.#supportedTransactionVersions = arraysEqual(supportedTransactionVersions, ['legacy'])
? null
: new Set(supportedTransactionVersions);
if (SolanaSignTransaction in this.#wallet.features && this.#account?.features.includes(SolanaSignTransaction)) {
this.signTransaction = this.#signTransaction;
this.signAllTransactions = this.#signAllTransactions;
} else {
delete this.signTransaction;
delete this.signAllTransactions;
}
if (SolanaSignMessage in this.#wallet.features && this.#account?.features.includes(SolanaSignMessage)) {
this.signMessage = this.#signMessage;
} else {
delete this.signMessage;
}
if (SolanaSignIn in this.#wallet.features) {
this.signIn = this.#signIn;
} else {
delete this.signIn;
}
}
#changed: StandardEventsListeners['change'] = (properties) => {
// If accounts have changed on the wallet, reflect this on the adapter.
if ('accounts' in properties) {
const account = this.#wallet.accounts[0];
// If the adapter isn't connected, or is disconnecting, or the first account hasn't changed, do nothing.
if (this.#account && !this.#disconnecting && account !== this.#account) {
// If there's a connected account, connect the adapter. Otherwise, disconnect it.
if (account) {
// Connect the adapter.
this.#connected(account);
} else {
// Emit an error because the wallet spontaneously disconnected.
this.emit('error', new WalletDisconnectedError());
// Disconnect the adapter.
this.#disconnected();
}
}
}
// After reflecting account changes, if features have changed on the wallet, reflect this on the adapter.
if ('features' in properties) {
this.#reset();
}
};
async sendTransaction<T extends Transaction | VersionedTransaction>(
transaction: T,
connection: Connection,
options: SendTransactionOptions = {}
): Promise<TransactionSignature> {
try {
const account = this.#account;
if (!account) throw new WalletNotConnectedError();
let feature: typeof SolanaSignAndSendTransaction | typeof SolanaSignTransaction;
if (SolanaSignAndSendTransaction in this.#wallet.features) {
if (account.features.includes(SolanaSignAndSendTransaction)) {
feature = SolanaSignAndSendTransaction;
} else if (
SolanaSignTransaction in this.#wallet.features &&
account.features.includes(SolanaSignTransaction)
) {
feature = SolanaSignTransaction;
} else {
throw new WalletAccountError();
}
} else if (SolanaSignTransaction in this.#wallet.features) {
if (!account.features.includes(SolanaSignTransaction)) throw new WalletAccountError();
feature = SolanaSignTransaction;
} else {
throw new WalletConfigError();
}
const chain = getChainForEndpoint(connection.rpcEndpoint);
if (!account.chains.includes(chain)) throw new WalletSendTransactionError();
try {
const { signers, ...sendOptions } = options;
let serializedTransaction: Uint8Array;
if (isVersionedTransaction(transaction)) {
signers?.length && transaction.sign(signers);
serializedTransaction = transaction.serialize();
} else {
transaction = (await this.prepareTransaction(transaction, connection, sendOptions)) as T;
signers?.length && (transaction as Transaction).partialSign(...signers);
serializedTransaction = new Uint8Array(
(transaction as Transaction).serialize({
requireAllSignatures: false,
verifySignatures: false,
})
);
}
if (feature === SolanaSignAndSendTransaction) {
const [output] = await (this.#wallet.features as SolanaSignAndSendTransactionFeature)[
SolanaSignAndSendTransaction
].signAndSendTransaction({
account,
chain,
transaction: serializedTransaction,
options: {
preflightCommitment: getCommitment(
sendOptions.preflightCommitment || connection.commitment
),
skipPreflight: sendOptions.skipPreflight,
maxRetries: sendOptions.maxRetries,
minContextSlot: sendOptions.minContextSlot,
},
});
return bs58.encode(output!.signature);
} else {
const [output] = await (this.#wallet.features as SolanaSignTransactionFeature)[
SolanaSignTransaction
].signTransaction({
account,
chain,
transaction: serializedTransaction,
options: {
preflightCommitment: getCommitment(
sendOptions.preflightCommitment || connection.commitment
),
minContextSlot: sendOptions.minContextSlot,
},
});
return await connection.sendRawTransaction(output!.signedTransaction, {
...sendOptions,
preflightCommitment: getCommitment(sendOptions.preflightCommitment || connection.commitment),
});
}
} catch (error: any) {
if (error instanceof WalletError) throw error;
throw new WalletSendTransactionError(error?.message, error);
}
} catch (error: any) {
this.emit('error', error);
throw error;
}
}
signTransaction: (<T extends Transaction | VersionedTransaction>(transaction: T) => Promise<T>) | undefined;
async #signTransaction<T extends Transaction | VersionedTransaction>(transaction: T): Promise<T> {
try {
const account = this.#account;
if (!account) throw new WalletNotConnectedError();
if (!(SolanaSignTransaction in this.#wallet.features)) throw new WalletConfigError();
if (!account.features.includes(SolanaSignTransaction)) throw new WalletAccountError();
try {
const signedTransactions = await this.#wallet.features[SolanaSignTransaction].signTransaction({
account,
transaction: isVersionedTransaction(transaction)
? transaction.serialize()
: new Uint8Array(
transaction.serialize({
requireAllSignatures: false,
verifySignatures: false,
})
),
});
const serializedTransaction = signedTransactions[0]!.signedTransaction;
return (
isVersionedTransaction(transaction)
? VersionedTransaction.deserialize(serializedTransaction)
: Transaction.from(serializedTransaction)
) as T;
} catch (error: any) {
if (error instanceof WalletError) throw error;
throw new WalletSignTransactionError(error?.message, error);
}
} catch (error: any) {
this.emit('error', error);
throw error;
}
}
signAllTransactions: (<T extends Transaction | VersionedTransaction>(transaction: T[]) => Promise<T[]>) | undefined;
async #signAllTransactions<T extends Transaction | VersionedTransaction>(transactions: T[]): Promise<T[]> {
try {
const account = this.#account;
if (!account) throw new WalletNotConnectedError();
if (!(SolanaSignTransaction in this.#wallet.features)) throw new WalletConfigError();
if (!account.features.includes(SolanaSignTransaction)) throw new WalletAccountError();
try {
const signedTransactions = await this.#wallet.features[SolanaSignTransaction].signTransaction(
...transactions.map((transaction) => ({
account,
transaction: isVersionedTransaction(transaction)
? transaction.serialize()
: new Uint8Array(
transaction.serialize({
requireAllSignatures: false,
verifySignatures: false,
})
),
}))
);
return transactions.map((transaction, index) => {
const signedTransaction = signedTransactions[index]!.signedTransaction;
return (
isVersionedTransaction(transaction)
? VersionedTransaction.deserialize(signedTransaction)
: Transaction.from(signedTransaction)
) as T;
});
} catch (error: any) {
throw new WalletSignTransactionError(error?.message, error);
}
} catch (error: any) {
this.emit('error', error);
throw error;
}
}
signMessage: ((message: Uint8Array) => Promise<Uint8Array>) | undefined;
async #signMessage(message: Uint8Array): Promise<Uint8Array> {
try {
const account = this.#account;
if (!account) throw new WalletNotConnectedError();
if (!(SolanaSignMessage in this.#wallet.features)) throw new WalletConfigError();
if (!account.features.includes(SolanaSignMessage)) throw new WalletAccountError();
try {
const signedMessages = await this.#wallet.features[SolanaSignMessage].signMessage({
account,
message,
});
return signedMessages[0]!.signature;
} catch (error: any) {
throw new WalletSignMessageError(error?.message, error);
}
} catch (error: any) {
this.emit('error', error);
throw error;
}
}
signIn: ((input?: SolanaSignInInput) => Promise<SolanaSignInOutput>) | undefined;
async #signIn(input: SolanaSignInInput = {}): Promise<SolanaSignInOutput> {
try {
if (!(SolanaSignIn in this.#wallet.features)) throw new WalletConfigError();
let output: SolanaSignInOutput | undefined;
try {
[output] = await this.#wallet.features[SolanaSignIn].signIn(input);
} catch (error: any) {
throw new WalletSignInError(error?.message, error);
}
if (!output) throw new WalletSignInError();
this.#connected(output.account);
return output;
} catch (error: any) {
this.emit('error', error);
throw error;
}
}
}

View File

@@ -0,0 +1,3 @@
export * from './adapter.js';
export * from './types.js';
export * from './wallet.js';

View File

@@ -0,0 +1,26 @@
import {
isWalletAdapterCompatibleStandardWallet,
type StandardWalletAdapter,
type WalletAdapterCompatibleStandardWallet,
} from '@solana/wallet-adapter-base';
/**
* @deprecated Use `StandardWalletAdapter` from `@solana/wallet-adapter-base` instead.
*
* @group Deprecated
*/
export type StandardAdapter = StandardWalletAdapter;
/**
* @deprecated Use `WalletAdapterCompatibleStandardWallet` from `@solana/wallet-adapter-base` instead.
*
* @group Deprecated
*/
export type WalletAdapterCompatibleWallet = WalletAdapterCompatibleStandardWallet;
/**
* @deprecated Use `isWalletAdapterCompatibleStandardWallet` from `@solana/wallet-adapter-base` instead.
*
* @group Deprecated
*/
export const isWalletAdapterCompatibleWallet = isWalletAdapterCompatibleStandardWallet;

View File

@@ -0,0 +1,457 @@
import { type Adapter, isVersionedTransaction, WalletReadyState } from '@solana/wallet-adapter-base';
import { isSolanaChain, type SolanaChain } from '@solana/wallet-standard-chains';
import {
SolanaSignAndSendTransaction,
type SolanaSignAndSendTransactionFeature,
type SolanaSignAndSendTransactionMethod,
type SolanaSignAndSendTransactionOutput,
SolanaSignIn,
type SolanaSignInFeature,
type SolanaSignInMethod,
type SolanaSignInOutput,
SolanaSignMessage,
type SolanaSignMessageFeature,
type SolanaSignMessageMethod,
type SolanaSignMessageOutput,
SolanaSignTransaction,
type SolanaSignTransactionFeature,
type SolanaSignTransactionMethod,
type SolanaSignTransactionOutput,
type SolanaTransactionVersion,
} from '@solana/wallet-standard-features';
import { getEndpointForChain } from '@solana/wallet-standard-util';
import { Connection, Transaction, VersionedTransaction } from '@solana/web3.js';
import { getWallets } from '@wallet-standard/app';
import type { Wallet, WalletIcon } from '@wallet-standard/base';
import {
StandardConnect,
type StandardConnectFeature,
type StandardConnectMethod,
StandardDisconnect,
type StandardDisconnectFeature,
type StandardDisconnectMethod,
StandardEvents,
type StandardEventsFeature,
type StandardEventsListeners,
type StandardEventsNames,
type StandardEventsOnMethod,
} from '@wallet-standard/features';
import { arraysEqual, bytesEqual, ReadonlyWalletAccount } from '@wallet-standard/wallet';
import bs58 from 'bs58';
/** TODO: docs */
export class SolanaWalletAdapterWalletAccount extends ReadonlyWalletAccount {
// eslint-disable-next-line no-unused-private-class-members
readonly #adapter: Adapter;
constructor({
adapter,
address,
publicKey,
chains,
}: {
adapter: Adapter;
address: string;
publicKey: Uint8Array;
chains: readonly SolanaChain[];
}) {
const features: (keyof (SolanaSignAndSendTransactionFeature &
SolanaSignTransactionFeature &
SolanaSignMessageFeature &
SolanaSignInFeature))[] = [SolanaSignAndSendTransaction];
if ('signTransaction' in adapter) {
features.push(SolanaSignTransaction);
}
if ('signMessage' in adapter) {
features.push(SolanaSignMessage);
}
if ('signIn' in adapter) {
features.push(SolanaSignIn);
}
super({ address, publicKey, chains, features });
if (new.target === SolanaWalletAdapterWalletAccount) {
Object.freeze(this);
}
this.#adapter = adapter;
}
}
/** TODO: docs */
export class SolanaWalletAdapterWallet implements Wallet {
readonly #listeners: {
[E in StandardEventsNames]?: StandardEventsListeners[E][];
} = {};
readonly #adapter: Adapter;
readonly #supportedTransactionVersions: readonly SolanaTransactionVersion[];
readonly #chain: SolanaChain;
readonly #endpoint: string | undefined;
#account: SolanaWalletAdapterWalletAccount | undefined;
get version() {
return '1.0.0' as const;
}
get name() {
return this.#adapter.name;
}
get icon() {
return this.#adapter.icon as WalletIcon;
}
get chains() {
return [this.#chain];
}
get features(): StandardConnectFeature &
StandardDisconnectFeature &
StandardEventsFeature &
SolanaSignAndSendTransactionFeature &
Partial<SolanaSignTransactionFeature & SolanaSignMessageFeature & SolanaSignInFeature> {
const features: StandardConnectFeature &
StandardDisconnectFeature &
StandardEventsFeature &
SolanaSignAndSendTransactionFeature = {
[StandardConnect]: {
version: '1.0.0',
connect: this.#connect,
},
[StandardDisconnect]: {
version: '1.0.0',
disconnect: this.#disconnect,
},
[StandardEvents]: {
version: '1.0.0',
on: this.#on,
},
[SolanaSignAndSendTransaction]: {
version: '1.0.0',
supportedTransactionVersions: this.#supportedTransactionVersions,
signAndSendTransaction: this.#signAndSendTransaction,
},
};
let signTransactionFeature: SolanaSignTransactionFeature | undefined;
if ('signTransaction' in this.#adapter) {
signTransactionFeature = {
[SolanaSignTransaction]: {
version: '1.0.0',
supportedTransactionVersions: this.#supportedTransactionVersions,
signTransaction: this.#signTransaction,
},
};
}
let signMessageFeature: SolanaSignMessageFeature | undefined;
if ('signMessage' in this.#adapter) {
signMessageFeature = {
[SolanaSignMessage]: {
version: '1.0.0',
signMessage: this.#signMessage,
},
};
}
let signInFeature: SolanaSignInFeature | undefined;
if ('signIn' in this.#adapter) {
signInFeature = {
[SolanaSignIn]: {
version: '1.0.0',
signIn: this.#signIn,
},
};
}
return { ...features, ...signTransactionFeature, ...signMessageFeature };
}
get accounts() {
return this.#account ? [this.#account] : [];
}
get endpoint() {
return this.#endpoint;
}
constructor(adapter: Adapter, chain: SolanaChain, endpoint?: string) {
if (new.target === SolanaWalletAdapterWallet) {
Object.freeze(this);
}
const supportedTransactionVersions = [...(adapter.supportedTransactionVersions || ['legacy'])];
if (!supportedTransactionVersions.length) {
supportedTransactionVersions.push('legacy');
}
this.#adapter = adapter;
this.#supportedTransactionVersions = supportedTransactionVersions;
this.#chain = chain;
this.#endpoint = endpoint;
adapter.on('connect', this.#connected, this);
adapter.on('disconnect', this.#disconnected, this);
this.#connected();
}
destroy(): void {
this.#adapter.off('connect', this.#connected, this);
this.#adapter.off('disconnect', this.#disconnected, this);
}
#connected(): void {
const publicKey = this.#adapter.publicKey?.toBytes();
if (publicKey) {
const address = this.#adapter.publicKey!.toBase58();
const account = this.#account;
if (
!account ||
account.address !== address ||
account.chains.includes(this.#chain) ||
!bytesEqual(account.publicKey, publicKey)
) {
this.#account = new SolanaWalletAdapterWalletAccount({
adapter: this.#adapter,
address,
publicKey,
chains: [this.#chain],
});
this.#emit('change', { accounts: this.accounts });
}
}
}
#disconnected(): void {
if (this.#account) {
this.#account = undefined;
this.#emit('change', { accounts: this.accounts });
}
}
#connect: StandardConnectMethod = async ({ silent } = {}) => {
if (!silent && !this.#adapter.connected) {
await this.#adapter.connect();
}
this.#connected();
return { accounts: this.accounts };
};
#disconnect: StandardDisconnectMethod = async () => {
await this.#adapter.disconnect();
};
#on: StandardEventsOnMethod = (event, listener) => {
this.#listeners[event]?.push(listener) || (this.#listeners[event] = [listener]);
return (): void => this.#off(event, listener);
};
#emit<E extends StandardEventsNames>(event: E, ...args: Parameters<StandardEventsListeners[E]>): void {
// eslint-disable-next-line prefer-spread
this.#listeners[event]?.forEach((listener) => listener.apply(null, args));
}
#off<E extends StandardEventsNames>(event: E, listener: StandardEventsListeners[E]): void {
this.#listeners[event] = this.#listeners[event]?.filter((existingListener) => listener !== existingListener);
}
#deserializeTransaction(serializedTransaction: Uint8Array): Transaction | VersionedTransaction {
const transaction = VersionedTransaction.deserialize(serializedTransaction);
if (!this.#supportedTransactionVersions.includes(transaction.version))
throw new Error('unsupported transaction version');
if (transaction.version === 'legacy' && arraysEqual(this.#supportedTransactionVersions, ['legacy']))
return Transaction.from(serializedTransaction);
return transaction;
}
#signAndSendTransaction: SolanaSignAndSendTransactionMethod = async (...inputs) => {
const outputs: SolanaSignAndSendTransactionOutput[] = [];
if (inputs.length === 1) {
const input = inputs[0]!;
if (input.account !== this.#account) throw new Error('invalid account');
if (!isSolanaChain(input.chain)) throw new Error('invalid chain');
const transaction = this.#deserializeTransaction(input.transaction);
const { commitment, preflightCommitment, skipPreflight, maxRetries, minContextSlot } = input.options || {};
const endpoint = getEndpointForChain(input.chain, this.#endpoint);
const connection = new Connection(endpoint, commitment || 'confirmed');
const latestBlockhash = commitment
? await connection.getLatestBlockhash({
commitment: preflightCommitment || commitment,
minContextSlot,
})
: undefined;
const signature = await this.#adapter.sendTransaction(transaction, connection, {
preflightCommitment,
skipPreflight,
maxRetries,
minContextSlot,
});
if (latestBlockhash) {
await connection.confirmTransaction(
{
...latestBlockhash,
signature,
},
commitment || 'confirmed'
);
}
outputs.push({ signature: bs58.decode(signature) });
} else if (inputs.length > 1) {
// Adapters have no `sendAllTransactions` method, so just sign and send each transaction in serial.
for (const input of inputs) {
outputs.push(...(await this.#signAndSendTransaction(input)));
}
}
return outputs;
};
#signTransaction: SolanaSignTransactionMethod = async (...inputs) => {
if (!('signTransaction' in this.#adapter)) throw new Error('signTransaction not implemented by adapter');
const outputs: SolanaSignTransactionOutput[] = [];
if (inputs.length === 1) {
const input = inputs[0]!;
if (input.account !== this.#account) throw new Error('invalid account');
if (input.chain && !isSolanaChain(input.chain)) throw new Error('invalid chain');
const transaction = this.#deserializeTransaction(input.transaction);
const signedTransaction = await this.#adapter.signTransaction(transaction);
const serializedTransaction = isVersionedTransaction(signedTransaction)
? signedTransaction.serialize()
: new Uint8Array(
signedTransaction.serialize({
requireAllSignatures: false,
verifySignatures: false,
})
);
outputs.push({ signedTransaction: serializedTransaction });
} else if (inputs.length > 1) {
for (const input of inputs) {
if (input.account !== this.#account) throw new Error('invalid account');
if (input.chain && !isSolanaChain(input.chain)) throw new Error('invalid chain');
}
const transactions = inputs.map(({ transaction }) => this.#deserializeTransaction(transaction));
const signedTransactions = await this.#adapter.signAllTransactions(transactions);
outputs.push(
...signedTransactions.map((signedTransaction) => {
const serializedTransaction = isVersionedTransaction(signedTransaction)
? signedTransaction.serialize()
: new Uint8Array(
signedTransaction.serialize({
requireAllSignatures: false,
verifySignatures: false,
})
);
return { signedTransaction: serializedTransaction };
})
);
}
return outputs;
};
#signMessage: SolanaSignMessageMethod = async (...inputs) => {
if (!('signMessage' in this.#adapter)) throw new Error('signMessage not implemented by adapter');
const outputs: SolanaSignMessageOutput[] = [];
if (inputs.length === 1) {
const input = inputs[0]!;
if (input.account !== this.#account) throw new Error('invalid account');
const signature = await this.#adapter.signMessage(input.message);
outputs.push({ signedMessage: input.message, signature });
} else if (inputs.length > 1) {
// Adapters have no `signAllMessages` method, so just sign each message in serial.
for (const input of inputs) {
outputs.push(...(await this.#signMessage(input)));
}
}
return outputs;
};
#signIn: SolanaSignInMethod = async (...inputs) => {
if (!('signIn' in this.#adapter)) throw new Error('signIn not implemented by adapter');
if (inputs.length > 1) {
// Adapters don't support `signIn` with multiple inputs, so just sign in with each input in serial.
const outputs: SolanaSignInOutput[] = [];
for (const input of inputs) {
outputs.push(await this.#adapter.signIn(input));
}
return outputs;
} else {
return [await this.#adapter.signIn(inputs[0])];
}
};
}
/** TODO: docs */
export function registerWalletAdapter(
adapter: Adapter,
chain: SolanaChain,
endpoint?: string,
match: (wallet: Wallet) => boolean = (wallet) => wallet.name === adapter.name
): () => void {
const { register, get, on } = getWallets();
const destructors: (() => void)[] = [];
function destroy(): void {
destructors.forEach((destroy) => destroy());
destructors.length = 0;
}
function setup(): boolean {
// If the adapter is unsupported, or a standard wallet that matches it has already been registered, do nothing.
if (adapter.readyState === WalletReadyState.Unsupported || get().some(match)) return true;
// If the adapter isn't ready, try again later.
const ready =
adapter.readyState === WalletReadyState.Installed || adapter.readyState === WalletReadyState.Loadable;
if (ready) {
const wallet = new SolanaWalletAdapterWallet(adapter, chain, endpoint);
destructors.push(() => wallet.destroy());
// Register the adapter wrapped as a standard wallet, and receive a function to unregister the adapter.
destructors.push(register(wallet));
// Whenever a standard wallet is registered ...
destructors.push(
on('register', (...wallets) => {
// ... check if it matches the adapter.
if (wallets.some(match)) {
// If it does, remove the event listener and unregister the adapter.
destroy();
}
})
);
}
return ready;
}
if (!setup()) {
function listener(): void {
if (setup()) {
adapter.off('readyStateChange', listener);
}
}
adapter.on('readyStateChange', listener);
destructors.push(() => adapter.off('readyStateChange', listener));
}
return destroy;
}