UNPKG

bitcore-wallet-service

Version:
344 lines 17.4 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.DogeChain = void 0; const async = __importStar(require("async")); const crypto_wallet_core_1 = require("crypto-wallet-core"); const lodash_1 = __importDefault(require("lodash")); 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 btc_1 = require("../btc"); const Constants = common_1.Common.Constants; const Utils = common_1.Common.Utils; const Defaults = common_1.Common.Defaults; class DogeChain extends btc_1.BtcChain { constructor(bitcoreLibDoge = crypto_wallet_core_1.BitcoreLibDoge) { super(crypto_wallet_core_1.BitcoreLibDoge); this.bitcoreLibDoge = bitcoreLibDoge; } selectTxInputs(server, txp, wallet, opts, cb) { const MAX_TX_SIZE_IN_KB = Defaults.MAX_TX_SIZE_IN_KB_DOGE; if (txp.inputs && !lodash_1.default.isEmpty(txp.inputs)) { 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 }; const txpAmount = txp.getTotalAmount(); 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 (utxo.satoshis <= feePerInput) return false; if (txp.excludeUnconfirmedUtxos && !utxo.confirmations) return false; if (excludeIndex[utxo.txid + ':' + utxo.vout]) return false; return true; }); }; const select = (utxos, coin, cb) => { const totalValueInUtxos = lodash_1.default.sumBy(utxos, 'satoshis'); const netValueInUtxos = totalValueInUtxos - (baseTxpFee - utxos.length * feePerInput); 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); } 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 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); fee = Math.max(fee, this.bitcoreLibDoge.Transaction.DUST_AMOUNT); logger_1.default.debug('Tx size: ' + Utils.formatSize(txpSize) + ', Tx fee: ' + Utils.formatAmountInBtc(fee)); const feeVsAmountRatio = fee / txpAmount; const amountVsUtxoRatio = netInputAmount / txpAmount; 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 >= txpAmount) { const changeAmount = Math.round(total - txpAmount - fee); logger_1.default.debug('Tx change: %o', Utils.formatAmountInBtc(changeAmount)); const dustThreshold = Math.max(Defaults.MIN_OUTPUT_AMOUNT, this.bitcoreLibDoge.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 < txpAmount) { logger_1.default.debug('Could not reach Txp total (' + Utils.formatAmountInBtc(txpAmount) + '), still missing: ' + Utils.formatAmountInBtc(txpAmount - 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); fee = Math.max(fee, this.bitcoreLibDoge.Transaction.DUST_AMOUNT); 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({}, (err, utxos) => { if (err) return cb(err); let totalAmount; let availableAmount; const balance = this.totalizeUtxos(utxos); if (txp.excludeUnconfirmedUtxos) { 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++]; const candidateUtxos = lodash_1.default.filter(utxos, utxo => { return utxo.confirmations >= group; }); 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); }); }); } 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_DOGE; 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; info.size = this.getEstimatedSize(txp, { conservativeEstimation: true }); info.fee = fee; info.amount = amount; if (amount < Defaults.MIN_OUTPUT_AMOUNT) return cb(null, info); if (opts.returnInputs) { info.inputs = lodash_1.default.shuffle(inputs); } return cb(null, info); }); }); } } exports.DogeChain = DogeChain; //# sourceMappingURL=index.js.map