UNPKG

@abcpros/bitcore-wallet-service

Version:
348 lines 17.5 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __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; var crypto_wallet_core_1 = require("@abcpros/crypto-wallet-core"); var async = __importStar(require("async")); var lodash_1 = __importDefault(require("lodash")); var logger_1 = __importDefault(require("../../logger")); var model_1 = require("../../model"); var btc_1 = require("../btc"); var $ = require('preconditions').singleton(); var clienterror_1 = require("../../errors/clienterror"); var Common = require('../../common'); var Constants = Common.Constants; var Utils = Common.Utils; var Defaults = Common.Defaults; var Errors = require('../../errors/errordefinitions'); var DogeChain = (function (_super) { __extends(DogeChain, _super); function DogeChain(bitcoreLibDoge) { if (bitcoreLibDoge === void 0) { bitcoreLibDoge = crypto_wallet_core_1.BitcoreLibDoge; } var _this = _super.call(this, crypto_wallet_core_1.BitcoreLibDoge) || this; _this.bitcoreLibDoge = bitcoreLibDoge; return _this; } DogeChain.prototype.selectTxInputs = function (server, txp, wallet, opts, cb) { var _this = this; var 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)); } var feeOpts = { conservativeEstimation: opts.payProUrl ? true : false }; var txpAmount = txp.getTotalAmount(); var baseTxpSize = this.getEstimatedSize(txp, feeOpts); var baseTxpFee = (baseTxpSize * txp.feePerKb) / 1000; var sizePerInput = this.getEstimatedSizeForSingleInput(txp, feeOpts); var feePerInput = (sizePerInput * txp.feePerKb) / 1000; logger_1.default.debug("Amount " + Utils.formatAmountInBtc(txpAmount) + " baseSize " + baseTxpSize + " baseTxpFee " + baseTxpFee + " sizePerInput " + sizePerInput + " feePerInput " + feePerInput); var sanitizeUtxos = function (utxos) { var excludeIndex = lodash_1.default.reduce(opts.utxosToExclude, function (res, val) { res[val] = val; return res; }, {}); return lodash_1.default.filter(utxos, function (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; }); }; var select = function (utxos, coin, cb) { var totalValueInUtxos = lodash_1.default.sumBy(utxos, 'satoshis'); var 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(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(Errors.codes.INSUFFICIENT_FUNDS_FOR_FEE, Errors.INSUFFICIENT_FUNDS_FOR_FEE.message + ". RequiredFee: " + baseTxpFee + " Coin: " + txp.coin + " feePerKb: " + txp.feePerKb + " Err2", { coin: txp.coin, feePerKb: txp.feePerKb, requiredFee: baseTxpFee })); } var bigInputThreshold = txpAmount * Defaults.UTXO_SELECTION_MAX_SINGLE_UTXO_FACTOR + (baseTxpFee + feePerInput); logger_1.default.debug('Big input threshold ' + Utils.formatAmountInBtc(bigInputThreshold)); var partitions = lodash_1.default.partition(utxos, function (utxo) { return utxo.satoshis > bigInputThreshold; }); var bigInputs = lodash_1.default.sortBy(partitions[0], 'satoshis'); var smallInputs = lodash_1.default.sortBy(partitions[1], function (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) + ')'); var total = 0; var netTotal = -baseTxpFee; var selected = []; var fee; var error; lodash_1.default.each(smallInputs, function (input, i) { logger_1.default.debug('Input #' + i + ': ' + Utils.formatUtxos(input)); var netInputAmount = input.satoshis - feePerInput; logger_1.default.debug('The input contributes ' + Utils.formatAmountInBtc(netInputAmount)); selected.push(input); total += input.satoshis; netTotal += netInputAmount; var txpSize = baseTxpSize + selected.length * sizePerInput; fee = Math.round(baseTxpFee + selected.length * feePerInput); logger_1.default.debug('Tx size: ' + Utils.formatSize(txpSize) + ', Tx fee: ' + Utils.formatAmountInBtc(fee)); var feeVsAmountRatio = fee / txpAmount; var amountVsUtxoRatio = netInputAmount / txpAmount; if (txpSize / 1000 > MAX_TX_SIZE_IN_KB) { error = 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) { var 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) { var changeAmount = Math.round(total - txpAmount - fee); logger_1.default.debug('Tx change: ', Utils.formatAmountInBtc(changeAmount)); var 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)) { var input = lodash_1.default.head(bigInputs); logger_1.default.debug('Using big input: ', 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(Errors.codes.INSUFFICIENT_FUNDS_FOR_FEE, 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({}, function (err, utxos) { if (err) return cb(err); var totalAmount; var availableAmount; var 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(Errors.INSUFFICIENT_FUNDS); if (availableAmount < txp.getTotalAmount()) return cb(Errors.LOCKED_FUNDS); utxos = sanitizeUtxos(utxos); var groups = [6, 1]; if (!txp.excludeUnconfirmedUtxos) groups.push(0); var inputs = []; var fee; var selectionError; var i = 0; var lastGroupLength; async.whilst(function () { return i < groups.length && lodash_1.default.isEmpty(inputs); }, function (next) { var group = groups[i++]; var candidateUtxos = lodash_1.default.filter(utxos, function (utxo) { return utxo.confirmations >= group; }); if (lastGroupLength === candidateUtxos.length) { return next(); } lastGroupLength = candidateUtxos.length; select(candidateUtxos, txp.coin, function (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(); }); }, function (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) { var 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', err); } return cb(err); }); }); }; DogeChain.prototype.getWalletSendMaxInfo = function (server, wallet, opts, cb) { var _this = this; server.getUtxosForCurrentWallet({}, function (err, utxos) { if (err) return cb(err); var MAX_TX_SIZE_IN_KB = Defaults.MAX_TX_SIZE_IN_KB_DOGE; var info = { size: 0, amount: 0, fee: 0, feePerKb: 0, inputs: [], utxosBelowFee: 0, amountBelowFee: 0, utxosAboveMaxSize: 0, amountAboveMaxSize: 0 }; var inputs = lodash_1.default.reject(utxos, 'locked'); if (!!opts.excludeUnconfirmedUtxos) { inputs = lodash_1.default.filter(inputs, 'confirmations'); } inputs = lodash_1.default.sortBy(inputs, function (input) { return -input.satoshis; }); if (lodash_1.default.isEmpty(inputs)) return cb(null, info); server._getFeePerKb(wallet, opts, function (err, feePerKb) { if (err) return cb(err); info.feePerKb = feePerKb; var txp = model_1.TxProposal.create({ walletId: server.walletId, coin: wallet.coin, addressType: wallet.addressType, network: wallet.network, walletM: wallet.m, walletN: wallet.n, feePerKb: feePerKb }); var baseTxpSize = _this.getEstimatedSize(txp, { conservativeEstimation: true }); var sizePerInput = _this.getEstimatedSizeForSingleInput(txp, { conservativeEstimation: true }); var feePerInput = (sizePerInput * txp.feePerKb) / 1000; var partitionedByAmount = lodash_1.default.partition(inputs, function (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, function (input, i) { var 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); var fee = _this.getEstimatedFee(txp, { conservativeEstimation: true }); var 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); }); }); }; return DogeChain; }(btc_1.BtcChain)); exports.DogeChain = DogeChain; //# sourceMappingURL=index.js.map