Files
FrenoCorp/node_modules/@solana/wallet-standard-wallet-adapter-base/src/wallet.ts
Michael Freno 7c684a42cc 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>
2026-04-25 00:08:01 -04:00

458 lines
16 KiB
TypeScript

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;
}