UNPKG

bitcore-wallet-service

Version:
518 lines 22 kB
"use strict"; 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