did-btcr2-js

@did-btcr2/bitcoin

Sans-I/O Bitcoin client for did-btcr2-js. Speaks Esplora REST (mempool.space, blockstream.info, or any compatible indexer) and Bitcoin Core JSON-RPC.

Part of the did-btcr2-js monorepo.

Summary

The DID method needs to read transactions from beacon addresses, fetch block metadata, and broadcast signed updates. This package provides those operations as a pluggable, browser-compatible client.

Install

npm install @did-btcr2/bitcoin

Or with pnpm:

pnpm add @did-btcr2/bitcoin

Requires Node >= 22. Ships both ESM and CJS; pick whichever your bundler needs.

Key Exports

Concern Entry point
Per-network connection BitcoinConnection, BitcoinConnectionOptions
REST client (Esplora) BitcoinRestClient, sub-clients BitcoinAddress, BitcoinBlock, BitcoinTransaction
RPC client (Bitcoin Core) BitcoinCoreRpcClient, JsonRpcTransport, RpcMethodMap, TypedRpcMethod
Sans-I/O protocol layer EsploraProtocol, JsonRpcProtocol, HttpRequest, HttpExecutor, defaultHttpExecutor, createFetchExecutor, FetchExecutorOptions
Fee estimation FeeEstimator, StaticFeeEstimator
Network params getNetwork(name), BTCNetwork, NetworkName
Errors BitcoinRpcError, BitcoinRestError, RpcErrorType
Bitcoin constants INITIAL_BLOCK_REWARD, HALVING_INTERVAL, COINBASE_MATURITY_DELAY, DEFAULT_BLOCK_CONFIRMATIONS, GENESIS_TX_ID

Quick Start

import { BitcoinConnection } from '@did-btcr2/bitcoin';

// Public network: REST only. Supply the Esplora host explicitly.
const btc = new BitcoinConnection({
  network : 'mutinynet',
  rest    : { host: 'https://mutinynet.com/api' },
});

const height = await btc.rest.block.count();
const utxos  = await btc.rest.address.getUtxos('tb1q...');
const txHex  = await btc.rest.transaction.getHex('abc123...');

// Regtest with explicit REST host and RPC credentials.
const regtest = new BitcoinConnection({
  network : 'regtest',
  rest    : { host: 'http://localhost:3000' },
  rpc     : { host: 'http://localhost:18443', username: 'polaruser', password: 'polarpass' },
});
await regtest.rpc!.generateToAddress(6, await regtest.rpc!.getNewAddress('bech32'));

Fresh responses and browsers

EsploraProtocol sets fresh: true on each request whose response can change over time: the chain tip, a transaction with its status, a block hash by height, and the data of an address. A request for a transaction or a block by its hash has no fresh field, so a cache can keep the response.

The default executor gets a fresh response from the origin server. It sets the fetch option cache: 'no-store' and adds a random _ query parameter, so that no browser cache and no CDN cache answers the request. It adds no request header. A GET request carries no Content-Type, and POST /tx carries text/plain. So a browser sends no CORS preflight, and the default config works in a web app.

Injecting a custom executor

For a request timeout, use createFetchExecutor:

import { BitcoinConnection, createFetchExecutor } from '@did-btcr2/bitcoin';

const btc = new BitcoinConnection({
  network  : 'mutinynet',
  rest     : { host: 'https://mutinynet.com/api' },
  executor : createFetchExecutor({ timeoutMs: 5_000 }),
});

For tests, sandboxes, or rate-limited fetchers, pass your own HttpExecutor. It must honor req.fresh. To keep the default rules, wrap an executor from createFetchExecutor:

const fetchExecutor = createFetchExecutor({ timeoutMs: 5_000 });
const btc = new BitcoinConnection({
  network  : 'mutinynet',
  rest     : { host: 'https://mutinynet.com/api' },
  executor : async (req) => {
    await rateLimiter.acquire();
    return fetchExecutor(req);
  },
});

Architecture Principles

Build & Test

# From packages/bitcoin/
pnpm build              # Compile ESM + CJS + type declarations
pnpm build:tests        # Compile tests to tests/compiled/
pnpm test               # Run the test suite with coverage
pnpm lint               # ESLint (zero warnings tolerated)

Documentation

License

MPL-2.0