UNPKG

@bigmi/client

Version:

Reactive primitives for Bitcoin apps.

226 lines (203 loc) 6.53 kB
import { type SignPsbtParameters, base64ToHex, hexToBase64 } from '@bigmi/core' import type { UTXOWalletProvider } from './types.js' import { type Address, MethodNotSupportedRpcError, UserRejectedRequestError, } from '@bigmi/core' import { ProviderNotFoundError } from '../errors/connectors.js' import { createConnector } from '../factories/createConnector.js' import type { ProviderRequestParams, UTXOConnectorParameters } from './types.js' export type DynamicWalletConnectorEventMap = { accountChange(props: { accounts: string[] }): void } export type DynamicWalletConnectorEvents = { addListener<TEvent extends keyof DynamicWalletConnectorEventMap>( event: TEvent, listener: DynamicWalletConnectorEventMap[TEvent] ): void removeListener<TEvent extends keyof DynamicWalletConnectorEventMap>( event: TEvent, listener: DynamicWalletConnectorEventMap[TEvent] ): void } export type DynamicWalletConnector = { providerId: string name: string id: string getAddress(): string _metadata: { icon?: string } } & DynamicWalletConnectorEvents type BitcoinAddress = { address: string type: 'ordinals' | 'payment' publicKey: string } export type BitcoinSignPsbtRequestSignature = { address: string signingIndexes: number[] | undefined disableAddressValidation?: boolean } type BitcoinSignPsbtRequest = { allowedSighash: number[] unsignedPsbtBase64: string signature?: BitcoinSignPsbtRequestSignature[] } type BitcoinSignPsbtResponse = { signedPsbt: string } type DynamicBitcoinWallet = { connector: DynamicWalletConnector additionalAddresses: BitcoinAddress[] address: string isAuthenticated: boolean signPsbt( parameters: BitcoinSignPsbtRequest ): Promise<BitcoinSignPsbtResponse | undefined> } type DynamicConnectorProperties = { getAccounts(): Promise<readonly Address[]> onAccountsChanged(accounts: Address[]): void getInternalProvider(): Promise<DynamicWalletConnector> } & UTXOWalletProvider type DynamicConnectorParameters = { wallet: DynamicBitcoinWallet } & UTXOConnectorParameters dynamic.type = 'UTXO' as const export function dynamic(parameters: DynamicConnectorParameters) { const { chainId, shimDisconnect = true, wallet } = parameters let accountChanged: ((accounts: string[]) => void) | undefined return createConnector< UTXOWalletProvider | undefined, DynamicConnectorProperties >((config) => ({ id: wallet.connector.providerId, name: wallet.connector.name, type: dynamic.type, icon: wallet.connector._metadata?.icon, emitter: config.emitter, async isAuthorized() { return wallet.isAuthenticated }, async request( this: DynamicBitcoinWallet, { method, params }: ProviderRequestParams ): Promise<any> { switch (method) { case 'signPsbt': { try { const { psbt, ...options } = params as SignPsbtParameters const allowedSighash: number[] = options.inputsToSign.map((input) => Number(input.sigHash) ) const psbtBase64 = hexToBase64(psbt) const response = await wallet.signPsbt({ allowedSighash, unsignedPsbtBase64: psbtBase64, signature: options.inputsToSign, }) if (!response) { throw new Error('Error signing the transaction') } const { signedPsbt } = response const signedPsbtHex = base64ToHex(signedPsbt) return signedPsbtHex } catch (error: any) { throw new UserRejectedRequestError(error.message) } } default: throw new MethodNotSupportedRpcError() } }, async setup() { // }, async getProvider() { const internalProvider = await this.getInternalProvider() if (!internalProvider) { throw new ProviderNotFoundError() } const provider = { request: this.request.bind(internalProvider), } return provider }, async connect() { if (!wallet.connector) { throw new Error('DynamicWalletConnector not defined') } try { const accounts = await this.getAccounts() const chainId = await this.getChainId() if (!accountChanged) { accountChanged = this.onAccountsChanged.bind(this) wallet.connector.addListener('accountChange', ({ accounts }) => accountChanged?.(accounts) ) } // Remove disconnected shim if it exists if (shimDisconnect) { await Promise.all([ config.storage?.setItem(`${this.id}.connected`, true), config.storage?.removeItem(`${this.id}.disconnected`), ]) } return { accounts, chainId } } catch (error: any) { throw new UserRejectedRequestError(error.message) } }, async disconnect() { const provider = wallet.connector if (accountChanged) { provider.removeListener('accountChange', ({ accounts }) => accountChanged?.(accounts) ) accountChanged = undefined } if (shimDisconnect) { await Promise.all([ config.storage?.setItem(`${this.id}.disconnected`, true), config.storage?.removeItem(`${this.id}.connected`), ]) } }, async getAccounts() { const paymentAdress = wallet.additionalAddresses.find( (wallet) => wallet.type === 'payment' ) if (!paymentAdress) { throw new Error('Please connect a wallet with a segwit address') } return [paymentAdress.address] as Address[] }, async getChainId() { return chainId! }, async getInternalProvider() { return wallet.connector }, async onAccountsChanged(accounts) { if (accounts.length === 0) { this.onDisconnect() } else { config.emitter.emit('change', { accounts: accounts as Address[], }) } }, onChainChanged(chain) { const chainId = Number(chain) config.emitter.emit('change', { chainId }) }, async onDisconnect(_error) { // No need to remove `${this.id}.disconnected` from storage because `onDisconnect` is typically // only called when the wallet is disconnected through the wallet's interface, meaning the wallet // actually disconnected and we don't need to simulate it. config.emitter.emit('disconnect') }, })) }