bitcore-wallet-service
Version:
A service for Mutisig HD Bitcoin Wallets
518 lines • 22 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EthChain = void 0;
const crypto_wallet_core_1 = require("crypto-wallet-core");
const crypto_wallet_core_2 = 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 abi_erc20_1 = require("./abi-erc20");
const abi_invoice_1 = require("./abi-invoice");
const { toBN } = crypto_wallet_core_2.Web3.utils;
const Constants = common_1.Common.Constants;
const Defaults = common_1.Common.Defaults;
function requireUncached(module) {
delete require.cache[require.resolve(module)];
return require(module);
}
const Erc20Decoder = requireUncached('abi-decoder');
Erc20Decoder.addABI(abi_erc20_1.ERC20Abi);
function getErc20Decoder() {
return Erc20Decoder;
}
const InvoiceDecoder = requireUncached('abi-decoder');
InvoiceDecoder.addABI(abi_invoice_1.InvoiceAbi);
function getInvoiceDecoder() {
return InvoiceDecoder;
}
class EthChain {
constructor() {
this.chain = 'ETH';
}
convertBitcoreBalance(bitcoreBalance, locked) {
const { unconfirmed, confirmed, balance } = bitcoreBalance;
const convertedBalance = {
totalAmount: balance,
totalConfirmedAmount: confirmed,
lockedAmount: locked,
lockedConfirmedAmount: locked,
availableAmount: balance - locked,
availableConfirmedAmount: confirmed - locked,
byAddress: []
};
return convertedBalance;
}
getSizeSafetyMargin() {
return 0;
}
getInputSizeSafetyMargin() {
return 0;
}
notifyConfirmations() {
return false;
}
supportsMultisig() {
return false;
}
getWalletBalance(server, wallet, opts, cb) {
const bc = server._getBlockchainExplorer(wallet.chain || wallet.coin, wallet.network);
if (opts.tokenAddress) {
wallet.tokenAddress = opts.tokenAddress;
}
if (opts.multisigContractAddress) {
wallet.multisigContractAddress = opts.multisigContractAddress;
opts.network = wallet.network;
}
bc.getBalance(wallet, (err, balance) => {
if (err) {
return cb(err);
}
server.getPendingTxs(opts, (err, txps) => {
if (err)
return cb(err);
let fees = 0;
let amounts = 0;
txps = txps.filter(txp => {
if (!opts.tokenAddress) {
fees += txp.fee || 0;
}
if (txp.tokenAddress && !opts.tokenAddress) {
return false;
}
amounts += txp.amount;
return true;
});
const lockedSum = (amounts + fees) || 0;
const convertedBalance = this.convertBitcoreBalance(balance, lockedSum);
server.storage.fetchAddresses(server.walletId, (err, addresses) => {
if (err)
return cb(err);
if (addresses.length > 0) {
const byAddress = [
{
address: addresses[0].address,
path: addresses[0].path,
amount: convertedBalance.totalAmount
}
];
convertedBalance.byAddress = byAddress;
}
return cb(null, convertedBalance);
});
});
});
}
getWalletSendMaxInfo(server, wallet, opts, cb) {
server.getBalance({}, (err, balance) => {
if (err)
return cb(err);
const { totalAmount, availableAmount } = balance;
let fee = opts.feePerKb * Defaults.MIN_GAS_LIMIT;
return cb(null, {
utxosBelowFee: 0,
amountBelowFee: 0,
amount: availableAmount - fee,
feePerKb: opts.feePerKb,
fee
});
});
}
getDustAmountValue() {
return 0;
}
getTransactionCount(server, wallet, from) {
return new Promise((resolve, reject) => {
server._getTransactionCount(wallet, from, (err, nonce) => {
if (err)
return reject(err);
return resolve(nonce);
});
});
}
getChangeAddress() { }
checkDust(output, opts) { }
checkScriptOutput(output) { }
getFee(server, wallet, opts) {
return new Promise(resolve => {
server._getFeePerKb(wallet, opts, async (err, inFeePerKb) => {
let feePerKb = inFeePerKb;
let gasPrice = inFeePerKb;
let maxGasFee;
let priorityGasFee;
const { from, txType, priorityFeePercentile, gasLimitBuffer } = opts;
const { coin, network, chain } = wallet;
let inGasLimit = 0;
let gasLimit = 0;
let fee = 0;
const defaultGasLimit = this.getDefaultGasLimit(opts);
let outputAddresses = [];
let outputAmounts = [];
let totalValue = toBN(0);
logger_1.default.info(`getFee for address ${from} on network ${network} and chain ${chain}`);
logger_1.default.info('getFee.opts: %o', { from, txType, priorityFeePercentile, gasLimitBuffer });
logger_1.default.info(`[${from}] Add gas limit buffer?: ${!!gasLimitBuffer}`);
for (let output of opts.outputs) {
if (opts.multiSendContractAddress) {
outputAddresses.push(output.toAddress);
outputAmounts.push(toBN(BigInt(output.amount).toString()));
if (!opts.tokenAddress) {
totalValue = totalValue.add(toBN(BigInt(output.amount).toString()));
}
inGasLimit += output.gasLimit ? output.gasLimit : defaultGasLimit;
continue;
}
if (!output.gasLimit) {
try {
const to = opts.payProUrl
? output.toAddress
: opts.tokenAddress
? opts.tokenAddress
: opts.multisigContractAddress
? opts.multisigContractAddress
: output.toAddress;
const value = opts.tokenAddress || opts.multisigContractAddress ? 0 : output.amount;
const gasLimitEstimate = await server.estimateGas({
coin,
chain: this.chain,
network,
from,
to,
value,
data: output.data,
gasPrice
});
output.gasLimit = gasLimitEstimate || defaultGasLimit;
}
catch (err) {
output.gasLimit = defaultGasLimit;
}
}
inGasLimit += output.gasLimit;
logger_1.default.info(`[${from}][${output?.toAddress || opts?.tokenAddress}] Output level gas limit: ${output.gasLimit}`);
if (gasLimitBuffer) {
const gasBuffer = Math.ceil(output.gasLimit * (gasLimitBuffer / 100));
output.gasLimit += gasBuffer;
inGasLimit += gasBuffer;
logger_1.default.info(`[${from}][${output?.toAddress || opts?.tokenAddress}] Output gas limit with buffer: ${output.gasLimit}`);
}
if (lodash_1.default.isNumber(opts.fee)) {
gasPrice = feePerKb = Number((opts.fee / (inGasLimit || defaultGasLimit)).toFixed());
}
fee += feePerKb * output.gasLimit;
}
gasLimit = inGasLimit;
logger_1.default.info(`[${from}] Current top level gas limit: ${gasLimit}`);
if (opts.multiSendContractAddress) {
let _gasLimit;
try {
const data = this.encodeContractParameters(Constants.BITPAY_CONTRACTS.MULTISEND, { addresses: outputAddresses, amounts: outputAmounts }, opts);
_gasLimit = await server.estimateGas({
coin,
chain: this.chain,
network,
from,
to: opts.multiSendContractAddress,
value: totalValue.toString(),
data,
gasPrice
});
logger_1.default.info(`[${from}] Estimated multisend gas limit: ${_gasLimit}`);
}
catch (error) {
logger_1.default.error('Error estimating gas for MultiSend contract: %o', error);
}
const buffer = gasLimitBuffer ? gasLimitBuffer / 100 : Defaults.MS_GAS_LIMIT_BUFFER_PERCENT;
gasLimit = _gasLimit || gasLimit;
gasLimit += Math.ceil(gasLimit * buffer);
fee += feePerKb * gasLimit;
logger_1.default.info(`[${from}] Top level gas limit with buffer: ${gasLimit}`);
}
if (Number(txType) === 2) {
maxGasFee = await server.estimateFee({ network, chain: wallet.chain || coin, txType: 2 });
priorityGasFee = await server.estimatePriorityFee({ network, chain: wallet.chain || coin, percentile: priorityFeePercentile || 15 });
}
return resolve({ feePerKb, gasPrice, gasLimit, maxGasFee, priorityGasFee, fee });
});
});
}
getBitcoreTx(txp, opts = { signed: true }) {
const { data, outputs, payProUrl, tokenAddress, multisigContractAddress, multiSendContractAddress, isTokenSwap, multiTx } = txp;
if (multiTx) {
throw errordefinitions_1.Errors.MULTI_TX_UNSUPPORTED;
}
const isERC20 = tokenAddress && !payProUrl && !isTokenSwap;
const isETHMULTISIG = multisigContractAddress;
const chain = isETHMULTISIG ? `${this.chain}MULTISIG` : isERC20 ? `${this.chain}ERC20` : this.chain;
const recipients = outputs.map(output => {
return {
amount: output.amount,
address: output.toAddress,
data: output.data,
gasLimit: output.gasLimit
};
});
if (data) {
recipients[0].data = data;
}
const unsignedTxs = [];
if (multiSendContractAddress) {
let multiSendParams = {
nonce: Number(txp.nonce),
recipients,
contractAddress: multiSendContractAddress
};
unsignedTxs.push(crypto_wallet_core_1.Transactions.create({ ...txp, chain, ...multiSendParams }));
}
else {
for (let index = 0; index < recipients.length; index++) {
let params = {
...recipients[index],
nonce: Number(txp.nonce) + Number(index),
recipients: [recipients[index]]
};
unsignedTxs.push(crypto_wallet_core_1.Transactions.create({ ...txp, chain, ...params }));
}
}
let tx = {
uncheckedSerialize: () => unsignedTxs,
txid: () => txp.txid,
toObject: () => {
let ret = lodash_1.default.clone(txp);
ret.outputs[0].satoshis = ret.outputs[0].amount;
return ret;
},
getFee: () => {
return txp.fee;
},
getChangeOutput: () => null
};
if (opts.signed) {
const sigs = txp.getCurrentSignatures();
sigs.forEach(x => {
this.addSignaturesToBitcoreTx(tx, txp.inputs, txp.inputPaths, x.signatures, x.xpub);
});
}
return tx;
}
getDefaultGasLimit(opts) {
let defaultGasLimit = opts.tokenAddress ? Defaults.DEFAULT_ERC20_GAS_LIMIT : Defaults.DEFAULT_GAS_LIMIT;
if (opts.multiSendContractAddress) {
defaultGasLimit = opts.tokenAddress
? Defaults.DEFAULT_MULTISEND_RECIPIENT_ERC20_GAS_LIMIT
: Defaults.DEFAULT_MULTISEND_RECIPIENT_GAS_LIMIT;
}
return defaultGasLimit;
}
encodeContractParameters(contract, params, opts) {
if (contract === Constants.BITPAY_CONTRACTS.MULTISEND) {
const web3 = new crypto_wallet_core_2.Web3();
return {
addresses: web3.eth.abi.encodeParameter('address[]', params.addresses),
amounts: web3.eth.abi.encodeParameter('uint256[]', params.amounts),
method: opts.tokenAddress ? 'sendErc20' : 'sendEth',
tokenAddress: opts.tokenAddress,
type: Constants.BITPAY_CONTRACTS.MULTISEND
};
}
}
convertFeePerKb(p, feePerKb) {
return [p, feePerKb];
}
checkTx(txp) {
try {
const tx = this.getBitcoreTx(txp);
}
catch (ex) {
logger_1.default.debug('Error building Bitcore transaction: %o', ex);
return ex;
}
return null;
}
checkTxUTXOs(server, txp, opts, cb) {
return cb();
}
selectTxInputs(server, txp, wallet, opts, cb) {
server.getBalance({ wallet, tokenAddress: opts.tokenAddress, multisigContractAddress: opts.multisigContractAddress }, (err, balance) => {
if (err)
return cb(err);
const getInvoiceValue = txp => {
let totalAmount;
txp.outputs.forEach(output => {
const decodedData = getInvoiceDecoder().decodeMethod(output.data);
if (decodedData && decodedData.name === 'pay') {
totalAmount = decodedData.params[0].value;
}
});
return totalAmount;
};
const { totalAmount, availableAmount } = balance;
const txpTotalAmount = (opts.multisigContractAddress || opts.tokenAddress) && txp.payProUrl
? getInvoiceValue(txp)
: txp.getTotalAmount();
if (totalAmount < txpTotalAmount) {
return cb(errordefinitions_1.Errors.INSUFFICIENT_FUNDS);
}
else if (availableAmount < txpTotalAmount) {
return cb(errordefinitions_1.Errors.LOCKED_FUNDS);
}
else {
if (opts.tokenAddress || opts.multisigContractAddress) {
server.getBalance({}, (err, ethBalance) => {
if (err)
return cb(err);
const { totalAmount, availableAmount } = ethBalance;
if (totalAmount < txp.fee) {
return cb(this.getInsufficientFeeError(txp));
}
else if (availableAmount < txp.fee) {
return cb(this.getLockedFeeError(txp));
}
else {
return cb(this.checkTx(txp));
}
});
}
else if (availableAmount - txp.fee < txpTotalAmount) {
return cb(new clienterror_1.ClientError(errordefinitions_1.Errors.codes.INSUFFICIENT_FUNDS_FOR_FEE, `${errordefinitions_1.Errors.INSUFFICIENT_FUNDS_FOR_FEE.message}. RequiredFee: ${txp.fee}`, {
requiredFee: txp.fee
}));
}
else {
return cb(this.checkTx(txp));
}
}
});
}
getInsufficientFeeError(txp) {
return new clienterror_1.ClientError(errordefinitions_1.Errors.codes[`INSUFFICIENT_${this.chain}_FEE`], `${errordefinitions_1.Errors[`INSUFFICIENT_${this.chain}_FEE`].message}. RequiredFee: ${txp.fee}`, {
requiredFee: txp.fee
});
}
getLockedFeeError(txp) {
return new clienterror_1.ClientError(errordefinitions_1.Errors.codes[`LOCKED_${this.chain}_FEE`], `${errordefinitions_1.Errors[`LOCKED_${this.chain}_FEE`].message}. RequiredFee: ${txp.fee}`, {
requiredFee: txp.fee
});
}
checkUtxos(opts) { }
checkValidTxAmount(output) {
try {
if (output.amount == null ||
output.amount < 0 ||
isNaN(output.amount) ||
crypto_wallet_core_2.Web3.utils.toBN(BigInt(output.amount).toString()).toString() !== BigInt(output.amount).toString()) {
throw new Error('output.amount is not a valid value: ' + output.amount);
}
return true;
}
catch (err) {
logger_1.default.warn(`Invalid output amount (${output.amount}) in checkValidTxAmount: $o`, err);
return false;
}
}
isUTXOChain() {
return false;
}
isSingleAddress() {
return true;
}
addressFromStorageTransform(network, address) {
if (network != 'livenet') {
const x = address.address.indexOf(':');
if (x >= 0) {
address.address = address.address.substr(0, x);
}
}
}
addressToStorageTransform(network, address) {
if (network != 'livenet')
address.address += ':' + network;
}
addSignaturesToBitcoreTx(tx, inputs, inputPaths, signatures, xpub) {
if (signatures.length === 0) {
throw new Error('Signatures Required');
}
const unsignedTxs = tx.uncheckedSerialize();
const signedTxs = [];
for (let index = 0; index < signatures.length; index++) {
const signed = crypto_wallet_core_1.Transactions.applySignature({
chain: this.chain,
tx: unsignedTxs[index],
signature: signatures[index]
});
signedTxs.push(signed);
tx.id = crypto_wallet_core_1.Transactions.getHash({ tx: signed, chain: this.chain });
}
tx.uncheckedSerialize = () => signedTxs;
}
validateAddress(wallet, inaddr, opts) {
const chain = this.chain.toLowerCase();
const isValidTo = crypto_wallet_core_1.Validation.validateAddress(chain, wallet.network, inaddr);
if (!isValidTo) {
throw errordefinitions_1.Errors.INVALID_ADDRESS;
}
const isValidFrom = crypto_wallet_core_1.Validation.validateAddress(chain, wallet.network, opts.from);
if (!isValidFrom) {
throw errordefinitions_1.Errors.INVALID_ADDRESS;
}
return;
}
onCoin(coin) {
return null;
}
onTx(tx) {
let tokenAddress;
let multisigContractAddress;
let address;
let amount;
if (tx.effects && tx.effects.length) {
const multisigConfirm = tx.effects.findIndex(e => e.type === 'MULTISIG:confirmTransaction');
const multisigSubmit = tx.effects.findIndex(e => e.type === 'MULTISIG:submitTransaction');
const erc20Transfer = tx.effects.findIndex(e => e.type === 'ERC20:transer');
const internalTransfer = tx.effects.findIndex(e => !e.type && !e.contractAddress);
const exists = [multisigConfirm, multisigSubmit, erc20Transfer, internalTransfer].filter(i => i > -1);
if (exists.length) {
const best = tx.effects[exists[0]];
if (best.type == 'MULTISIG:confirmTransaction' || best.type == 'MULTISIG:submitTransaction') {
multisigContractAddress = best.contractAddress;
address = best.to;
amount = best.amount;
}
else if (best.type == 'ERC20:transfer') {
tokenAddress = best.contractAddress;
address = best.to;
amount = best.amount;
}
else {
address = best.to;
amount = best.amount;
}
}
}
if (!address) {
address = tx.to;
amount = tx.value;
}
return {
txid: tx.txid,
out: {
address,
amount,
tokenAddress,
multisigContractAddress
}
};
}
getReserve(server, wallet, cb) {
return cb(null, 0);
}
refreshTxData(_server, txp, _opts, cb) {
return cb(null, txp);
}
}
exports.EthChain = EthChain;
//# sourceMappingURL=index.js.map