@bigmi/client
Version:
Reactive primitives for Bitcoin apps.
265 lines (246 loc) • 7.36 kB
text/typescript
import type { Account, Address, SignPsbtParameters } from '@bigmi/core'
import {
base64ToHex,
getAddressChainId,
getAddressInfo,
hexToBase64,
MethodNotSupportedRpcError,
ProviderNotFoundError,
UserRejectedRequestError,
withTimeout,
} from '@bigmi/core'
import { ConnectorChainIdDetectionError } from '../errors/connectors.js'
import { createConnector } from '../factories/createConnector.js'
import type { CreateConnectorFn } from '../types/connector.js'
import type {
ProviderRequestParams,
UTXOConnectorParameters,
UTXOWalletProvider,
} from './types.js'
type ReownConnectorProperties = {
getAccounts(): Promise<readonly Account[]>
onAccountsChanged(accounts: Address[]): void
getInternalProvider(): Promise<InternalReownProvider | undefined>
} & UTXOWalletProvider
type InternalReownProvider = {
id: string
name: string
imageUrl?: string
connector: BitcoinConnector
address: Address | undefined
}
export declare enum AddressPurpose {
Ordinal = 'ordinal',
Payment = 'payment',
Stacks = 'stx',
}
type BitcoinConnector = {
connect(): Promise<string>
getAccountAddresses(): Promise<
Array<{
address: string
publicKey?: string
purpose: AddressPurpose
}>
>
signPSBT(params: {
psbt: string
signInputs: Array<{
address: string
index: number
sighashTypes: number[]
}>
broadcast?: boolean
}): Promise<{ psbt: string; txid?: string }>
}
export type ReownWalletInfo = {
name?: string
icon?: string
}
export type ReownConnectorParameters = {
connector: BitcoinConnector
address?: Address
walletInfo?: ReownWalletInfo
} & UTXOConnectorParameters
export function reown(
parameters: ReownConnectorParameters
): CreateConnectorFn<UTXOWalletProvider | undefined, ReownConnectorProperties> {
const {
chainId,
shimDisconnect = true,
connector,
address,
walletInfo,
} = parameters
// Generate connector id and name from wallet info
const id = walletInfo?.name?.toLowerCase().replace(/\s+/g, '-') || 'reown'
const name = walletInfo?.name || 'Reown Bitcoin Wallet'
const imageUrl = walletInfo?.icon
return createConnector<
UTXOWalletProvider | undefined,
ReownConnectorProperties
>((config) => ({
id,
name,
type: reown.type,
icon: imageUrl,
async setup() {
//
},
async getInternalProvider() {
// Create internal provider that wraps the BitcoinConnector
const internalProvider: InternalReownProvider = {
id,
name,
imageUrl,
connector,
address,
}
return internalProvider
},
async getProvider() {
const provider = await this.getInternalProvider()
if (!provider) {
return
}
const walletProvider = {
request: this.request.bind(provider),
}
return walletProvider
},
async request(
this: InternalReownProvider,
{ method, params }: ProviderRequestParams
): Promise<any> {
switch (method) {
case 'signPsbt': {
const { psbt, ...options } = params as SignPsbtParameters
const signInputs = options.inputsToSign.flatMap(
({ address, signingIndexes, sigHash }) =>
signingIndexes.map((index) => ({
address,
index,
sighashTypes: sigHash !== undefined ? [sigHash] : [],
}))
)
const psbtBase64 = hexToBase64(psbt)
const result = await this.connector.signPSBT({
psbt: psbtBase64,
signInputs,
broadcast: false,
})
const signedPsbtHex = base64ToHex(result.psbt)
return signedPsbtHex
}
default:
throw new MethodNotSupportedRpcError(method)
}
},
async connect() {
const provider = await this.getInternalProvider()
if (!provider) {
throw new ProviderNotFoundError()
}
try {
await provider.connector.connect()
const accounts = await this.getAccounts()
const detectedChainId = getAddressChainId(accounts[0].address)
// 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: chainId ?? detectedChainId }
} catch (error: any) {
throw new UserRejectedRequestError(error.message)
}
},
async disconnect() {
// 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 createAccount = (addr: string, publicKey = ''): Account => {
const { type, purpose } = getAddressInfo(addr)
return { address: addr, addressType: type, publicKey, purpose }
}
try {
const accounts = await withTimeout(
() => provider.connector.getAccountAddresses(),
{ timeout: 1000 }
)
const paymentAccount = accounts.find((acc) => acc.purpose === 'payment')
if (paymentAccount) {
return [
createAccount(
paymentAccount.address,
paymentAccount.publicKey ?? ''
),
]
}
} catch {
// getAccountAddresses not supported or timed out
}
// fallback to provided address if available
if (provider.address) {
return [createAccount(provider.address)]
}
throw new ProviderNotFoundError()
},
async getChainId() {
if (chainId) {
return chainId
}
const accounts = await this.getAccounts()
if (accounts.length === 0) {
throw new ConnectorChainIdDetectionError({ connector: this.name })
}
return getAddressChainId(accounts[0].address)
},
async isAuthorized() {
try {
const isConnected =
shimDisconnect &&
// Check storage to see if a connection exists already
Boolean(await config.storage?.getItem(`${this.id}.connected`))
return isConnected
} catch {
return false
}
},
async onAccountsChanged(accounts) {
if (accounts.length === 0) {
this.onDisconnect()
} else {
const newAccounts = await this.getAccounts()
config.emitter.emit('change', {
accounts: newAccounts,
})
}
},
onChainChanged(chainId) {
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')
},
}))
}
export declare namespace reown {
export var type: 'UTXO'
}
reown.type = 'UTXO' as const