From 58b93b5f8839bbbb0340ff7ac77f03617325c506 Mon Sep 17 00:00:00 2001 From: Eugene Chybisov Date: Mon, 28 Oct 2024 15:18:15 +0100 Subject: [PATCH] feat: add okx wallet --- packages/client/src/connectors/okx.ts | 204 ++++++++++++++++++++++++++ packages/client/src/index.ts | 1 + 2 files changed, 205 insertions(+) create mode 100644 packages/client/src/connectors/okx.ts diff --git a/packages/client/src/connectors/okx.ts b/packages/client/src/connectors/okx.ts new file mode 100644 index 0000000..7e7493f --- /dev/null +++ b/packages/client/src/connectors/okx.ts @@ -0,0 +1,204 @@ +import type { SignPsbtParameters, UTXOWalletProvider } from '@bigmi/core' +import { + type Address, + MethodNotSupportedRpcError, + UserRejectedRequestError, + withRetry, +} from 'viem' +import { ProviderNotFoundError, createConnector } from 'wagmi' +import type { UTXOConnectorParameters } from './types.js' + +export type OKXBitcoinEventMap = { + accountsChanged(accounts: Address[]): void +} + +export type OKXBitcoinEvents = { + addListener( + event: TEvent, + listener: OKXBitcoinEventMap[TEvent] + ): void + removeListener( + event: TEvent, + listener: OKXBitcoinEventMap[TEvent] + ): void +} + +type OKXConnectorProperties = { + getAccounts(): Promise + onAccountsChanged(accounts: Address[]): void + getInternalProvider(): Promise +} & UTXOWalletProvider + +type OKXBitcoinProvider = { + requestAccounts(): Promise + getAccounts(): Promise + signPsbt( + psbtHex: string, + options: { + toSignInputs: { + index: number + address: string + sighashTypes?: number[] + }[] + autoFinalized?: boolean + } + ): Promise +} & OKXBitcoinEvents + +okx.type = 'UTXO' as const +export function okx(parameters: UTXOConnectorParameters = {}) { + const { chainId, shimDisconnect = true } = parameters + let accountsChanged: ((accounts: Address[]) => void) | undefined + return createConnector< + UTXOWalletProvider | undefined, + OKXConnectorProperties + >((config) => ({ + id: 'com.okex.wallet.bitcoin', + name: 'OKX Wallet', + type: okx.type, + icon: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBmaWxsPSIjMDAwIiBkPSJNMCAwaDMydjMySDB6Ii8+CiAgICA8cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTIuNiA3SDcuNGEuNC40IDAgMCAwLS40LjR2NS4yYzAgLjIyLjE3OS40LjQuNGg1LjJhLjQuNCAwIDAgMCAuNC0uNFY3LjRhLjQuNCAwIDAgMC0uNC0uNE0xOC42MDIgMTMuMDAyaC01LjJhLjQuNCAwIDAgMC0uNC40djUuMmMwIC4yMi4xNzkuNC40LjRoNS4yYS40LjQgMCAwIDAgLjQtLjR2LTUuMmEuNC40IDAgMCAwLS40LS40TTE5LjQgN2g1LjJhLjQuNCAwIDAgMSAuNC40djUuMmEuNC40IDAgMCAxLS40LjRoLTUuMmEuNC40IDAgMCAxLS40LS40VjcuNGEuNC40IDAgMCAxIC40LS40TTEyLjYgMTlINy40YS40LjQgMCAwIDAtLjQuNHY1LjJjMCAuMjIuMTc5LjQuNC40aDUuMmEuNC40IDAgMCAwIC40LS40di01LjJhLjQuNCAwIDAgMC0uNC0uNE0xOS40IDE5aDUuMmEuNC40IDAgMCAxIC40LjR2NS4yYS40LjQgMCAwIDEtLjQuNGgtNS4yYS40LjQgMCAwIDEtLjQtLjR2LTUuMmEuNC40IDAgMCAxIC40LS40Ii8+Cjwvc3ZnPgo=', + async setup() { + // + }, + async getInternalProvider() { + if (typeof window === 'undefined') { + return + } + if ('okxwallet' in window) { + const anyWindow: any = window + const internalProvider = anyWindow.okxwallet?.bitcoin + + if (internalProvider?.isOkxWallet) { + return internalProvider + } + } + }, + async getProvider() { + const internalProvider = await this.getInternalProvider() + if (!internalProvider) { + return + } + const provider = { + request: this.request.bind(internalProvider), + } + return provider + }, + async request(this: OKXBitcoinProvider, { method, params }): Promise { + switch (method) { + case 'signPsbt': { + const { psbt, ...options } = params as SignPsbtParameters + const toSignInputs = options.inputsToSign.flatMap( + ({ sigHash, address, signingIndexes }) => + signingIndexes.map((index) => ({ + index, + address, + sighashTypes: sigHash !== undefined ? [sigHash] : undefined, + })) + ) + const signedPsbt = await this.signPsbt(psbt, { + toSignInputs, + autoFinalized: options.finalize, + }) + return signedPsbt + } + default: + throw new MethodNotSupportedRpcError( + new Error(MethodNotSupportedRpcError.name), + { + method, + } + ) + } + }, + async connect() { + const provider = await this.getInternalProvider() + if (!provider) { + throw new ProviderNotFoundError() + } + try { + const accounts = await provider.requestAccounts() + const chainId = await this.getChainId() + + if (!accountsChanged) { + accountsChanged = this.onAccountsChanged.bind(this) + provider.addListener('accountsChanged', accountsChanged) + } + + // 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({ + name: UserRejectedRequestError.name, + message: error.message, + }) + } + }, + async disconnect() { + const provider = await this.getInternalProvider() + + if (accountsChanged) { + provider?.removeListener('accountsChanged', accountsChanged) + accountsChanged = 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.getAccounts() + return accounts as Address[] + }, + async getChainId() { + return chainId! + }, + async isAuthorized() { + try { + const isDisconnected = + shimDisconnect && + // If shim exists in storage, connector is disconnected + (await config.storage?.getItem(`${this.id}.disconnected`)) + if (isDisconnected) { + return false + } + const accounts = await withRetry(() => this.getAccounts()) + return !!accounts.length + } catch { + return false + } + }, + 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') + }, + })) +} diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 47a4b7a..d41aa7a 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -5,6 +5,7 @@ export { getConnectorClient } from './actions/getConnectorClient.js' export { reconnect } from './actions/reconnect.js' export { watchAccount } from './actions/watchAccount.js' export { ctrl } from './connectors/ctrl.js' +export { okx } from './connectors/okx.js' export type { CtrlBitcoinEventMap, CtrlBitcoinEvents,