UNPKG

@atomiqlabs/sdk

Version:

atomiq labs SDK for cross-chain swaps between smart chains and bitcoin

237 lines (205 loc) 7.04 kB
import {getLogger} from "../../utils/Logger"; const logger = getLogger("CoinSelect: "); // baseline estimates, used to improve performance const TX_EMPTY_SIZE = 4 + 1 + 1 + 4; const TX_INPUT_BASE = 32 + 4 + 1 + 4; const WITNESS_OVERHEAD = 2/4; const P2WPKH_WITNESS = (1+1+72+1+33)/4; const P2TR_WITNESS = (1+1+65)/4; const TX_INPUT_PUBKEYHASH = 107; const TX_INPUT_P2SH_P2WPKH = 23 + P2WPKH_WITNESS + 1; const TX_INPUT_P2WPKH = 0 + P2WPKH_WITNESS; const TX_INPUT_P2WSH = 0 + (1+1+64)/4; const TX_INPUT_P2TR = 0 + P2TR_WITNESS; const TX_OUTPUT_BASE = 8 + 1; const TX_OUTPUT_PUBKEYHASH = 25; const TX_OUTPUT_P2SH_P2WPKH = 23; const TX_OUTPUT_P2WPKH = 22; const TX_OUTPUT_P2WSH = 34; const TX_OUTPUT_P2TR = 34; /** * Defines a type of the address used by the wallet, for proper coinselection (as coinselection * depends on estimating the cost for spending the input + the cost of adding a change output) * * @category Bitcoin */ export type CoinselectAddressTypes = "p2sh-p2wpkh" | "p2wpkh" | "p2wsh" | "p2tr" | "p2pkh"; export type CoinselectTxInput = { script?: Buffer, txId: string, vout: number, type?: CoinselectAddressTypes, value: number, outputScript?: Buffer, address?: string, cpfp?: { txVsize: number, txEffectiveFeeRate: number } }; export type CoinselectTxOutput = { script?: Buffer, address?: string, type?: CoinselectAddressTypes, value: number }; const INPUT_BYTES = { "p2sh-p2wpkh": TX_INPUT_P2SH_P2WPKH, "p2wpkh": TX_INPUT_P2WPKH, "p2tr": TX_INPUT_P2TR, "p2pkh": TX_INPUT_PUBKEYHASH, "p2wsh": TX_INPUT_P2WSH }; function inputBytes (input: { script?: Buffer, type?: CoinselectAddressTypes }) { if(input.script==null && input.type==null) throw new Error("Needs either script or type defined!"); return TX_INPUT_BASE + (input.script ? input.script.length : INPUT_BYTES[input.type!]); } const OUTPUT_BYTES = { "p2sh-p2wpkh": TX_OUTPUT_P2SH_P2WPKH, "p2wpkh": TX_OUTPUT_P2WPKH, "p2tr": TX_OUTPUT_P2TR, "p2pkh": TX_OUTPUT_PUBKEYHASH, "p2wsh": TX_OUTPUT_P2WSH }; function outputBytes (output: { script?: Buffer, type?: CoinselectAddressTypes }): number { if(output.script==null && output.type==null) throw new Error("Needs either script or type defined!"); return TX_OUTPUT_BASE + (output.script ? output.script.length : OUTPUT_BYTES[output.type!]); } export const DUST_THRESHOLDS = { "p2sh-p2wpkh": 540, "p2wpkh": 294, "p2tr": 330, "p2pkh": 546, "p2wsh": 330 }; function dustThreshold (output: { script?: Buffer, type: CoinselectAddressTypes }): number { return DUST_THRESHOLDS[output.type]; } function transactionBytes ( inputs: { script?: Buffer, type?: CoinselectAddressTypes }[], outputs: { script?: Buffer, type?: CoinselectAddressTypes }[], changeType?: CoinselectAddressTypes ): number { let size = TX_EMPTY_SIZE; let isSegwit = false; if(changeType!=null && changeType!=="p2pkh") { size += WITNESS_OVERHEAD; isSegwit = true; } for(let input of inputs) { if(!isSegwit && (input.type!=="p2pkh")) { isSegwit = true; size += WITNESS_OVERHEAD; } size += inputBytes(input); } for(let output of outputs) { size += outputBytes(output); } return Math.ceil(size); } function numberOrNaN(v: number): number { if (typeof v !== 'number') return NaN; if (!isFinite(v)) return NaN; if (v < 0) return NaN; return v; } function uintOrNaN(v: number): number { if (typeof v !== 'number') return NaN; if (!isFinite(v)) return NaN; if (Math.floor(v) !== v) return NaN; if (v < 0) return NaN; return v; } function sumForgiving(range: {value: number}[]): number { return range.reduce((a, x) => a + (isFinite(x.value) ? x.value : 0), 0); } function sumOrNaN(range: {value: number}[]): number { return range.reduce((a, x) => a + uintOrNaN(x.value), 0); } function finalize<T extends Omit<CoinselectTxInput, "txId" | "address" | "vout" | "outputScript">>( inputs: T[], outputs: CoinselectTxOutput[], feeRate: number, changeType: CoinselectAddressTypes | null, cpfpAddFee: number = 0 ): { inputs?: T[], outputs?: CoinselectTxOutput[], effectiveFeeRate?: number, fee: number } { const bytesAccum = transactionBytes(inputs, outputs, changeType ?? undefined); logger.debug("finalize(): Transaction bytes: ", bytesAccum); if(changeType!=null) { const feeAfterExtraOutput = (feeRate * (bytesAccum + outputBytes({type: changeType}))) + cpfpAddFee; logger.debug("finalize(): TX fee after adding change output: ", feeAfterExtraOutput); const remainderAfterExtraOutput = Math.floor(sumOrNaN(inputs) - (sumOrNaN(outputs) + feeAfterExtraOutput)); logger.debug("finalize(): Leaves change (changeType="+changeType+") value: ", remainderAfterExtraOutput); // is it worth a change output? if (remainderAfterExtraOutput >= dustThreshold({type: changeType})) { outputs = outputs.concat({ value: remainderAfterExtraOutput, type: changeType }) } } const fee = sumOrNaN(inputs) - sumOrNaN(outputs); logger.debug("finalize(): Re-calculated total fee: ", fee); if (!isFinite(fee) || fee<0) return { fee: (feeRate * bytesAccum) + cpfpAddFee } let txVSize = utils.transactionBytes(inputs, outputs); let txFee = fee; const cpfpSortedInputs = [...inputs].sort( (a, b) => (b.cpfp?.txEffectiveFeeRate ?? 0) - (a.cpfp?.txEffectiveFeeRate ?? 0) ); cpfpSortedInputs.forEach(input => { if(input.cpfp==null) return; const currentEffectiveFeeRate = txFee / txVSize; if(currentEffectiveFeeRate > input.cpfp.txEffectiveFeeRate) { txVSize += input.cpfp.txVsize; txFee += input.cpfp.txVsize * input.cpfp.txEffectiveFeeRate; } } ); return { inputs: inputs, outputs: outputs, effectiveFeeRate: txFee / txVSize, fee: fee } } function isDetrimentalInput( feeRate: number, utxo: Omit<CoinselectTxInput, "txId" | "address" | "vout" | "outputScript"> ) { const utxoBytes = utils.inputBytes(utxo); const utxoFee = feeRate * utxoBytes; let cpfpFee = 0; if(utxo.cpfp!=null && utxo.cpfp.txEffectiveFeeRate<feeRate) cpfpFee = Math.ceil(utxo.cpfp.txVsize*(feeRate - utxo.cpfp.txEffectiveFeeRate)); // skip detrimental input return utxoFee + cpfpFee > utxo.value; } export const utils = { dustThreshold: dustThreshold, finalize: finalize, inputBytes: inputBytes, outputBytes: outputBytes, sumOrNaN: sumOrNaN, sumForgiving: sumForgiving, transactionBytes: transactionBytes, uintOrNaN: uintOrNaN, numberOrNaN: numberOrNaN, isDetrimentalInput };