@abcpros/bitcore-wallet-service
Version:
A service for Mutisig HD Bitcoin Wallets
348 lines • 17.5 kB
JavaScript
"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