UNPKG

@atomiqlabs/sdk-lib

Version:

Basic SDK functionality library for atomiq

507 lines (461 loc) 14.9 kB
import {Buffer} from "buffer"; import {fetchWithTimeout, promiseAny, tryWithRetries} from "../../utils/Utils"; import {RequestError} from "../../errors/RequestError"; export type BitcoinTransactionStatus = { confirmed: boolean, block_height: number, block_hash: string, block_time: number }; export type TxVout = { scriptpubkey: string, scriptpubkey_asm: string, scriptpubkey_type: string, scriptpubkey_address: string, value: number }; export type TxVin = { txid: string, vout: number, prevout: TxVout, scriptsig: string, scriptsig_asm: string, witness: string[], is_coinbase: boolean, sequence: number, inner_witnessscript_asm: string }; export type BitcoinTransaction = { txid: string, version: number, locktime: number, vin: TxVin[], vout: TxVout[], size: number, weight: number, fee: number, status: BitcoinTransactionStatus }; export type BlockData = { bits: number, difficulty: number, extras: any, height: number, id: string, mediantime: number, merkle_root: string, nonce: number, previousblockhash: string, size: number, timestamp: number, tx_count: number, version: number, weight: number } export type BitcoinBlockHeader = { id: string, height: number, version: number, timestamp: number, tx_count: number, size: number, weight: number, merkle_root: string, previousblockhash: string, mediantime: number, nonce: number, bits: number, difficulty: number }; export type LNNodeInfo = { public_key: string, alias: string, first_seen: number, updated_at: number, color: string, sockets: string, as_number: number, city_id: number, country_id: number, subdivision_id: number, longtitude: number, latitude: number, iso_code: string, as_organization: string, city: {[lang: string]: string}, country: {[lang: string]: string}, subdivision: {[lang: string]: string}, active_channel_count: number, capacity: string, opened_channel_count: number, closed_channel_count: number }; export type AddressInfo = { address: string; chain_stats: { funded_txo_count: number; funded_txo_sum: number; spent_txo_count: number; spent_txo_sum: number; tx_count: number; }; mempool_stats: { funded_txo_count: number; funded_txo_sum: number; spent_txo_count: number; spent_txo_sum: number; tx_count: number; }; }; export type TransactionCPFPData = { ancestors: { txid: string, fee: number, weight: number }[], descendants: { txid: string, fee: number, weight: number }[], effectiveFeePerVsize: number, sigops: number, adjustedVsize: number }; export type BitcoinFees = { fastestFee: number, halfHourFee: number, hourFee: number, economyFee: number, minimumFee: number }; export type BitcoinPendingBlock = { blockSize: number, blockVSize: number, nTx: number, totalFees: number, medianFee: number, feeRange: number[] }; export type BlockStatus = { in_best_chain: boolean, height: number, next_best: string }; export type TransactionProof = { block_height: number, merkle: string[], pos: number }; export type TransactionOutspend = { spent: boolean, txid: string, vin: number, status: BitcoinTransactionStatus }; export class MempoolApi { backends: { url: string, operational: boolean | null }[]; timeout: number; /** * Returns api url that should be operational * * @private */ private getOperationalApi(): {url: string, operational: boolean} { 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 */ private getMaybeOperationalApis(): {url: string, operational: boolean}[] { 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 */ private async _request<T>( url: string, path: string, responseType: T extends string ? "str" : "obj", type: "GET" | "POST" = "GET", body?: string | any ) : Promise<T> { const response: Response = await fetchWithTimeout(url+path, { method: type, timeout: this.timeout, body: typeof(body)==="string" ? body : JSON.stringify(body) }); if(response.status!==200) { let resp: string; try { resp = await response.text(); } catch (e) { throw new RequestError(response.statusText, response.status); } throw RequestError.parse(resp, response.status); } if(responseType==="str") return await response.text() as any; return await response.json(); } /** * Sends request in parallel to multiple maybe operational api urls * * @param path * @param responseType * @param type * @param body * @private */ private async requestFromMaybeOperationalUrls<T>( path: string, responseType: T extends string ? "str" : "obj", type: "GET" | "POST" = "GET", body?: string | any ) : Promise<T> { try { return await promiseAny<T>(this.getMaybeOperationalApis().map( obj => (async () => { try { const result = await this._request<T>(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 RequestError && Math.floor(e.httpCode/100)!==5) { obj.operational = true; throw e; } else { obj.operational = false; throw e; } } })() )) } catch (e) { throw e.find(err => err instanceof RequestError && Math.floor(err.httpCode/100)!==5) || e[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 */ private async request<T>( path: string, responseType: T extends string ? "str" : "obj", type: "GET" | "POST" = "GET", body?: string | any ) : Promise<T> { return tryWithRetries<T>(() => { 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 RequestError && 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); }, null, (err: any) => err instanceof RequestError && Math.floor(err.httpCode/100)!==5); } constructor(url?: string | string[], timeout?: number) { url = url ?? "https://mempool.space/testnet/api/"; if(Array.isArray(url)) { this.backends = url.map(val => { return {url: val, operational: null} }); } else { this.backends = [ {url: url, operational: null} ]; } this.timeout = timeout; } /** * Returns information about a specific lightning network node as identified by the public key (in hex encoding) * * @param pubkey */ getLNNodeInfo(pubkey: string): Promise<LNNodeInfo | null> { return this.request<LNNodeInfo>("v1/lightning/nodes/"+pubkey, "obj").catch((e: Error) => { if(e.message==="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: string): Promise<BitcoinTransaction | null> { return this.request<BitcoinTransaction>("tx/"+txId, "obj").catch((e: Error) => { if(e.message==="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: string): Promise<Buffer> { const rawTransaction: string = await this.request<string>("tx/"+txId+"/hex", "str").catch((e: Error) => { if(e.message==="Transaction not found") return null; throw e; }); return rawTransaction==null ? null : Buffer.from(rawTransaction, "hex") } /** * Returns confirmed & unconfirmed balance of the specific bitcoin address * * @param address */ async getAddressBalances(address: string): Promise<{ confirmedBalance: bigint, unconfirmedBalance: bigint }> { const jsonBody = await this.request<AddressInfo>("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: string): Promise<TransactionCPFPData> { return this.request<TransactionCPFPData>("v1/cpfp/"+txId, "obj"); } /** * Returns UTXOs (unspent transaction outputs) for a given address * * @param address */ async getAddressUTXOs(address: string): Promise<{ txid: string, vout: number, status: { confirmed: boolean, block_height: number, block_hash: string, block_time: number }, value: bigint }[]> { let jsonBody: any = await this.request<any>("address/"+address+"/utxo", "obj"); jsonBody.forEach(e => e.value = BigInt(e.value)); return jsonBody; } /** * Returns current on-chain bitcoin fees */ getFees(): Promise<BitcoinFees> { return this.request<BitcoinFees>("v1/fees/recommended", "obj"); } /** * Returns all transactions for a given address * * @param address */ getAddressTransactions(address: string): Promise<BitcoinTransaction[]> { return this.request<BitcoinTransaction[]>("address/"+address+"/txs", "obj"); } /** * Returns expected pending (mempool) blocks */ getPendingBlocks(): Promise<BitcoinPendingBlock[]> { return this.request<BitcoinPendingBlock[]>("v1/fees/mempool-blocks", "obj"); } /** * Returns the blockheight of the current bitcoin blockchain's tip */ async getTipBlockHeight() : Promise<number> { const response: string = await this.request<string>("blocks/tip/height", "str"); return parseInt(response); } /** * Returns the bitcoin blockheader as identified by its blockhash * * @param blockhash */ getBlockHeader(blockhash: string): Promise<BitcoinBlockHeader> { return this.request<BitcoinBlockHeader>("block/"+blockhash, "obj"); } /** * Returns the block status * * @param blockhash */ getBlockStatus(blockhash: string): Promise<BlockStatus> { return this.request<BlockStatus>("block/"+blockhash+"/status", "obj"); } /** * Returns the transaction's proof (merkle proof) * * @param txId */ getTransactionProof(txId: string) : Promise<TransactionProof> { return this.request<TransactionProof>("tx/"+txId+"/merkle-proof", "obj"); } /** * Returns the transaction's proof (merkle proof) * * @param txId */ getOutspends(txId: string) : Promise<TransactionOutspend[]> { return this.request<TransactionOutspend[]>("tx/"+txId+"/outspends", "obj"); } /** * Returns blockhash of a block at a specific blockheight * * @param height */ getBlockHash(height: number): Promise<string> { return this.request<string>("block-height/"+height, "str"); } /** * Returns past 15 blockheaders before (and including) the specified height * * @param endHeight */ getPast15BlockHeaders(endHeight: number) : Promise<BlockData[]> { return this.request<BlockData[]>("v1/blocks/"+endHeight, "obj"); } /** * Sends raw hex encoded bitcoin transaction * * @param transactionHex */ sendTransaction(transactionHex: string): Promise<string> { return this.request<string>("tx", "str", "POST", transactionHex); } }