UNPKG

bitcore-wallet-service

Version:
823 lines 37.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BtcChain = void 0; const async = __importStar(require("async")); const crypto_wallet_core_1 = require("crypto-wallet-core"); const lodash_1 = __importDefault(require("lodash")); const config_1 = __importDefault(require("../../../config")); const common_1 = require("../../common"); const clienterror_1 = require("../../errors/clienterror"); const errordefinitions_1 = require("../../errors/errordefinitions"); const logger_1 = __importDefault(require("../../logger")); const model_1 = require("../../model"); const $ = require('preconditions').singleton(); const Constants = common_1.Common.Constants; const Utils = common_1.Common.Utils; const Defaults = common_1.Common.Defaults; class BtcChain { constructor(bitcoreLib = crypto_wallet_core_1.BitcoreLib) { this.bitcoreLib = bitcoreLib; this.sizeEstimationMargin = config_1.default.btc?.sizeEstimationMargin ?? 0.01; this.inputSizeEstimationMargin = config_1.default.btc?.inputSizeEstimationMargin ?? 2; } getSizeSafetyMargin(opts = {}) { if (opts.conservativeEstimation) { return this.sizeEstimationMargin; } return 0; } getInputSizeSafetyMargin(opts = {}) { if (opts.conservativeEstimation) { return this.inputSizeEstimationMargin; } return 0; } getWalletBalance(server, wallet, opts, cb) { server.getUtxosForCurrentWallet({ coin: opts.coin, addresses: opts.addresses }, (err, utxos) => { if (err) return cb(err); const balance = { ...this.totalizeUtxos(utxos), byAddress: [] }; const byAddress = {}; lodash_1.default.each(lodash_1.default.keyBy(lodash_1.default.sortBy(utxos, 'address'), 'address'), (value, key) => { byAddress[key] = { address: key, path: value.path, amount: 0 }; }); lodash_1.default.each(utxos, utxo => { byAddress[utxo.address].amount += utxo.satoshis; }); balance.byAddress = lodash_1.default.values(byAddress); return cb(null, balance); }); } getWalletSendMaxInfo(server, wallet, opts, cb) { server.getUtxosForCurrentWallet({}, (err, utxos) => { if (err) return cb(err); const MAX_TX_SIZE_IN_KB = Defaults.MAX_TX_SIZE_IN_KB_BTC; const info = { size: 0, amount: 0, fee: 0, feePerKb: 0, inputs: [], utxosBelowFee: 0, amountBelowFee: 0, utxosAboveMaxSize: 0, amountAboveMaxSize: 0 }; let inputs = lodash_1.default.reject(utxos, 'locked'); if (!!opts.excludeUnconfirmedUtxos) { inputs = lodash_1.default.filter(inputs, 'confirmations'); } inputs = lodash_1.default.sortBy(inputs, input => { return -input.satoshis; }); if (lodash_1.default.isEmpty(inputs)) return cb(null, info); server._getFeePerKb(wallet, opts, (err, feePerKb) => { if (err) return cb(err); info.feePerKb = feePerKb; const txp = model_1.TxProposal.create({ walletId: server.walletId, coin: wallet.coin, addressType: wallet.addressType, network: wallet.network, walletM: wallet.m, walletN: wallet.n, feePerKb }); const baseTxpSize = this.getEstimatedSize(txp, { conservativeEstimation: true }); const sizePerInput = this.getEstimatedSizeForSingleInput(txp, { conservativeEstimation: true }); const feePerInput = (sizePerInput * txp.feePerKb) / 1000; const partitionedByAmount = lodash_1.default.partition(inputs, input => { return input.satoshis > feePerInput; }); info.utxosBelowFee = partitionedByAmount[1].length; info.amountBelowFee = lodash_1.default.sumBy(partitionedByAmount[1], 'satoshis'); inputs = partitionedByAmount[0]; lodash_1.default.each(inputs, (input, i) => { const sizeInKb = (baseTxpSize + (i + 1) * sizePerInput) / 1000; if (sizeInKb > MAX_TX_SIZE_IN_KB) { info.utxosAboveMaxSize = inputs.length - i; info.amountAboveMaxSize = lodash_1.default.sumBy(lodash_1.default.slice(inputs, i), 'satoshis'); return false; } txp.inputs.push(input); }); if (lodash_1.default.isEmpty(txp.inputs)) return cb(null, info); const fee = this.getEstimatedFee(txp, { conservativeEstimation: true }); const amount = lodash_1.default.sumBy(txp.inputs, 'satoshis') - fee; if (amount < Defaults.MIN_OUTPUT_AMOUNT) return cb(null, info); info.size = this.getEstimatedSize(txp, { conservativeEstimation: true }); info.fee = fee; info.amount = amount; if (opts.returnInputs) { info.inputs = lodash_1.default.shuffle(txp.inputs); } return cb(null, info); }); }); } getDustAmountValue() { return this.bitcoreLib.Transaction.DUST_AMOUNT; } getTransactionCount() { return null; } getChangeAddress(server, wallet, opts) { return new Promise((resolve, reject) => { const getChangeAddress = (wallet, cb) => { if (wallet.singleAddress) { server.storage.fetchAddresses(server.walletId, (err, addresses) => { if (err) return cb(err); if (lodash_1.default.isEmpty(addresses)) return cb(new clienterror_1.ClientError('The wallet has no addresses')); return cb(null, lodash_1.default.head(addresses)); }); } else { if (opts.changeAddress) { try { this.validateAddress(wallet, opts.changeAddress, opts); } catch (addrErr) { return cb(addrErr); } server.storage.fetchAddressByWalletId(wallet.id, opts.changeAddress, (err, address) => { if (err || !address) return cb(errordefinitions_1.Errors.INVALID_CHANGE_ADDRESS); return cb(null, address); }); } else { const escrowInputs = opts.instantAcceptanceEscrow ? opts.inputs : undefined; return cb(null, wallet.createAddress(true, undefined, escrowInputs), true); } } }; getChangeAddress(wallet, (err, address, isNew) => { if (err) return reject(err); return resolve(address); }); }); } checkDust(output) { const dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, this.bitcoreLib.Transaction.DUST_AMOUNT); if (output.amount < dustThreshold) { return errordefinitions_1.Errors.DUST_AMOUNT; } } checkScriptOutput(output) { if (output.script) { if (typeof output.script !== 'string') { return errordefinitions_1.Errors.SCRIPT_TYPE; } if (!output.script.startsWith('6a')) { return errordefinitions_1.Errors.SCRIPT_OP_RETURN; } if (output.script.startsWith('6a') && output.amount != 0) { return errordefinitions_1.Errors.SCRIPT_OP_RETURN_AMOUNT; } } } getEstimatedSizeForSingleInput(txp, opts = { conservativeEstimation: false }) { const SIGNATURE_SIZE = 72 + 1; const PUBKEY_SIZE = 33 + 1; const inputSafetyMargin = this.getInputSizeSafetyMargin({ conservativeEstimation: opts.conservativeEstimation }); switch (txp.addressType) { case Constants.SCRIPT_TYPES.P2PKH: return 148 + inputSafetyMargin; case Constants.SCRIPT_TYPES.P2WPKH: return 69 + inputSafetyMargin; case Constants.SCRIPT_TYPES.P2TR: return 58 + inputSafetyMargin; case Constants.SCRIPT_TYPES.P2WSH: return Math.ceil(32 + 4 + 1 + (5 + txp.requiredSignatures * 74 + txp.walletN * 34) / 4 + 4) + inputSafetyMargin; case Constants.SCRIPT_TYPES.P2SH: return 46 + txp.requiredSignatures * SIGNATURE_SIZE + txp.walletN * PUBKEY_SIZE + inputSafetyMargin; default: logger_1.default.warn('Unknown address type at getEstimatedSizeForSingleInput: %o', txp.addressType); return 46 + txp.requiredSignatures * SIGNATURE_SIZE + txp.walletN * PUBKEY_SIZE + inputSafetyMargin; } } getEstimatedSizeForSingleOutput(address) { let addressType = ''; if (address) { const a = this.bitcoreLib.Address(address); addressType = a.type; } return this.getEstimatedSizeForAddressType(addressType); } getEstimatedSizeForAddressType(addressType) { let scriptSize; switch (addressType) { case 'pubkeyhash': scriptSize = 25; break; case 'scripthash': scriptSize = 23; break; case 'witnesspubkeyhash': scriptSize = 22; break; case 'witnessscripthash': scriptSize = 34; break; default: scriptSize = 34; break; } return scriptSize + 8 + 1; } getEstimatedSize(txp, opts) { const overhead = 4 + 4 + 1 + 1; const inputSize = this.getEstimatedSizeForSingleInput(txp, opts); const nbInputs = txp.inputs.length; let outputsSize = 0; let outputs = lodash_1.default.isArray(txp.outputs) ? txp.outputs : [txp.toAddress]; let addresses = outputs.map(x => x.toAddress); if (txp.changeAddress) { addresses.push(txp.changeAddress.address); } lodash_1.default.each(addresses, x => { outputsSize += this.getEstimatedSizeForSingleOutput(x); }); if (opts && opts.instantAcceptanceEscrow) { outputsSize += this.getEstimatedSizeForAddressType('scripthash'); } if (!outputsSize) { outputsSize = this.getEstimatedSizeForSingleOutput(); } const size = overhead + inputSize * nbInputs + outputsSize; return Math.ceil(size * 1 + this.getSizeSafetyMargin(opts)); } getEstimatedFee(txp, opts) { $.checkState(lodash_1.default.isNumber(txp.feePerKb), 'Failed state: txp.feePerKb is not a number at <getEstimatedFee()>'); let fee; if (txp.inputs.length && !txp.changeAddress && txp.outputs.length) { const totalInputs = lodash_1.default.sumBy(txp.inputs, 'satoshis'); const totalOutputs = lodash_1.default.sumBy(txp.outputs, 'amount'); if (totalInputs && totalOutputs) { fee = totalInputs - totalOutputs; } } if (!fee) { fee = (txp.feePerKb * this.getEstimatedSize(txp, opts)) / 1000; fee = Math.max(fee, this.bitcoreLib.Transaction.DUST_AMOUNT); } return parseInt(fee.toFixed(0)); } getFee(server, wallet, opts) { return new Promise(resolve => { server._getFeePerKb(wallet, opts, (err, feePerKb) => { return resolve({ feePerKb }); }); }); } getBitcoreTx(txp, opts = { signed: true }) { const t = new this.bitcoreLib.Transaction(); if (txp.version <= 3) { t.setVersion(1); } else { t.setVersion(2); if (txp.lockUntilBlockHeight) t.lockUntilBlockHeight(txp.lockUntilBlockHeight); } if (txp.multiTx) { throw errordefinitions_1.Errors.MULTI_TX_UNSUPPORTED; } let inputs = txp.inputs.map(x => { return { address: x.address, txid: x.txid, vout: x.vout, outputIndex: x.outputIndex, scriptPubKey: x.scriptPubKey, satoshis: x.satoshis, publicKeys: x.publicKeys }; }); switch (txp.addressType) { case Constants.SCRIPT_TYPES.P2WSH: case Constants.SCRIPT_TYPES.P2SH: for (const i of inputs) { $.checkState(i.publicKeys, 'Failed state: Inputs should include public keys at <getBitcoreTx()>'); t.from(i, i.publicKeys, txp.requiredSignatures); } break; case Constants.SCRIPT_TYPES.P2WPKH: case Constants.SCRIPT_TYPES.P2PKH: case Constants.SCRIPT_TYPES.P2TR: t.from(inputs); break; } for (const o of txp.outputs || []) { $.checkState(o.script || o.toAddress, 'Failed state: Output should have either toAddress or script specified at <getBitcoreTx()>'); if (o.script) { t.addOutput(new this.bitcoreLib.Transaction.Output({ script: o.script, satoshis: o.amount })); } else { t.to(o.toAddress, o.amount); } } t.fee(txp.fee); if (txp.instantAcceptanceEscrow && txp.escrowAddress) { t.escrow(txp.escrowAddress.address, txp.instantAcceptanceEscrow + txp.fee); } if (txp.enableRBF) t.enableRBF(); if (txp.changeAddress) { t.change(txp.changeAddress.address); } if (t.outputs.length > 1) { const outputOrder = lodash_1.default.reject(txp.outputOrder, (order) => { return order >= t.outputs.length; }); $.checkState(t.outputs.length == outputOrder.length, 'Failed state: t.outputs.length not equal to outputOrder.length at <getBitcoreTx()>'); t.sortOutputs(outputs => { return lodash_1.default.map(outputOrder, i => { return outputs[i]; }); }); } const totalInputs = lodash_1.default.sumBy(t.inputs, 'output.satoshis'); const totalOutputs = lodash_1.default.sumBy(t.outputs, 'satoshis'); $.checkState(totalInputs > 0 && totalOutputs > 0 && totalInputs >= totalOutputs, 'Failed state: not-enough-inputs at <getBitcoreTx()>'); $.checkState(totalInputs - totalOutputs <= Defaults.MAX_TX_FEE[txp.coin], 'Failed state: fee-too-high at <getBitcoreTx()>'); if (opts.signed) { const sigs = txp.getCurrentSignatures(); lodash_1.default.each(sigs, x => { this.addSignaturesToBitcoreTx(t, txp.inputs, txp.inputPaths, x.signatures, x.xpub, txp.signingMethod); }); } return t; } convertFeePerKb(p, feePerKb) { return [p, Utils.strip(feePerKb * 1e8)]; } checkTx(txp) { let bitcoreError; const MAX_TX_SIZE_IN_KB = Defaults.MAX_TX_SIZE_IN_KB_BTC; if (this.getEstimatedSize(txp, { conservativeEstimation: true }) / 1000 > MAX_TX_SIZE_IN_KB) return errordefinitions_1.Errors.TX_MAX_SIZE_EXCEEDED; const serializationOpts = { disableIsFullySigned: true, disableSmallFees: true, disableLargeFees: true, disableDustOutputs: false }; if (txp.outputs && Array.isArray(txp.outputs)) { for (let output of txp.outputs) { if (output.script && output.script.startsWith('6a')) { serializationOpts.disableDustOutputs = true; } } } if (lodash_1.default.isEmpty(txp.inputPaths)) return errordefinitions_1.Errors.NO_INPUT_PATHS; try { const bitcoreTx = this.getBitcoreTx(txp); bitcoreError = bitcoreTx.getSerializationError(serializationOpts); if (!bitcoreError) { txp.fee = bitcoreTx.getFee(); } } catch (ex) { logger_1.default.warn('Error building Bitcore transaction: %o', ex); return ex; } if (bitcoreError instanceof this.bitcoreLib.errors.Transaction.FeeError) { return new clienterror_1.ClientError(errordefinitions_1.Errors.codes.INSUFFICIENT_FUNDS_FOR_FEE, `${errordefinitions_1.Errors.INSUFFICIENT_FUNDS_FOR_FEE.message}. RequiredFee: ${txp.fee} Coin: ${txp.coin} feePerKb: ${txp.feePerKb} Err1`, { coin: txp.coin, feePerKb: txp.feePerKb, requiredFee: txp.fee }); } if (bitcoreError instanceof this.bitcoreLib.errors.Transaction.DustOutputs) return errordefinitions_1.Errors.DUST_AMOUNT; return bitcoreError; } checkTxUTXOs(server, txp, opts, cb) { logger_1.default.debug('Rechecking UTXOs availability for publishTx'); if (txp.replaceTxByFee) { logger_1.default.debug('Ignoring spend utxos check (Replacing tx designated as RBF)'); return cb(); } const utxoKey = utxo => { return utxo.txid + '|' + utxo.vout; }; server.getUtxosForCurrentWallet({ addresses: txp.inputs }, (err, utxos) => { if (err) return cb(err); const txpInputs = lodash_1.default.map(txp.inputs, utxoKey); const utxosIndex = lodash_1.default.keyBy(utxos, utxoKey); const unavailable = lodash_1.default.some(txpInputs, i => { const utxo = utxosIndex[i]; return !utxo || utxo.locked; }); if (unavailable) return cb(errordefinitions_1.Errors.UNAVAILABLE_UTXOS); return cb(); }); } totalizeUtxos(utxos) { const balance = { totalAmount: lodash_1.default.sumBy(utxos, 'satoshis'), lockedAmount: lodash_1.default.sumBy(lodash_1.default.filter(utxos, 'locked'), 'satoshis'), totalConfirmedAmount: lodash_1.default.sumBy(lodash_1.default.filter(utxos, 'confirmations'), 'satoshis'), lockedConfirmedAmount: lodash_1.default.sumBy(lodash_1.default.filter(lodash_1.default.filter(utxos, 'locked'), 'confirmations'), 'satoshis'), availableAmount: undefined, availableConfirmedAmount: undefined }; balance.availableAmount = balance.totalAmount - balance.lockedAmount; balance.availableConfirmedAmount = balance.totalConfirmedAmount - balance.lockedConfirmedAmount; return balance; } selectTxInputs(server, txp, wallet, opts, cb) { const MAX_TX_SIZE_IN_KB = Defaults.MAX_TX_SIZE_IN_KB_BTC; if (txp.inputs && !lodash_1.default.isEmpty(txp.inputs) && !txp.replaceTxByFee) { if (!lodash_1.default.isNumber(txp.fee)) txp.fee = this.getEstimatedFee(txp, { conservativeEstimation: true }); return cb(this.checkTx(txp)); } const feeOpts = { conservativeEstimation: opts.payProUrl ? true : false, instantAcceptanceEscrow: opts.instantAcceptanceEscrow }; const escrowAmount = opts.instantAcceptanceEscrow || 0; const txpAmount = txp.getTotalAmount() + escrowAmount; const baseTxpSize = this.getEstimatedSize(txp, feeOpts); const baseTxpFee = (baseTxpSize * txp.feePerKb) / 1000; const sizePerInput = this.getEstimatedSizeForSingleInput(txp, feeOpts); const feePerInput = (sizePerInput * txp.feePerKb) / 1000; logger_1.default.debug(`Amount ${Utils.formatAmountInBtc(txpAmount)} baseSize ${baseTxpSize} baseTxpFee ${baseTxpFee} sizePerInput ${sizePerInput} feePerInput ${feePerInput}`); const sanitizeUtxos = utxos => { const excludeIndex = lodash_1.default.reduce(opts.utxosToExclude, (res, val) => { res[val] = val; return res; }, {}); return lodash_1.default.filter(utxos, utxo => { if (utxo.locked) return false; if (txp.excludeUnconfirmedUtxos && !txp.replaceTxByFee && !utxo.confirmations) return false; if (excludeIndex[utxo.txid + ':' + utxo.vout]) return false; return true; }); }; const select = (utxos, coin, cb) => { let totalValueInUtxos = lodash_1.default.sumBy(utxos, 'satoshis'); if (totalValueInUtxos < txpAmount) { logger_1.default.debug('Total value in all utxos (' + Utils.formatAmountInBtc(totalValueInUtxos) + ') is insufficient to cover for txp amount (' + Utils.formatAmountInBtc(txpAmount) + ')'); return cb(errordefinitions_1.Errors.INSUFFICIENT_FUNDS); } utxos = lodash_1.default.filter(utxos, utxo => { if (utxo.satoshis <= feePerInput) return false; return true; }); totalValueInUtxos = lodash_1.default.sumBy(utxos, 'satoshis'); const netValueInUtxos = totalValueInUtxos - (baseTxpFee - utxos.length * feePerInput); if (netValueInUtxos < txpAmount) { logger_1.default.debug('Value after fees in all utxos (' + Utils.formatAmountInBtc(netValueInUtxos) + ') is insufficient to cover for txp amount (' + Utils.formatAmountInBtc(txpAmount) + ')'); return cb(new clienterror_1.ClientError(errordefinitions_1.Errors.codes.INSUFFICIENT_FUNDS_FOR_FEE, `${errordefinitions_1.Errors.INSUFFICIENT_FUNDS_FOR_FEE.message}. RequiredFee: ${baseTxpFee} Coin: ${txp.coin} feePerKb: ${txp.feePerKb} Err2`, { coin: txp.coin, feePerKb: txp.feePerKb, requiredFee: baseTxpFee })); } const bigInputThreshold = txpAmount * Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR + (baseTxpFee + feePerInput); logger_1.default.debug('Big input threshold ' + Utils.formatAmountInBtc(bigInputThreshold)); const partitions = lodash_1.default.partition(utxos, utxo => { return utxo.satoshis > bigInputThreshold; }); const bigInputs = lodash_1.default.sortBy(partitions[0], 'satoshis'); const smallInputs = lodash_1.default.sortBy(partitions[1], utxo => { return -utxo.satoshis; }); logger_1.default.debug('Considering ' + bigInputs.length + ' big inputs (' + Utils.formatUtxos(bigInputs) + ')'); logger_1.default.debug('Considering ' + smallInputs.length + ' small inputs (' + Utils.formatUtxos(smallInputs) + ')'); let total = 0; let netTotal = -baseTxpFee; let selected = []; let fullTxpAmount = txpAmount; let fee; let error; lodash_1.default.each(smallInputs, (input, i) => { logger_1.default.debug('Input #' + i + ': ' + Utils.formatUtxos(input)); const netInputAmount = input.satoshis - feePerInput; logger_1.default.debug('The input contributes ' + Utils.formatAmountInBtc(netInputAmount)); selected.push(input); total += input.satoshis; netTotal += netInputAmount; const txpSize = baseTxpSize + selected.length * sizePerInput; fee = Math.round(baseTxpFee + selected.length * feePerInput); fullTxpAmount = escrowAmount ? txpAmount + fee : txpAmount; logger_1.default.debug('Tx size: ' + Utils.formatSize(txpSize) + ', Tx fee: ' + Utils.formatAmountInBtc(fee)); const feeVsAmountRatio = fee / fullTxpAmount; const amountVsUtxoRatio = netInputAmount / fullTxpAmount; if (txpSize / 1000 > MAX_TX_SIZE_IN_KB) { error = errordefinitions_1.Errors.TX_MAX_SIZE_EXCEEDED; return false; } if (!lodash_1.default.isEmpty(bigInputs)) { if (amountVsUtxoRatio < Defaults.UTXO_SELECTION_MIN_TX_AMOUNT_VS_UTXO_FACTOR) { return false; } if (feeVsAmountRatio > Defaults.UTXO_SELECTION_MAX_FEE_VS_TX_AMOUNT_FACTOR) { const feeVsSingleInputFeeRatio = fee / (baseTxpFee + feePerInput); if (feeVsSingleInputFeeRatio > Defaults.UTXO_SELECTION_MAX_FEE_VS_SINGLE_UTXO_FEE_FACTOR) { return false; } } } logger_1.default.debug('Cumuled total so far: ' + Utils.formatAmountInBtc(total) + ', Net total so far: ' + Utils.formatAmountInBtc(netTotal)); if (netTotal >= fullTxpAmount) { const changeAmount = Math.round(total - fullTxpAmount - fee); logger_1.default.debug('Tx change: %o', Utils.formatAmountInBtc(changeAmount)); const dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, this.bitcoreLib.Transaction.DUST_AMOUNT); if (changeAmount > 0 && changeAmount <= dustThreshold) { logger_1.default.debug('Change below dust threshold (' + Utils.formatAmountInBtc(dustThreshold) + '). Incrementing fee to remove change.'); fee += changeAmount; } return false; } }); if (netTotal < fullTxpAmount) { logger_1.default.debug('Could not reach Txp total (' + Utils.formatAmountInBtc(fullTxpAmount) + '), still missing: ' + Utils.formatAmountInBtc(fullTxpAmount - netTotal)); selected = []; if (!lodash_1.default.isEmpty(bigInputs)) { const input = lodash_1.default.head(bigInputs); logger_1.default.debug('Using big input: %o', Utils.formatUtxos(input)); total = input.satoshis; fee = Math.round(baseTxpFee + feePerInput); netTotal = total - fee; selected = [input]; } } if (lodash_1.default.isEmpty(selected)) { return cb(error || new clienterror_1.ClientError(errordefinitions_1.Errors.codes.INSUFFICIENT_FUNDS_FOR_FEE, `${errordefinitions_1.Errors.INSUFFICIENT_FUNDS_FOR_FEE.message}. RequiredFee: ${fee} Coin: ${txp.coin} feePerKb: ${txp.feePerKb} Err3`, { coin: txp.coin, feePerKb: txp.feePerKb, requiredFee: fee })); } return cb(null, selected, fee); }; server.getUtxosForCurrentWallet({ instantAcceptanceEscrow: txp.instantAcceptanceEscrow, replaceTxByFee: txp.replaceTxByFee, inputs: txp.inputs }, (err, utxos) => { if (err) return cb(err); let totalAmount; let availableAmount; const balance = this.totalizeUtxos(utxos); if (txp.excludeUnconfirmedUtxos && !txp.replaceTxByFee) { totalAmount = balance.totalConfirmedAmount; availableAmount = balance.availableConfirmedAmount; } else { totalAmount = balance.totalAmount; availableAmount = balance.availableAmount; } if (totalAmount < txp.getTotalAmount()) return cb(errordefinitions_1.Errors.INSUFFICIENT_FUNDS); if (availableAmount < txp.getTotalAmount()) return cb(errordefinitions_1.Errors.LOCKED_FUNDS); utxos = sanitizeUtxos(utxos); const groups = [6, 1]; if (!txp.excludeUnconfirmedUtxos) groups.push(0); let inputs = []; let fee; let selectionError; let i = 0; let lastGroupLength; async.whilst(() => { return i < groups.length && lodash_1.default.isEmpty(inputs); }, next => { const group = groups[i++]; let candidateUtxos = lodash_1.default.filter(utxos, utxo => { return utxo.confirmations >= group; }); if (opts.instantAcceptanceEscrow && wallet.isZceCompatible()) { const utxosSortedByDescendingAmount = candidateUtxos.sort((a, b) => b.amount - a.amount); const utxosWithUniqueAddresses = lodash_1.default.uniqBy(utxosSortedByDescendingAmount, 'address'); candidateUtxos = utxosWithUniqueAddresses; } if (txp.replaceTxByFee) { const txIdArray = lodash_1.default.map(opts.inputs, 'txid'); candidateUtxos = candidateUtxos.sort((a, b) => { return txIdArray.indexOf(b.txid) - txIdArray.indexOf(a.txid); }); } if (lastGroupLength === candidateUtxos.length) { return next(); } lastGroupLength = candidateUtxos.length; select(candidateUtxos, txp.coin, (err, selectedInputs, selectedFee) => { if (err) { selectionError = err; return next(); } selectionError = null; inputs = selectedInputs; fee = selectedFee; logger_1.default.debug('Selected inputs from this group: ' + Utils.formatUtxos(inputs)); logger_1.default.debug('Fee for this selection: ' + Utils.formatAmountInBtc(fee)); return next(); }); }, err => { if (err) return cb(err); if (selectionError || lodash_1.default.isEmpty(inputs)) return cb(selectionError || new Error('Could not select tx inputs')); txp.setInputs(lodash_1.default.shuffle(inputs)); txp.fee = fee; err = this.checkTx(txp); if (!err) { const change = lodash_1.default.sumBy(txp.inputs, 'satoshis') - lodash_1.default.sumBy(txp.outputs, 'amount') - txp.fee; logger_1.default.debug('Successfully built transaction. Total fees: ' + Utils.formatAmountInBtc(txp.fee) + ', total change: ' + Utils.formatAmountInBtc(change)); } else { logger_1.default.warn('Error building transaction: %o', err); } return cb(err); }); }); } checkUtxos(opts) { if (lodash_1.default.isNumber(opts.fee) && lodash_1.default.isEmpty(opts.inputs)) return true; } checkValidTxAmount(output) { if (!lodash_1.default.isNumber(output.amount) || lodash_1.default.isNaN(output.amount) || output.amount <= 0) { return false; } return true; } supportsMultisig() { return true; } notifyConfirmations(network) { if (network != 'livenet') return false; return true; } isUTXOChain() { return true; } isSingleAddress() { return false; } addressFromStorageTransform(network, address) { } addressToStorageTransform(network, address) { } addSignaturesToBitcoreTx(tx, inputs, inputPaths, signatures, xpub, signingMethod) { signingMethod = signingMethod || 'ecdsa'; if (signatures.length != inputs.length) throw new Error('Number of signatures does not match number of inputs'); let i = 0; const x = new this.bitcoreLib.HDPublicKey(xpub); for (const signatureHex of signatures) { try { const signature = this.bitcoreLib.crypto.Signature.fromString(signatureHex); const pub = x.deriveChild(inputPaths[i]).publicKey; const SIGHASH_TYPE = this.bitcoreLib.crypto.Signature.SIGHASH_ALL | this.bitcoreLib.crypto.Signature.SIGHASH_FORKID; const s = { inputIndex: i, signature, sigtype: SIGHASH_TYPE, publicKey: pub }; tx.inputs[i].addSignature(tx, s, signingMethod); i++; } catch (e) { } } if (i != tx.inputs.length) throw new Error('Wrong signatures'); } validateAddress(wallet, inaddr, opts) { const A = this.bitcoreLib.Address; let addr = {}; try { addr = new A(inaddr); } catch (ex) { throw errordefinitions_1.Errors.INVALID_ADDRESS; } if (!this._isCorrectNetwork(wallet, addr)) { throw errordefinitions_1.Errors.INCORRECT_ADDRESS_NETWORK; } return; } _isCorrectNetwork(wallet, addr) { const addrNetwork = Utils.getNetworkName(wallet.chain, addr.network.toString()); const walNetwork = wallet.network; if (Utils.getNetworkType(addrNetwork) === 'testnet' && walNetwork === 'regtest') { return !!config_1.default.allowRegtest; } return addrNetwork === walNetwork; } onCoin(coin) { if (!coin || !coin.address) return; return { out: { address: coin.address, amount: coin.value }, txid: coin.mintTxid }; } onTx(tx) { return null; } getReserve(server, wallet, cb) { return cb(null, 0); } refreshTxData(_server, txp, _opts, cb) { return cb(null, txp); } } exports.BtcChain = BtcChain; //# sourceMappingURL=index.js.map