Skip to content

Commit

Permalink
Add bitcoind provider
Browse files Browse the repository at this point in the history
  • Loading branch information
mrfelton committed Feb 19, 2024
1 parent 755ac3e commit 1ad73bd
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 2 deletions.
99 changes: 99 additions & 0 deletions src/bitcoind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import RpcClient from "bitcoind-rpc";
import { LOGLEVEL, BITCOIND_ESTIMATE_MODE } from "./util";
import { logger } from "./logger";
import { promisify } from "util";

const log = logger(LOGLEVEL);

export class BitcoindProvider implements Provider {
public rpc: RpcClient;
private targets: number[];

constructor(url: string, user: string, pass: string, targets: number[]) {
let { protocol, hostname: host, port } = new URL(url);
protocol = protocol.replace(/.$/, "");
this.rpc = new RpcClient({ protocol, host, port, user, pass });
this.targets = targets;
}

async getBlockHeight(): Promise<number> {
const getBlockCount = promisify(this.rpc.getBlockCount.bind(this.rpc));

const response = await getBlockCount();
log.trace({ msg: "getBlockCount", response: response.result });

return response.result;
}

async getBlockHash(): Promise<string> {
const getBestBlockHash = promisify(
this.rpc.getBestBlockHash.bind(this.rpc),
);

const response = await getBestBlockHash();
log.trace({ msg: "getBestBlockHash", response: response.result });

return response.result;
}

async getFeeEstimate(target: number): Promise<number> {
const estimateSmartFee = promisify(
this.rpc.estimateSmartFee.bind(this.rpc),
);

const response = await estimateSmartFee(target, BITCOIND_ESTIMATE_MODE);
log.trace({ msg: "estimateSmartFee", response: response.result });

return response.result?.feerate;
}

async getFeeEstimates(): Promise<FeeByBlockTarget> {
const batch = promisify(this.rpc.batch.bind(this.rpc));

const targets = this.targets;
const rpc = this.rpc;

function batchCall() {
targets.forEach(function (target) {
rpc.estimateSmartFee(target, BITCOIND_ESTIMATE_MODE);
});
}

const responses = await batch(batchCall);

const fees: FeeByBlockTarget = {};
let errorCount = 0;

this.targets.forEach((target, i) => {
try {
let feeRate = responses[i].result?.feerate;
if (feeRate) {
// convert the returned value to satoshis, as it's currently returned in BTC.
fees[target] = feeRate * 1e8;
} else {
throw new Error(responses[i].result?.errors[0]);
}
} catch (error) {
errorCount++;
log.warn({
msg: `Error getting fee estimate for target ${target}:`,
errors: responses[i].result?.errors.join(", "),
});
}
});

if (errorCount === this.targets.length) {
log.error({ msg: "Error getting fee estimates" });
throw new Error("Error getting fee estimates");
}

return fees;
}

async getAllData(): Promise<ProviderData> {
const blockHeight = await this.getBlockHeight();
const blockHash = await this.getBlockHash();
const feeEstimates = await this.getFeeEstimates();
return { blockHeight, blockHash, feeEstimates };
}
}
10 changes: 9 additions & 1 deletion src/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface SiteData {
type ExpectedResponseType = "json" | "text"; // can be either 'json' or 'text'

// BatchRequest represents a bitcoind batch request response.
interface BitcoindRpcBatchResponse {
interface EstimateSmartFeeBatchResponse {
result?: EstimateSmartFeeResponse;
error?: any;
}
Expand All @@ -52,6 +52,14 @@ interface EstimateSmartFeeResponse {
blocks?: number; // block number where estimate was found
}

interface BlockCountResponse {
result: number;
}

interface BestBlockHashResponse {
result: string;
}

// EstimateMode represents the mode for fee estimation.
type EstimateMode = "ECONOMICAL" | "CONSERVATIVE"; // estimate mode can be either 'ECONOMICAL' or 'CONSERVATIVE'

Expand Down
2 changes: 1 addition & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ export async function fetchBitcoindData(): Promise<FeeByBlockTarget | null> {

rpc.batch(
batchCall,
(error: Error | null, response: BitcoindRpcBatchResponse[]) => {
(error: Error | null, response: EstimateSmartFeeBatchResponse[]) => {
if (error) {
log.error({
message: "Unable to fetch fee estimates from bitcoind: {error}",
Expand Down
72 changes: 72 additions & 0 deletions test/bitcoind.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { expect, test, mock } from "bun:test";
import { BitcoindProvider } from "../src/bitcoind";
import RpcClient from "bitcoind-rpc";

// Mock the RpcClient
const mockRpcClient: RpcClient = new RpcClient({
protocol: "",
host: "",
port: "",
user: "",
pass: "",
});
mock<RpcClient>(mockRpcClient);

// Mock the methods
mockRpcClient.getBlockCount = (
cb: (error: any, result: BlockCountResponse) => void,
) => cb(null, { result: 1000 });

mockRpcClient.getBestBlockHash = (
cb: (error: any, result: BestBlockHashResponse) => void,
) =>
cb(null, {
result: "00000000000000000007d0f98d9edca880a6c1249057a01b78b182568c64005d",
});

mockRpcClient.estimateSmartFee = (
target: number,
mode: string,
cb: (error: any, result: EstimateSmartFeeBatchResponse) => void,
) => cb(null, { result: { feerate: 1000 } });

const provider = new BitcoindProvider(
"http://localhost:18445",
"user",
"pass",
[2],
);

// Override the rpc property with the mock
provider.rpc = mockRpcClient;

test("getBlockHeight", async () => {
const result = await provider.getBlockHeight();
expect(result).toBe(1000);
});

test("getBlockHash", async () => {
const result = await provider.getBlockHash();
expect(result).toBe(
"00000000000000000007d0f98d9edca880a6c1249057a01b78b182568c64005d",
);
});

test("getFeeEstimate", async () => {
const result = await provider.getFeeEstimate(2);
expect(result).toEqual(1000);
});

// test("getFeeEstimates", async () => {
// const result = await provider.getFeeEstimates();
// expect(result).toEqual({ 2: 1000 });
// });

// test("getAllData", async () => {
// const result = await provider.getAllData();
// expect(result).toEqual({
// blockHeight: 1000,
// blockHash: "00000000000000000007d0f98d9edca880a6c1249057a01b78b182568c64005d",
// feeEstimates: { 2: 1000, 3: 1000, 5: 1000 },
// });
// });

0 comments on commit 1ad73bd

Please sign in to comment.