UNPKG

@atomiqlabs/sdk-lib

Version:

Basic SDK functionality library for atomiq

287 lines (252 loc) 10.6 kB
import {BigIntBufferUtils, BtcBlockWithTxs, BtcSyncInfo, BtcTx} from "@atomiqlabs/base"; import {MempoolBitcoinBlock} from "./MempoolBitcoinBlock"; import {BitcoinTransaction, MempoolApi, TxVout} from "./MempoolApi"; import {Buffer} from "buffer"; import {BitcoinRpcWithTxoListener, BtcTxWithBlockheight} from "../BitcoinRpcWithTxoListener"; import {LightningNetworkApi, LNNodeLiquidity} from "../LightningNetworkApi"; import {timeoutPromise} from "../../utils/Utils"; import {Transaction} from "@scure/btc-signer"; import {sha256} from "@noble/hashes/sha2"; const BITCOIN_BLOCKTIME = 600 * 1000; const BITCOIN_BLOCKSIZE = 1024*1024; export class MempoolBitcoinRpc implements BitcoinRpcWithTxoListener<MempoolBitcoinBlock>, LightningNetworkApi { api: MempoolApi; constructor(mempoolApi: MempoolApi) { this.api = mempoolApi; } /** * Returns a txo hash for a specific transaction vout * * @param vout * @private */ private static getTxoHash(vout: TxVout): Buffer { return Buffer.from(sha256(Buffer.concat([ BigIntBufferUtils.toBuffer(BigInt(vout.value), "le", 8), Buffer.from(vout.scriptpubkey, "hex") ]))); } /** * Returns delay in milliseconds till an unconfirmed transaction is expected to confirm, returns -1 * if the transaction won't confirm any time soon * * @param feeRate * @private */ private async getTimeTillConfirmation(feeRate: number): Promise<number> { const mempoolBlocks = await this.api.getPendingBlocks(); const mempoolBlockIndex = mempoolBlocks.findIndex(block => block.feeRange[0]<=feeRate); if(mempoolBlockIndex===-1) return -1; //Last returned block is usually an aggregate (or a stack) of multiple btc blocks, if tx falls in this block // and the last returned block really is an aggregate one (size bigger than BITCOIN_BLOCKSIZE) we return -1 if( mempoolBlockIndex+1===mempoolBlocks.length && mempoolBlocks[mempoolBlocks.length-1].blockVSize>BITCOIN_BLOCKSIZE ) return -1; return (mempoolBlockIndex+1) * BITCOIN_BLOCKTIME; } /** * Returns an estimate after which time the tx will confirm with the required amount of confirmations, * confirmationDelay of -1 means the transaction won't confirm in the near future * * @param tx * @param requiredConfirmations * @private * * @returns estimated confirmation delay, -1 if the transaction won't confirm in the near future, null if the * transaction was replaced or was confirmed in the meantime */ async getConfirmationDelay(tx: BtcTx, requiredConfirmations: number): Promise<number | null> { if(tx.confirmations>requiredConfirmations) return 0; if(tx.confirmations===0) { //Get CPFP data const cpfpData = await this.api.getCPFPData(tx.txid); if(cpfpData.effectiveFeePerVsize==null) { //Transaction is either confirmed in the meantime, or replaced return null; } let confirmationDelay = (await this.getTimeTillConfirmation(cpfpData.effectiveFeePerVsize)); if(confirmationDelay!==-1) confirmationDelay += (requiredConfirmations-1)*BITCOIN_BLOCKTIME; return confirmationDelay; } return ((requiredConfirmations-tx.confirmations)*BITCOIN_BLOCKTIME); } /** * Converts mempool API's transaction to BtcTx object * @param tx Transaction to convert * @param getRaw If the raw transaction field should be filled (requires one more network request) * @private */ private async toBtcTx(tx: BitcoinTransaction, getRaw: boolean = true): Promise<BtcTxWithBlockheight> { const rawTx: Buffer = !getRaw ? null : await this.api.getRawTransaction(tx.txid); let confirmations: number = 0; if(tx.status!=null && tx.status.confirmed) { const blockheight = await this.api.getTipBlockHeight(); confirmations = blockheight-tx.status.block_height+1; } let strippedRawTx: string; if(rawTx!=null) { //Strip witness data const btcTx = Transaction.fromRaw(rawTx); strippedRawTx = Buffer.from(btcTx.toBytes(true, false)).toString("hex"); } return { blockheight: tx.status?.block_height, blockhash: tx.status?.block_hash, confirmations, txid: tx.txid, vsize: tx.weight/4, hex: strippedRawTx, raw: rawTx==null ? null : rawTx.toString("hex"), outs: tx.vout.map((e, index) => { return { value: e.value, n: index, scriptPubKey: { hex: e.scriptpubkey, asm: e.scriptpubkey_asm } } }), ins: tx.vin.map(e => { return { txid: e.txid, vout: e.vout, scriptSig: { hex: e.scriptsig, asm: e.scriptsig_asm }, sequence: e.sequence, txinwitness: e.witness } }), }; } getTipHeight(): Promise<number> { return this.api.getTipBlockHeight(); } async getBlockHeader(blockhash: string): Promise<MempoolBitcoinBlock> { return new MempoolBitcoinBlock(await this.api.getBlockHeader(blockhash)); } async getMerkleProof(txId: string, blockhash: string): Promise<{ reversedTxId: Buffer; pos: number; merkle: Buffer[]; blockheight: number }> { const proof = await this.api.getTransactionProof(txId); return { reversedTxId: Buffer.from(txId, "hex").reverse(), pos: proof.pos, merkle: proof.merkle.map(e => Buffer.from(e, "hex").reverse()), blockheight: proof.block_height }; } async getTransaction(txId: string): Promise<BtcTxWithBlockheight> { const tx = await this.api.getTransaction(txId); if(tx==null) return null; return await this.toBtcTx(tx); } async isInMainChain(blockhash: string): Promise<boolean> { const blockStatus = await this.api.getBlockStatus(blockhash); return blockStatus.in_best_chain; } getBlockhash(height: number): Promise<string> { return this.api.getBlockHash(height); } getBlockWithTransactions(blockhash: string): Promise<BtcBlockWithTxs> { throw new Error("Unsupported."); } async getSyncInfo(): Promise<BtcSyncInfo> { const tipHeight = await this.api.getTipBlockHeight(); return { verificationProgress: 1, blocks: tipHeight, headers: tipHeight, ibd: false }; } async getPast15Blocks(height: number): Promise<MempoolBitcoinBlock[]> { return (await this.api.getPast15BlockHeaders(height)).map(blockHeader => new MempoolBitcoinBlock(blockHeader)); } async checkAddressTxos(address: string, txoHash: Buffer): Promise<{ tx: BtcTxWithBlockheight, vout: number } | null> { const allTxs = await this.api.getAddressTransactions(address); const relevantTxs = allTxs .map(tx => { return { tx, vout: tx.vout.findIndex(vout => MempoolBitcoinRpc.getTxoHash(vout).equals(txoHash)) } }) .filter(obj => obj.vout>=0) .sort((a, b) => { if(a.tx.status.confirmed && !b.tx.status.confirmed) return -1; if(!a.tx.status.confirmed && b.tx.status.confirmed) return 1; if(a.tx.status.confirmed && b.tx.status.confirmed) return a.tx.status.block_height-b.tx.status.block_height; return 0; }); if(relevantTxs.length===0) return null; return { tx: await this.toBtcTx(relevantTxs[0].tx, false), vout: relevantTxs[0].vout }; } /** * Waits till the address receives a transaction containing a specific txoHash * * @param address * @param txoHash * @param requiredConfirmations * @param stateUpdateCbk * @param abortSignal * @param intervalSeconds */ async waitForAddressTxo( address: string, txoHash: Buffer, requiredConfirmations: number, stateUpdateCbk:(confirmations: number, txId: string, vout: number, txEtaMS: number) => void, abortSignal?: AbortSignal, intervalSeconds?: number ): Promise<{ tx: BtcTxWithBlockheight, vout: number }> { if(abortSignal!=null) abortSignal.throwIfAborted(); while(abortSignal==null || !abortSignal.aborted) { await timeoutPromise((intervalSeconds || 5)*1000, abortSignal); const result = await this.checkAddressTxos(address, txoHash); if(result==null) { stateUpdateCbk(null, null, null, null); continue; } const confirmationDelay = await this.getConfirmationDelay(result.tx, requiredConfirmations); if(confirmationDelay==null) continue; if(stateUpdateCbk!=null) stateUpdateCbk( result.tx.confirmations, result.tx.txid, result.vout, confirmationDelay ); if(confirmationDelay===0) return result; } abortSignal.throwIfAborted(); } async getLNNodeLiquidity(pubkey: string): Promise<LNNodeLiquidity> { const nodeInfo = await this.api.getLNNodeInfo(pubkey); return { publicKey: nodeInfo.public_key, capacity: BigInt(nodeInfo.capacity), numChannels: nodeInfo.active_channel_count } } sendRawTransaction(rawTx: string): Promise<string> { return this.api.sendTransaction(rawTx); } sendRawPackage(rawTx: string[]): Promise<string[]> { throw new Error("Unsupported"); } }