Implementing a Bitcoin RPC Adapter
This guide explains how to implement a custom RPC adapter for interacting with Bitcoin nodes or services.
Overview
The BTCRpcAdapter
provides an abstraction layer for Bitcoin-specific operations like UTXO management, balance queries, and transaction broadcasting. You might want to implement a custom adapter when:
- Using a different Bitcoin API provider
- Implementing specialized UTXO selection strategies
Base Class
In order to implement a custom adapter, you need to implement the BTCRpcAdapter
abstract class:
import type { BTCTransaction, BTCInput, BTCOutput } from '@chains/Bitcoin/types'
export abstract class BTCRpcAdapter {
abstract selectUTXOs(
from: string,
targets: BTCOutput[]
): Promise<{ inputs: BTCInput[]; outputs: BTCOutput[] }>
abstract broadcastTransaction(transactionHex: string): Promise<string>
abstract getBalance(address: string): Promise<number>
abstract getTransaction(txid: string): Promise<BTCTransaction>
}
2. Example
There is an example implementation of the BTCRpcAdapter
abstract class in the Mempool
class:
// There is no types for coinselect
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import coinselect from 'coinselect'
import { BTCRpcAdapter } from '@chains/Bitcoin/BTCRpcAdapter/BTCRpcAdapter'
import {
type BTCFeeRecommendation,
type UTXO,
} from '@chains/Bitcoin/BTCRpcAdapter/Mempool/types'
import type { BTCTransaction, BTCInput, BTCOutput } from '@chains/Bitcoin/types'
export class Mempool extends BTCRpcAdapter {
private readonly providerUrl: string
constructor(providerUrl: string) {
super()
this.providerUrl = providerUrl
}
private async fetchFeeRate(confirmationTarget = 6): Promise<number> {
const response = await fetch(`${this.providerUrl}/v1/fees/recommended`)
const data = (await response.json()) as BTCFeeRecommendation
if (confirmationTarget <= 1) {
return data.fastestFee
} else if (confirmationTarget <= 3) {
return data.halfHourFee
} else if (confirmationTarget <= 6) {
return data.hourFee
} else {
return data.economyFee
}
}
private async fetchUTXOs(address: string): Promise<UTXO[]> {
try {
const response = await fetch(
`${this.providerUrl}/address/${address}/utxo`
)
return (await response.json()) as UTXO[]
} catch (error) {
console.error('Failed to fetch UTXOs:', error)
return []
}
}
async selectUTXOs(
from: string,
targets: BTCOutput[],
confirmationTarget = 6
): Promise<{ inputs: BTCInput[]; outputs: BTCOutput[] }> {
const utxos = await this.fetchUTXOs(from)
const feeRate = await this.fetchFeeRate(confirmationTarget)
// Add a small amount to the fee rate to ensure the transaction is confirmed
const ret = coinselect(utxos, targets, Math.ceil(feeRate + 1))
if (!ret.inputs || !ret.outputs) {
throw new Error(
'Invalid transaction: coinselect failed to find a suitable set of inputs and outputs. This could be due to insufficient funds, or no inputs being available that meet the criteria.'
)
}
return {
inputs: ret.inputs,
outputs: ret.outputs,
}
}
async broadcastTransaction(transactionHex: string): Promise<string> {
const response = await fetch(`${this.providerUrl}/tx`, {
method: 'POST',
body: transactionHex,
})
if (response.ok) {
return await response.text()
}
throw new Error(`Failed to broadcast transaction: ${await response.text()}`)
}
async getBalance(address: string): Promise<number> {
const response = await fetch(`${this.providerUrl}/address/${address}`)
const data = (await response.json()) as {
chain_stats: { funded_txo_sum: number; spent_txo_sum: number }
}
return data.chain_stats.funded_txo_sum - data.chain_stats.spent_txo_sum
}
async getTransaction(txid: string): Promise<BTCTransaction> {
const response = await fetch(`${this.providerUrl}/tx/${txid}`)
return (await response.json()) as BTCTransaction
}
}