/** * NOTE: This file is forked from https://github.com/anza-xyz/wallet-standard/blob/2c354cf07daa1d440ba9631fcefc5c00b07aa9dd/packages/wallet-adapter/base/src/wallet.ts#L233 * * We needed to slightly modify the auto connect logic, hence the fork and change. * Please see below for the modification */ 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 type { WalletAccount } from "@wallet-standard/base"; import bs58 from "bs58"; function walletAccountsEqual(a?: WalletAccount, b?: WalletAccount): boolean { if (!a && !b) { return true; } if (!a || !b) { return false; } return ( a.address === b.address && bytesEqual(a.publicKey, b.publicKey) && arraysEqual(a.chains, b.chains) && arraysEqual(a.features, b.features) && a.label === b.label && a.icon === b.icon ); } export class SolanaWalletAdapterWalletAccount extends ReadonlyWalletAccount { 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; } } 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 & 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, ...signInFeature, }; } 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) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 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 (!this.#adapter.connected) { // Here is the modification of the connect logic. If it's silent, // we trigger an autoconnect instead (no window being forced open) if (silent) { await this.#adapter.autoConnect(); } else { 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( event: E, ...args: Parameters ): void { // eslint-disable-next-line prefer-spread this.#listeners[event]?.forEach((listener) => listener.apply(null, args)); } #off( 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) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const input = inputs[0]!; if (!walletAccountsEqual(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) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const input = inputs[0]!; if (!walletAccountsEqual(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 (!walletAccountsEqual(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) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const input = inputs[0]!; if (!walletAccountsEqual(input.account, this.#account)) { console.error("input account", input.account); console.error("this 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) { const output = await this.#adapter.signIn(input); outputs.push(output); } return outputs; } else { const output = await this.#adapter.signIn(inputs[0]); return [output]; } }; } 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; } function listener(): void { if (setup()) { adapter.off("readyStateChange", listener); } } if (!setup()) { adapter.on("readyStateChange", listener); destructors.push(() => adapter.off("readyStateChange", listener)); } return destroy; }