import type { Account, AddressPurpose, SignPsbtParameters } from '@bigmi/core' import { base64ToHex, ChainId, hexToBase64, MethodNotSupportedRpcError, ProviderNotFoundError, UserRejectedRequestError, } from '@bigmi/core' import { createConnector } from '../factories/createConnector.js' import type { CreateConnectorFn } from '../types/connector.js' import { debounce } from '../utils/debounce.js' import type { ProviderRequestParams, UTXOConnectorParameters, UTXOWalletProvider, } from './types.js' export type XverseBitcoinNetwork = 'Mainnet' | 'Testnet' | 'Testnet4' | 'Signet' export type XverseStacksNetwork = 'Mainnet' | 'Testnet' export type XverseNetworkChangeEventParams = { type: 'network_change' bitcoin: { name: XverseBitcoinNetwork } stacks: { name: XverseStacksNetwork } addresses: Account[] } export type XverseBitcoinEventMap = { accountChange(accounts: Account[]): void networkChange(event: XverseNetworkChangeEventParams): void } export type XverseBitcoinEvents = { addListener( event: TEvent, listener: XverseBitcoinEventMap[TEvent] ): void removeListener?( event: TEvent, listener: XverseBitcoinEventMap[TEvent] ): void } type XverseConnectorProperties = { getAccounts(): Promise onAccountsChanged(accounts: Account[]): void getInternalProvider(): Promise } & UTXOWalletProvider type Error = { code: number; message: string } // Define the shape of the request parameters interface GetAccountsRequest { purposes: AddressPurpose[] } interface GetAccountsResponse { result?: { addresses: Account[] } error?: Error } interface RequestPermissionsResponse { result?: boolean error?: Error } interface GetNetworkResponse { result?: { bitcoin: { name: XverseBitcoinNetwork } stacks: { name: XverseStacksNetwork } } error?: Error } type XverseBitcoinProvider = { request( method: 'signPsbt', options: { psbt: string allowedSignHash: number signInputs: Record broadcast: boolean } ): Promise request( method: 'getAccounts' | 'getAddresses', options: GetAccountsRequest ): Promise request( method: 'wallet_requestPermissions' | 'wallet_renouncePermissions' ): Promise request(method: 'wallet_getNetwork'): Promise } & XverseBitcoinEvents type ChainChangeHandler = | (((event: XverseNetworkChangeEventParams) => void) & { cancel?: () => void }) | undefined export function xverse( parameters: UTXOConnectorParameters = {} ): CreateConnectorFn< UTXOWalletProvider | undefined, XverseConnectorProperties > { const XverseBitcoinChainIdMap: Record = { Mainnet: ChainId.BITCOIN_MAINNET, Testnet: ChainId.BITCOIN_TESTNET, Testnet4: ChainId.BITCOIN_TESTNET4, Signet: ChainId.BITCOIN_SIGNET, } const { shimDisconnect = true } = parameters let accountChange: ((accounts: Account[]) => void) | undefined let chainChange: ChainChangeHandler return createConnector< UTXOWalletProvider | undefined, XverseConnectorProperties >((config) => ({ id: 'XverseProviders.BitcoinProvider', name: 'Xverse Wallet', type: xverse.type, icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIj48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGZpbGw9IiMxNzE3MTciIGQ9Ik0wIDBoNjAwdjYwMEgweiIvPjxwYXRoIGZpbGw9IiNGRkYiIGZpbGwtcnVsZT0ibm9uemVybyIgZD0iTTQ0MCA0MzUuNHYtNTFjMC0yLS44LTMuOS0yLjItNS4zTDIyMCAxNjIuMmE3LjYgNy42IDAgMCAwLTUuNC0yLjJoLTUxLjFjLTIuNSAwLTQuNiAyLTQuNiA0LjZ2NDcuM2MwIDIgLjggNCAyLjIgNS40bDc4LjIgNzcuOGE0LjYgNC42IDAgMCAxIDAgNi41bC03OSA3OC43Yy0xIC45LTEuNCAyLTEuNCAzLjJ2NTJjMCAyLjQgMiA0LjUgNC42IDQuNUgyNDljMi42IDAgNC42LTIgNC42LTQuNlY0MDVjMC0xLjIuNS0yLjQgMS40LTMuM2w0Mi40LTQyLjJhNC42IDQuNiAwIDAgMSA2LjQgMGw3OC43IDc4LjRhNy42IDcuNiAwIDAgMCA1LjQgMi4yaDQ3LjVjMi41IDAgNC42LTIgNC42LTQuNloiLz48cGF0aCBmaWxsPSIjRUU3QTMwIiBmaWxsLXJ1bGU9Im5vbnplcm8iIGQ9Ik0zMjUuNiAyMjcuMmg0Mi44YzIuNiAwIDQuNiAyLjEgNC42IDQuNnY0Mi42YzAgNCA1IDYuMSA4IDMuMmw1OC43LTU4LjVjLjgtLjggMS4zLTIgMS4zLTMuMnYtNTEuMmMwLTIuNi0yLTQuNi00LjYtNC42TDM4NCAxNjBjLTEuMiAwLTIuNC41LTMuMyAxLjNsLTU4LjQgNTguMWE0LjYgNC42IDAgMCAwIDMuMiA3LjhaIi8+PC9nPjwvc3ZnPg==', async setup() { // }, async getInternalProvider() { if (typeof window === 'undefined') { return undefined } if ('XverseProviders' in window) { const anyWindow: any = window const provider = anyWindow.XverseProviders.BitcoinProvider return provider } }, async getProvider() { const internalProvider = await this.getInternalProvider() if (!internalProvider) { return } const provider = { request: this.request.bind(internalProvider), } return provider }, async request( this: XverseBitcoinProvider | any, { method, params }: ProviderRequestParams ): Promise { switch (method) { case 'signPsbt': { const { psbt, ...options } = params as SignPsbtParameters const psbtBase64 = hexToBase64(psbt) const signInputs = options.inputsToSign.reduce( (signInputs, input) => { if (!signInputs[input.address]) { signInputs[input.address] = [] } signInputs[input.address].push(...input.signingIndexes) return signInputs }, {} as Record ) const signedPsbt = await this.request('signPsbt', { psbt: psbtBase64, allowedSignHash: 1, // Default to Transaction.SIGHASH_ALL - 1 signInputs: signInputs, broadcast: options.finalize, }) if (signedPsbt?.error) { throw signedPsbt?.error } return base64ToHex(signedPsbt?.result?.psbt) } default: throw new MethodNotSupportedRpcError(method) } }, async connect({ isReconnecting } = {}) { const provider = await this.getInternalProvider() if (!provider) { throw new ProviderNotFoundError() } if (!isReconnecting) { const connected = await provider.request('wallet_requestPermissions') if (connected.error) { throw new UserRejectedRequestError(connected.error.message) } } const accounts = await this.getAccounts() const chainId = await this.getChainId() if (!accountChange) { accountChange = this.onAccountsChanged.bind(this) provider.addListener('accountChange', accountChange) } if (!chainChange) { // debounced because xverse wallet calls the event handler twice in rapid succession chainChange = debounce( (event: XverseNetworkChangeEventParams) => this.onChainChanged(XverseBitcoinChainIdMap[event.bitcoin.name]), 300 ) provider.addListener('networkChange', chainChange) } if (shimDisconnect) { // Remove disconnected shim if it exists await Promise.all([ config.storage?.setItem(`${this.id}.connected`, true), config.storage?.removeItem(`${this.id}.disconnected`), ]) } return { accounts, chainId } }, async disconnect() { const provider = await this.getInternalProvider() if (!provider) { throw new ProviderNotFoundError() } if (accountChange) { provider.removeListener?.('accountChange', accountChange) accountChange = undefined } if (chainChange) { provider.removeListener?.('networkChange', chainChange) chainChange.cancel?.() // check for existing call and cancel chainChange = undefined } // Add shim signalling connector is disconnected if (shimDisconnect) { await Promise.all([ config.storage?.setItem(`${this.id}.disconnected`, true), config.storage?.removeItem(`${this.id}.connected`), ]) } }, async getAccounts() { const provider = await this.getInternalProvider() if (!provider) { throw new ProviderNotFoundError() } const accounts = await provider.request('getAddresses', { purposes: ['payment'], }) if (!accounts.result) { throw new UserRejectedRequestError(accounts.error?.message) } return accounts.result.addresses }, async getChainId() { const provider = await this.getInternalProvider() if (!provider) { throw new ProviderNotFoundError() } const network = await provider.request('wallet_getNetwork') if (!network.result) { throw new UserRejectedRequestError( network.error?.message ?? 'Unknown error' ) } return XverseBitcoinChainIdMap[network.result.bitcoin.name] }, async isAuthorized() { try { const isConnected = shimDisconnect && // If shim exists in storage, connector is disconnected Boolean(await config.storage?.getItem(`${this.id}.connected`)) return isConnected } catch { return false } }, async onAccountsChanged() { const { accounts } = await this.connect() config.emitter.emit('change', { accounts, }) }, async onChainChanged() { const { accounts, chainId } = await this.connect() config.emitter.emit('change', { chainId, accounts }) }, async onDisconnect(_error) { config.emitter.emit('disconnect') }, })) } export declare namespace xverse { export var type: 'UTXO' } xverse.type = 'UTXO' as const