Skip to content

Commit

Permalink
feat: add okx bitcoin connector (#3408)
Browse files Browse the repository at this point in the history
  • Loading branch information
zoruka authored Dec 9, 2024
1 parent 1968eb8 commit 4f9a11b
Show file tree
Hide file tree
Showing 11 changed files with 559 additions and 396 deletions.
21 changes: 21 additions & 0 deletions .changeset/good-pets-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@reown/appkit-scaffold-ui': patch
'@reown/appkit': patch
'@reown/appkit-core': patch
'@reown/appkit-ui': patch
'@reown/appkit-adapter-ethers': patch
'@reown/appkit-adapter-ethers5': patch
'@reown/appkit-adapter-solana': patch
'@reown/appkit-adapter-wagmi': patch
'@reown/appkit-utils': patch
'@reown/appkit-cdn': patch
'appkit-cli': patch
'@reown/appkit-common': patch
'@reown/appkit-experimental': patch
'@reown/appkit-polyfills': patch
'@reown/appkit-siwe': patch
'@reown/appkit-siwx': patch
'@reown/appkit-wallet': patch
---

Add Bitcoin OKX Wallet connector
1 change: 1 addition & 0 deletions apps/laboratory/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"playwright:debug:multichain-ethers5-siwe": "pnpm playwright:test:multichain-ethers5-siwe --debug"
},
"dependencies": {
"@bitcoinerlab/secp256k1": "1.1.1",
"@chakra-ui/icons": "2.1.1",
"@chakra-ui/react": "2.8.2",
"@emotion/react": "11.11.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function BitcoinSignPSBTTest() {
}

