import type { SolanaSignAndSendTransactionFeature, SolanaSignAndSendTransactionMethod, SolanaSignAndSendTransactionOutput, SolanaSignTransactionFeature, SolanaSignTransactionMethod, SolanaSignTransactionOutput, } from '@solana/wallet-standard-features'; import { Transaction, VersionedTransaction } from '@solana/web3.js'; import type { Wallet } from '@wallet-standard/base'; import type { ConnectFeature, ConnectMethod, DisconnectFeature, DisconnectMethod, EventsFeature, EventsListeners, EventsNames, EventsOnMethod, SignMessageFeature, SignMessageMethod, SignMessageOutput, } from '@wallet-standard/features'; import bs58 from 'bs58'; import { GilderWalletAccount } from './account.js'; import { icon } from './icon.js'; import type { SolanaChain } from './solana.js'; import { isSolanaChain, SOLANA_CHAINS } from './solana.js'; import { bytesEqual } from './util.js'; import type { Gilder } from './window.js'; export type GilderFeature = { 'gilder:': { gilder: Gilder; }; }; export class GilderWallet implements Wallet { readonly #listeners: { [E in EventsNames]?: EventsListeners[E][] } = {}; readonly #version = '1.0.0' as const; readonly #name = 'Gilder' as const; readonly #icon = icon; #account: GilderWalletAccount | null = null; readonly #gilder: Gilder; get version() { return this.#version; } get name() { return this.#name; } get icon() { return this.#icon; } get chains() { return SOLANA_CHAINS.slice(); } get features(): ConnectFeature & DisconnectFeature & EventsFeature & SolanaSignAndSendTransactionFeature & SolanaSignTransactionFeature & SignMessageFeature & GilderFeature { return { 'standard:connect': { version: '1.0.0', connect: this.#connect, }, 'standard:disconnect': { version: '1.0.0', disconnect: this.#disconnect, }, 'standard:events': { version: '1.0.0', on: this.#on, }, 'solana:signAndSendTransaction': { version: '1.0.0', supportedTransactionVersions: ['legacy', 0], signAndSendTransaction: this.#signAndSendTransaction, }, 'solana:signTransaction': { version: '1.0.0', supportedTransactionVersions: ['legacy', 0], signTransaction: this.#signTransaction, }, 'standard:signMessage': { version: '1.0.0', signMessage: this.#signMessage, }, 'gilder:': { gilder: this.#gilder, }, }; } get accounts() { return this.#account ? [this.#account] : []; } constructor(gilder: Gilder) { if (new.target === GilderWallet) { Object.freeze(this); } this.#gilder = gilder; gilder.on('connect', this.#connected, this); gilder.on('disconnect', this.#disconnected, this); gilder.on('accountChanged', this.#reconnected, this); this.#connected(); } #on: EventsOnMethod = (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: EventsListeners[E]): void { this.#listeners[event] = this.#listeners[event]?.filter((existingListener) => listener !== existingListener); } #connected = () => { const address = this.#gilder.publicKey?.toBase58(); if (address) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const publicKey = this.#gilder.publicKey!.toBytes(); const account = this.#account; if (!account || account.address !== address || !bytesEqual(account.publicKey, publicKey)) { this.#account = new GilderWalletAccount({ address, publicKey }); this.#emit('change', { accounts: this.accounts }); } } }; #disconnected = () => { if (this.#account) { this.#account = null; this.#emit('change', { accounts: this.accounts }); } }; #reconnected = () => { if (this.#gilder.publicKey) { this.#connected(); } else { this.#disconnected(); } }; #connect: ConnectMethod = async ({ silent } = {}) => { if (!this.#account) { await this.#gilder.connect(silent ? { onlyIfTrusted: true } : undefined); } this.#connected(); return { accounts: this.accounts }; }; #disconnect: DisconnectMethod = async () => { await this.#gilder.disconnect(); }; #signAndSendTransaction: SolanaSignAndSendTransactionMethod = async (...inputs) => { if (!this.#account) throw new Error('not connected'); const outputs: SolanaSignAndSendTransactionOutput[] = []; if (inputs.length === 1) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { transaction, account, chain, options } = inputs[0]!; const { minContextSlot, preflightCommitment, skipPreflight, maxRetries } = options || {}; if (account !== this.#account) throw new Error('invalid account'); if (!isSolanaChain(chain)) throw new Error('invalid chain'); const { signature } = await this.#gilder.signAndSendTransaction( VersionedTransaction.deserialize(transaction), { preflightCommitment, minContextSlot, maxRetries, skipPreflight, } ); outputs.push({ signature: bs58.decode(signature) }); } else if (inputs.length > 1) { for (const input of inputs) { outputs.push(...(await this.#signAndSendTransaction(input))); } } return outputs; }; #signTransaction: SolanaSignTransactionMethod = async (...inputs) => { if (!this.#account) throw new Error('not connected'); const outputs: SolanaSignTransactionOutput[] = []; if (inputs.length === 1) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { transaction, account, chain } = inputs[0]!; if (account !== this.#account) throw new Error('invalid account'); if (chain && !isSolanaChain(chain)) throw new Error('invalid chain'); const signedTransaction = await this.#gilder.signTransaction(VersionedTransaction.deserialize(transaction)); outputs.push({ signedTransaction: signedTransaction.serialize() }); } else if (inputs.length > 1) { let chain: SolanaChain | undefined = undefined; for (const input of inputs) { if (input.account !== this.#account) throw new Error('invalid account'); if (input.chain) { if (!isSolanaChain(input.chain)) throw new Error('invalid chain'); if (chain) { if (input.chain !== chain) throw new Error('conflicting chain'); } else { chain = input.chain; } } } const transactions = inputs.map(({ transaction }) => Transaction.from(transaction)); const signedTransactions = await this.#gilder.signAllTransactions(transactions); outputs.push( ...signedTransactions.map((signedTransaction) => ({ signedTransaction: signedTransaction.serialize() })) ); } return outputs; }; #signMessage: SignMessageMethod = async (...inputs) => { if (!this.#account) throw new Error('not connected'); const outputs: SignMessageOutput[] = []; if (inputs.length === 1) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { message, account } = inputs[0]!; if (account !== this.#account) throw new Error('invalid account'); const { signature } = await this.#gilder.signMessage(message); outputs.push({ signedMessage: message, signature }); } else if (inputs.length > 1) { for (const input of inputs) { outputs.push(...(await this.#signMessage(input))); } } return outputs; }; }