UNPKG

@thorwallet/xchain-bitcoin

Version:

Custom Bitcoin client and utilities used by XChainJS clients

318 lines 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getPrefix = exports.getDefaultFees = exports.getDefaultFeesWithRates = exports.calcFee = exports.broadcastTx = exports.buildTx = exports.scanUTXOs = exports.validateAddress = exports.getBalance = exports.btcNetwork = exports.isTestnet = exports.arrayAverage = exports.getFee = exports.compileMemo = exports.BTC_DECIMAL = void 0; const tslib_1 = require("tslib"); const Bitcoin = tslib_1.__importStar(require("bitcoinjs-lib")); // https://github.com/bitcoinjs/bitcoinjs-lib const sochain = tslib_1.__importStar(require("./sochain-api")); const blockStream = tslib_1.__importStar(require("./blockstream-api")); const xchain_util_1 = require("@thorwallet/xchain-util"); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const accumulative_1 = tslib_1.__importDefault(require("coinselect/accumulative")); const const_1 = require("./const"); const haskoinApi = tslib_1.__importStar(require("./haskoin-api")); const TX_EMPTY_SIZE = 4 + 1 + 1 + 4; //10 const TX_INPUT_BASE = 32 + 4 + 1 + 4; // 41 const TX_INPUT_PUBKEYHASH = 107; const TX_OUTPUT_BASE = 8 + 1; //9 const TX_OUTPUT_PUBKEYHASH = 25; exports.BTC_DECIMAL = 8; const inputBytes = (input) => { return TX_INPUT_BASE + (input.witnessUtxo.script ? input.witnessUtxo.script.length : TX_INPUT_PUBKEYHASH); }; /** * Compile memo. * * @param {string} memo The memo to be compiled. * @returns {Buffer} The compiled memo. */ const compileMemo = (memo) => { const data = Buffer.from(memo, 'utf8'); // converts MEMO to buffer return Bitcoin.script.compile([Bitcoin.opcodes.OP_RETURN, data]); // Compile OP_RETURN script }; exports.compileMemo = compileMemo; /** * Get the transaction fee. * * @param {UTXOs} inputs The UTXOs. * @param {FeeRate} feeRate The fee rate. * @param {Buffer} data The compiled memo (Optional). * @returns {number} The fee amount. */ const getFee = (inputs, feeRate, data = null) => { let sum = TX_EMPTY_SIZE + inputs.reduce((a, x) => a + inputBytes(x), 0) + inputs.length + // +1 byte for each input signature TX_OUTPUT_BASE + TX_OUTPUT_PUBKEYHASH + TX_OUTPUT_BASE + TX_OUTPUT_PUBKEYHASH; if (data) { sum += TX_OUTPUT_BASE + data.length; } const fee = sum * feeRate; return fee > const_1.MIN_TX_FEE ? fee : const_1.MIN_TX_FEE; }; exports.getFee = getFee; /** * Get the average value of an array. * * @param {Array<number>} array * @returns {number} The average value. */ const arrayAverage = (array) => { let sum = 0; array.forEach((value) => (sum += value)); return sum / array.length; }; exports.arrayAverage = arrayAverage; /** * Check if give network is a testnet. * * @param {Network} network * @returns {boolean} `true` or `false` */ const isTestnet = (network) => { return network === 'testnet'; }; exports.isTestnet = isTestnet; /** * Get Bitcoin network to be used with bitcoinjs. * * @param {Network} network * @returns {Bitcoin.Network} The BTC network. */ const btcNetwork = (network) => { return exports.isTestnet(network) ? Bitcoin.networks.testnet : Bitcoin.networks.bitcoin; }; exports.btcNetwork = btcNetwork; /** * Get the balances of an address. * * @param {string} sochainUrl sochain Node URL. * @param {Network} network * @param {Address} address * @returns {Array<Balance>} The balances of the given address. */ const getBalance = (params) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { switch (params.network) { case 'mainnet': return [ { asset: xchain_util_1.AssetBTC, amount: yield haskoinApi.getBalance({ haskoinUrl: 'https://haskoin.ninerealms.com/btc', address: params.address, }), }, ]; case 'testnet': return [ { asset: xchain_util_1.AssetBTC, amount: yield sochain.getBalance(params), }, ]; } }); exports.getBalance = getBalance; /** * Validate the BTC address. * * @param {Address} address * @param {Network} network * @returns {boolean} `true` or `false`. */ const validateAddress = (address, network) => { try { Bitcoin.address.toOutputScript(address, exports.btcNetwork(network)); return true; } catch (error) { return false; } }; exports.validateAddress = validateAddress; /** * Scan UTXOs from sochain. * * @param {string} sochainUrl sochain Node URL. * @param {Network} network * @param {Address} address * @returns {Array<UTXO>} The UTXOs of the given address. */ const scanUTXOs = ({ sochainUrl, haskoinUrl, network, address, confirmedOnly = true, // default: scan only confirmed UTXOs }) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { if (network === 'testnet') { let utxos = []; const addressParam = { sochainUrl, network, address, }; if (confirmedOnly) { utxos = yield sochain.getConfirmedUnspentTxs(addressParam); } else { utxos = yield sochain.getUnspentTxs(addressParam); } return utxos.map((utxo) => ({ hash: utxo.txid, index: utxo.output_no, value: xchain_util_1.assetToBase(xchain_util_1.assetAmount(utxo.value, exports.BTC_DECIMAL)).amount().toNumber(), witnessUtxo: { value: xchain_util_1.assetToBase(xchain_util_1.assetAmount(utxo.value, exports.BTC_DECIMAL)).amount().toNumber(), script: Buffer.from(utxo.script_hex, 'hex'), }, })); } let utxos = []; if (confirmedOnly) { utxos = yield haskoinApi.getConfirmedUnspentTxs({ address, haskoinUrl }); } else { utxos = yield haskoinApi.getUnspentTxs({ address, haskoinUrl }); } return utxos.map((utxo) => ({ hash: utxo.txid, index: utxo.index, value: xchain_util_1.baseAmount(utxo.value, exports.BTC_DECIMAL).amount().toNumber(), witnessUtxo: { value: xchain_util_1.baseAmount(utxo.value, exports.BTC_DECIMAL).amount().toNumber(), script: Buffer.from(utxo.pkscript, 'hex'), }, })); }); exports.scanUTXOs = scanUTXOs; /** * Build transcation. * * @param {BuildParams} params The transaction build options. * @returns {Transaction} */ const buildTx = ({ amount, recipient, memo, feeRate, sender, network, sochainUrl, haskoinUrl, spendPendingUTXO = false, // default: prevent spending uncomfirmed UTXOs }) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { try { // search only confirmed UTXOs if pending UTXO is not allowed const confirmedOnly = !spendPendingUTXO; const utxos = yield exports.scanUTXOs({ sochainUrl, haskoinUrl, network, address: sender, confirmedOnly }); if (utxos.length === 0) { return Promise.reject(Error('No utxos to send')); } if (!exports.validateAddress(recipient, network)) { return Promise.reject(new Error('Invalid address')); } const feeRateWhole = Number(feeRate.toFixed(0)); const compiledMemo = memo ? exports.compileMemo(memo) : null; const targetOutputs = []; //1. add output amount and recipient to targets targetOutputs.push({ address: recipient, value: amount.amount().toNumber(), }); //2. add output memo to targets (optional) if (compiledMemo) { targetOutputs.push({ script: compiledMemo, value: 0 }); } const { inputs, outputs } = accumulative_1.default(utxos, targetOutputs, feeRateWhole); // .inputs and .outputs will be undefined if no solution was found if (!inputs || !outputs) { return Promise.reject(Error('Insufficient Balance for transaction')); } const psbt = new Bitcoin.Psbt({ network: exports.btcNetwork(network) }); // Network-specific // psbt add input from accumulative inputs inputs.forEach((utxo) => psbt.addInput({ hash: utxo.hash, index: utxo.index, witnessUtxo: utxo.witnessUtxo, })); // psbt add outputs from accumulative outputs outputs.forEach((output) => { if (!output.address) { //an empty address means this is the change ddress output.address = sender; } if (!output.script) { psbt.addOutput(output); } else { //we need to add the compiled memo this way to //avoid dust error tx when accumulating memo output with 0 value if (compiledMemo) { psbt.addOutput({ script: compiledMemo, value: 0 }); } } }); return { psbt, utxos }; } catch (e) { return Promise.reject(e); } }); exports.buildTx = buildTx; /** * Broadcast the transaction. * * @param {BroadcastTxParams} params The transaction broadcast options. * @returns {TxHash} The transaction hash. */ const broadcastTx = ({ network, txHex, blockstreamUrl }) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { return yield blockStream.broadcastTx({ network, txHex, blockstreamUrl }); }); exports.broadcastTx = broadcastTx; /** * Calculate fees based on fee rate and memo. * * @param {FeeRate} feeRate * @param {string} memo * @returns {BaseAmount} The calculated fees based on fee rate and the memo. */ const calcFee = (feeRate, memo) => { const compiledMemo = memo ? exports.compileMemo(memo) : null; const fee = exports.getFee([], feeRate, compiledMemo); return xchain_util_1.baseAmount(fee); }; exports.calcFee = calcFee; /** * Get the default fees with rates. * * @returns {FeesWithRates} The default fees and rates. */ const getDefaultFeesWithRates = () => { const rates = { fastest: 50, fast: 20, average: 10, }; const fees = { type: 'byte', fast: exports.calcFee(rates.fast), average: exports.calcFee(rates.average), fastest: exports.calcFee(rates.fastest), }; return { fees, rates, }; }; exports.getDefaultFeesWithRates = getDefaultFeesWithRates; /** * Get the default fees. * * @returns {Fees} The default fees. */ const getDefaultFees = () => { const { fees } = exports.getDefaultFeesWithRates(); return fees; }; exports.getDefaultFees = getDefaultFees; /** * Get address prefix based on the network. * * @param {Network} network * @returns {string} The address prefix based on the network. * **/ const getPrefix = (network) => (network === 'testnet' ? 'tb1' : 'bc1'); exports.getPrefix = getPrefix; //# sourceMappingURL=utils.js.map