try {
setLoading(true)
const utxos = await BitcoinUtil.getUTXOs(address, caipNetwork.caipNetworkId)
const feeRate = await BitcoinUtil.getFeeRate()

Expand Down
32 changes: 30 additions & 2 deletions apps/laboratory/src/utils/BitcoinUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import type { BitcoinConnector } from '@reown/appkit-adapter-bitcoin'
import type { CaipNetwork, CaipNetworkId } from '@reown/appkit'
import * as networks from '@reown/appkit/networks'
import * as bitcoin from 'bitcoinjs-lib'
import * as bitcoinPSBTUtils from 'bitcoinjs-lib/src/psbt/psbtutils'
import ecc from '@bitcoinerlab/secp256k1'

bitcoin.initEccLib(ecc)

export const BitcoinUtil = {
createSignPSBTParams(params: BitcoinUtil.CreateSignPSBTParams): BitcoinConnector.SignPSBTParams {
const network = this.getBitcoinNetwork(params.network.caipNetworkId)

const payment = this.getPaymentByAddress(params.senderAddress, network)
const psbt = new bitcoin.Psbt({ network })
const payment = bitcoin.payments.p2wpkh({ address: params.senderAddress, network })

if (!payment.output) {
throw new Error('Invalid payment output')
Expand Down Expand Up @@ -113,6 +116,31 @@ export const BitcoinUtil = {

getBitcoinNetwork(networkId: CaipNetworkId): bitcoin.Network {
return this.isTestnet(networkId) ? bitcoin.networks.testnet : bitcoin.networks.bitcoin
},

getPaymentByAddress(
address: string,
network: bitcoin.networks.Network
): bitcoin.payments.Payment {
const output = bitcoin.address.toOutputScript(address, network)

if (bitcoinPSBTUtils.isP2MS(output)) {
return bitcoin.payments.p2ms({ output, network })
} else if (bitcoinPSBTUtils.isP2PK(output)) {
return bitcoin.payments.p2pk({ output, network })
} else if (bitcoinPSBTUtils.isP2PKH(output)) {
return bitcoin.payments.p2pkh({ output, network })
} else if (bitcoinPSBTUtils.isP2WPKH(output)) {
return bitcoin.payments.p2wpkh({ output, network })
} else if (bitcoinPSBTUtils.isP2WSHScript(output)) {
return bitcoin.payments.p2wsh({ output, network })
} else if (bitcoinPSBTUtils.isP2SHScript(output)) {
return bitcoin.payments.p2sh({ output, network })
} else if (bitcoinPSBTUtils.isP2TR(output)) {
return bitcoin.payments.p2tr({ output, network })
}

throw new Error('Unsupported payment type')
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/adapters/bitcoin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@wallet-standard/app": "1.0.1",
"@wallet-standard/base": "1.0.1",
"@walletconnect/universal-provider": "2.17.0",
"bitcoinjs-lib": "6.1.7",
"sats-connect": "3.0.1"
},
"devDependencies": {
Expand Down
15 changes: 14 additions & 1 deletion packages/adapters/bitcoin/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SatsConnectConnector } from './connectors/SatsConnectConnector.js'
import { WalletStandardConnector } from './connectors/WalletStandardConnector.js'
import { WalletConnectProvider } from './utils/WalletConnectProvider.js'
import { LeatherConnector } from './connectors/LeatherConnector.js'
import { OKXConnector } from './connectors/OKXConnector.js'

export class BitcoinAdapter extends AdapterBlueprint<BitcoinConnector> {
private eventsToUnbind: (() => void)[] = []
Expand Down Expand Up @@ -78,6 +79,10 @@ export class BitcoinAdapter extends AdapterBlueprint<BitcoinConnector> {
}
}
override syncConnectors(_options?: AppKitOptions, appKit?: AppKit): void {
function getActiveNetwork() {
return appKit?.getCaipNetwork()
}

WalletStandardConnector.watchWallets({
callback: this.addConnector.bind(this),
requestedChains: this.networks
Expand All @@ -86,7 +91,7 @@ export class BitcoinAdapter extends AdapterBlueprint<BitcoinConnector> {
this.addConnector(
...SatsConnectConnector.getWallets({
requestedChains: this.networks,
getActiveNetwork: () => appKit?.getCaipNetwork()
getActiveNetwork
}).map(connector => {
switch (connector.wallet.id) {
case LeatherConnector.ProviderId:
Expand All @@ -99,6 +104,14 @@ export class BitcoinAdapter extends AdapterBlueprint<BitcoinConnector> {
}
})
)

const okxConnector = OKXConnector.getWallet({
requestedChains: this.networks,
getActiveNetwork
})
if (okxConnector) {
this.addConnector(okxConnector)
}
}

override syncConnection(
Expand Down
176 changes: 176 additions & 0 deletions packages/adapters/bitcoin/src/connectors/OKXConnector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import type { CaipNetwork } from '@reown/appkit-common'
import type { BitcoinConnector } from '../utils/BitcoinConnector.js'
import { ProviderEventEmitter } from '../utils/ProviderEventEmitter.js'
import type { RequestArguments } from '@reown/appkit-core'
import { MethodNotSupportedError } from '../errors/MethodNotSupportedError.js'
import { bitcoin } from '@reown/appkit/networks'
import { UnitsUtil } from '../utils/UnitsUtil.js'

export class OKXConnector extends ProviderEventEmitter implements BitcoinConnector {
public readonly id = 'OKX'
public readonly name = 'OKX Wallet'
public readonly chain = 'bip122'
public readonly type = 'ANNOUNCED'
public readonly imageUrl: string

public readonly provider = this

private readonly wallet: OKXConnector.Wallet
private readonly requestedChains: CaipNetwork[] = []
private readonly getActiveNetwork: () => CaipNetwork | undefined

constructor({
wallet,
requestedChains,
getActiveNetwork,
imageUrl
}: OKXConnector.ConstructorParams) {
super()
this.wallet = wallet
this.requestedChains = requestedChains
this.getActiveNetwork = getActiveNetwork
this.imageUrl = imageUrl
}

public get chains() {
return this.requestedChains.filter(chain => chain.caipNetworkId === bitcoin.caipNetworkId)
}

public async connect(): Promise<string> {
const result = await this.wallet.connect()

this.bindEvents()

return result.address
}

public async disconnect(): Promise<void> {
this.unbindEvents()
await this.wallet.disconnect()
}

public async getAccountAddresses(): Promise<BitcoinConnector.AccountAddress[]> {
const accounts = await this.wallet.getAccounts()

return accounts.map(account => ({
address: account,
purpose: 'payment'
}))
}

public async signMessage(params: BitcoinConnector.SignMessageParams): Promise<string> {
return this.wallet.signMessage(params.message)
}

public async sendTransfer(params: BitcoinConnector.SendTransferParams): Promise<string> {
const network = this.getActiveNetwork()

if (!network) {
throw new Error('No active network available')
}

const from = (await this.wallet.getAccounts())[0]

if (!from) {
throw new Error('No account available')
}

const result = await this.wallet.send({
from,
to: params.recipient,
value: UnitsUtil.parseSatoshis(params.amount, network)
})

return result.txhash
}

public async signPSBT(
params: BitcoinConnector.SignPSBTParams
): Promise<BitcoinConnector.SignPSBTResponse> {
const psbtHex = Buffer.from(params.psbt, 'base64').toString('hex')

const signedPsbtHex = await this.wallet.signPsbt(psbtHex)

let txid: string | undefined = undefined
if (params.broadcast) {
txid = await this.wallet.pushPsbt(signedPsbtHex)
}

return {
psbt: Buffer.from(signedPsbtHex, 'hex').toString('base64'),
txid
}
}

public request<T>(_args: RequestArguments): Promise<T> {
return Promise.reject(new MethodNotSupportedError(this.id, 'request'))
}

private bindEvents(): void {
this.unbindEvents()

this.wallet.on('accountChanged', account => {
if (typeof account === 'object' && account && 'address' in account) {
this.emit('accountsChanged', [account.address])
}
})
this.wallet.on('disconnect', () => {
this.emit('disconnect')
})
}

private unbindEvents(): void {
this.wallet.removeAllListeners()
}

public static getWallet(params: OKXConnector.GetWalletParams): OKXConnector | undefined {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const okxwallet = (window as any)?.okxwallet
const wallet = okxwallet?.bitcoin
/**
* OKX doesn't provide a way to get the image URL specifally for bitcoin
* so we use the icon for cardano as a fallback
*/
const imageUrl = okxwallet?.cardano?.icon || ''

if (wallet) {
return new OKXConnector({ wallet, imageUrl, ...params })
}

return undefined
}
}

export namespace OKXConnector {
export type ConstructorParams = {
wallet: Wallet
requestedChains: CaipNetwork[]
getActiveNetwork: () => CaipNetwork | undefined
imageUrl: string
}

export type Wallet = {
/*
* This interface doesn't include all available methods
* Reference: https://www.okx.com/web3/build/docs/sdks/chains/bitcoin/provider
*/
connect(): Promise<{ address: string; publicKey: string }>
disconnect(): Promise<void>
getAccounts(): Promise<string[]>
signMessage(signStr: string, type?: 'ecdsa' | 'bip322-simple'): Promise<string>
signPsbt(psbtHex: string): Promise<string>
pushPsbt(psbtHex: string): Promise<string>
send(params: {
from: string
to: string
value: string
satBytes?: string
memo?: string
memoPos?: number
}): Promise<{ txhash: string }>
on(event: string, listener: (param?: unknown) => void): void
removeAllListeners(): void
}

export type GetWalletParams = Omit<ConstructorParams, 'wallet' | 'imageUrl'>
}
12 changes: 12 additions & 0 deletions packages/adapters/bitcoin/src/utils/UnitsUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { CaipNetwork } from '@reown/appkit-common'

export const UnitsUtil = {
parseSatoshis(amount: string, network: CaipNetwork): string {
const value = parseFloat(amount) / 10 ** network.nativeCurrency.decimals

// eslint-disable-next-line new-cap
return Intl.NumberFormat('en-US', {
maximumFractionDigits: network.nativeCurrency.decimals
}).format(value)
}
}
Loading

0 comments on commit 4f9a11b

Please sign in to comment.