@atomiqlabs/sdk-lib
Version:
Basic SDK functionality library for atomiq
287 lines (252 loc) • 10.6 kB
text/typescript
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");
}
}