bitcore-wallet-service
Version:
A service for Mutisig HD Bitcoin Wallets
344 lines • 17.4 kB
JavaScript
"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