@atomiqlabs/btc-mempool
Version:
Connector and synchronizer using mempool.space API for bitcoin
334 lines (333 loc) • 11.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MempoolApi = void 0;
const buffer_1 = require("buffer");
const MempoolApiError_1 = require("../errors/MempoolApiError");
const base_1 = require("@atomiqlabs/base");
const MempoolApiEndpoints = {
[base_1.BitcoinNetwork.MAINNET]: [
"https://mempool.space/api/",
"https://mempool.fra.mempool.space/api/",
"https://mempool.va1.mempool.space/api/",
"https://mempool.tk7.mempool.space/api/"
],
[base_1.BitcoinNetwork.TESTNET]: [
"https://mempool.space/testnet/api/",
"https://mempool.fra.mempool.space/testnet/api/",
"https://mempool.va1.mempool.space/testnet/api/",
"https://mempool.tk7.mempool.space/testnet/api/"
],
[base_1.BitcoinNetwork.TESTNET4]: [
"https://mempool.space/testnet4/api/",
"https://mempool.fra.mempool.space/testnet4/api/",
"https://mempool.va1.mempool.space/testnet4/api/",
"https://mempool.tk7.mempool.space/testnet4/api/"
]
};
/**
* Mempool.space REST API client for Bitcoin blockchain data
*
* @category Bitcoin
*/
class MempoolApi {
/**
* Returns api url that should be operational
*
* @private
*/
getOperationalApi() {
return this.backends.find(e => e.operational === true);
}
/**
* Returns api urls that are maybe operational, in case none is considered operational returns all of the price
* apis such that they can be tested again whether they are operational
*
* @private
*/
getMaybeOperationalApis() {
let operational = this.backends.filter(e => e.operational === true || e.operational === null);
if (operational.length === 0) {
this.backends.forEach(e => e.operational = null);
operational = this.backends;
}
return operational;
}
/**
* Sends a GET or POST request to the mempool api, handling the non-200 responses as errors & throwing
*
* @param url
* @param path
* @param responseType
* @param type
* @param body
*/
async _request(url, path, responseType, type = "GET", body) {
const response = await fetch(url + path, {
method: type,
signal: AbortSignal.timeout(this.timeout),
body: typeof (body) === "string" ? body : JSON.stringify(body)
});
if (response.status !== 200) {
let resp;
try {
resp = await response.text();
}
catch (e) {
throw new MempoolApiError_1.MempoolApiError(response.statusText, response.status);
}
throw new MempoolApiError_1.MempoolApiError(resp, response.status);
}
if (responseType === "str")
return await response.text();
return await response.json();
}
/**
* Sends request in parallel to multiple maybe operational api urls
*
* @param path
* @param responseType
* @param type
* @param body
* @private
*/
async requestFromMaybeOperationalUrls(path, responseType, type = "GET", body) {
try {
return await Promise.any(this.getMaybeOperationalApis().map(obj => (async () => {
try {
const result = await this._request(obj.url, path, responseType, type, body);
obj.operational = true;
return result;
}
catch (e) {
//Only mark as non operational on 5xx server errors!
if (e instanceof MempoolApiError_1.MempoolApiError && Math.floor(e.httpCode / 100) !== 5) {
obj.operational = true;
throw e;
}
else {
obj.operational = false;
throw e;
}
}
})()));
}
catch (_e) {
const e = _e;
throw e.errors.find(err => err instanceof MempoolApiError_1.MempoolApiError && Math.floor(err.httpCode / 100) !== 5) || e.errors[0];
}
}
/**
* Sends a request to mempool API, first tries to use the operational API (if any) and if that fails it falls back
* to using maybe operational price APIs
*
* @param path
* @param responseType
* @param type
* @param body
* @private
*/
async request(path, responseType, type = "GET", body) {
return (0, base_1.tryWithRetries)(() => {
const operationalPriceApi = this.getOperationalApi();
if (operationalPriceApi != null) {
return this._request(operationalPriceApi.url, path, responseType, type, body).catch(err => {
//Only retry on 5xx server errors!
if (err instanceof MempoolApiError_1.MempoolApiError && Math.floor(err.httpCode / 100) !== 5)
throw err;
operationalPriceApi.operational = false;
return this.requestFromMaybeOperationalUrls(path, responseType, type, body);
});
}
return this.requestFromMaybeOperationalUrls(path, responseType, type, body);
}, undefined, (err) => err instanceof MempoolApiError_1.MempoolApiError && Math.floor(err.httpCode / 100) !== 5);
}
constructor(urlOrNetwork, timeout) {
if (typeof (urlOrNetwork) === "number") {
const endpoints = MempoolApiEndpoints[urlOrNetwork];
if (endpoints == null)
throw new Error(`No default endpoints found for ${base_1.BitcoinNetwork[urlOrNetwork]} network, please pass the manually as string or string[]`);
this.backends = endpoints.map(val => ({ url: val, operational: null }));
}
else {
if (Array.isArray(urlOrNetwork)) {
this.backends = urlOrNetwork.map(val => ({ url: val, operational: null }));
}
else {
this.backends = [{ url: urlOrNetwork, operational: null }];
}
}
this.timeout = timeout ?? 15 * 1000;
}
/**
* Returns information about a specific lightning network node as identified by the public key (in hex encoding)
*
* @param pubkey
*/
getLNNodeInfo(pubkey) {
//500, 200
return this.request("v1/lightning/nodes/" + pubkey, "obj").catch((e) => {
if (e.responseMessage === "This node does not exist, or our node is not seeing it yet")
return null;
throw e;
});
}
/**
* Returns on-chain transaction as identified by its txId
*
* @param txId
*/
getTransaction(txId) {
//404 ("Transaction not found"), 200
return this.request("tx/" + txId, "obj").catch((e) => {
if (e.responseMessage === "Transaction not found")
return null;
throw e;
});
}
/**
* Returns raw binary encoded bitcoin transaction, also strips the witness data from the transaction
*
* @param txId
*/
async getRawTransaction(txId) {
//404 ("Transaction not found"), 200
const rawTransaction = await this.request("tx/" + txId + "/hex", "str").catch((e) => {
if (e.responseMessage === "Transaction not found")
return null;
throw e;
});
return rawTransaction == null ? null : buffer_1.Buffer.from(rawTransaction, "hex");
}
/**
* Returns confirmed & unconfirmed balance of the specific bitcoin address
*
* @param address
*/
async getAddressBalances(address) {
//400 ("Invalid Bitcoin address"), 200
const jsonBody = await this.request("address/" + address, "obj");
const confirmedInput = BigInt(jsonBody.chain_stats.funded_txo_sum);
const confirmedOutput = BigInt(jsonBody.chain_stats.spent_txo_sum);
const unconfirmedInput = BigInt(jsonBody.mempool_stats.funded_txo_sum);
const unconfirmedOutput = BigInt(jsonBody.mempool_stats.spent_txo_sum);
return {
confirmedBalance: confirmedInput - confirmedOutput,
unconfirmedBalance: unconfirmedInput - unconfirmedOutput
};
}
/**
* Returns CPFP (children pays for parent) data for a given transaction
*
* @param txId
*/
getCPFPData(txId) {
//200
return this.request("v1/cpfp/" + txId, "obj");
}
/**
* Returns UTXOs (unspent transaction outputs) for a given address
*
* @param address
*/
async getAddressUTXOs(address) {
//400 ("Invalid Bitcoin address"), 200
let jsonBody = await this.request("address/" + address + "/utxo", "obj");
jsonBody.forEach(e => e.value = BigInt(e.value));
return jsonBody;
}
/**
* Returns current on-chain bitcoin fees
*/
getFees() {
//200
return this.request("v1/fees/recommended", "obj");
}
/**
* Returns all transactions for a given address
*
* @param address
*/
getAddressTransactions(address) {
//400 ("Invalid Bitcoin address"), 200
return this.request("address/" + address + "/txs", "obj");
}
/**
* Returns expected pending (mempool) blocks
*/
getPendingBlocks() {
//200
return this.request("v1/fees/mempool-blocks", "obj");
}
/**
* Returns the blockheight of the current bitcoin blockchain's tip
*/
async getTipBlockHeight() {
//200
const response = await this.request("blocks/tip/height", "str");
return parseInt(response);
}
/**
* Returns the bitcoin blockheader as identified by its blockhash
*
* @param blockhash
*/
getBlockHeader(blockhash) {
//404 ("Block not found"), 200
return this.request("block/" + blockhash, "obj");
}
/**
* Returns the block status
*
* @param blockhash
*/
getBlockStatus(blockhash) {
//200
return this.request("block/" + blockhash + "/status", "obj");
}
/**
* Returns the transaction's proof (merkle proof)
*
* @param txId
*/
getTransactionProof(txId) {
//404 ("Transaction not found or is unconfirmed"), 200
return this.request("tx/" + txId + "/merkle-proof", "obj");
}
/**
* Returns the transaction's proof (merkle proof)
*
* @param txId
*/
getOutspends(txId) {
//404 ("Transaction not found"), 200
return this.request("tx/" + txId + "/outspends", "obj");
}
/**
* Returns blockhash of a block at a specific blockheight
*
* @param height
*/
getBlockHash(height) {
//404 ("Block not found"), 200
return this.request("block-height/" + height, "str");
}
/**
* Returns past 15 blockheaders before (and including) the specified height
*
* @param endHeight
*/
getPast15BlockHeaders(endHeight) {
//200
return this.request("v1/blocks/" + endHeight, "obj");
}
/**
* Sends raw hex encoded bitcoin transaction
*
* @param transactionHex
*/
sendTransaction(transactionHex) {
//400??, 200
return this.request("tx", "str", "POST", transactionHex);
}
}
exports.MempoolApi = MempoolApi;