import abi from 'ethereumjs-abi' import ethUtil from 'ethereumjs-util' import { addCurrencies, conversionUtil, conversionGTE, multiplyCurrencies, conversionGreaterThan, conversionLessThan, } from '../../helpers/utils/conversion-util' import { calcTokenAmount } from '../../helpers/utils/token-util' import { BASE_TOKEN_GAS_COST, INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_TOKENS_ERROR, MIN_GAS_LIMIT_HEX, NEGATIVE_ETH_ERROR, SIMPLE_GAS_COST, TOKEN_TRANSFER_FUNCTION_SIGNATURE, } from './send.constants' export { addGasBuffer, calcGasTotal, calcTokenBalance, doesAmountErrorRequireUpdate, estimateGas, generateTokenTransferData, getAmountErrorObject, getGasFeeErrorObject, getToAddressForGasUpdate, isBalanceSufficient, isTokenBalanceSufficient, removeLeadingZeroes, ellipsify, } function calcGasTotal (gasLimit = '0', gasPrice = '0') { return multiplyCurrencies(gasLimit, gasPrice, { toNumericBase: 'hex', multiplicandBase: 16, multiplierBase: 16, }) } function isBalanceSufficient ({ amount = '0x0', balance = '0x0', conversionRate = 1, gasTotal = '0x0', primaryCurrency, }) { const totalAmount = addCurrencies(amount, gasTotal, { aBase: 16, bBase: 16, toNumericBase: 'hex', }) const balanceIsSufficient = conversionGTE( { value: balance, fromNumericBase: 'hex', fromCurrency: primaryCurrency, conversionRate, }, { value: totalAmount, fromNumericBase: 'hex', conversionRate, fromCurrency: primaryCurrency, }, ) return balanceIsSufficient } function isTokenBalanceSufficient ({ amount = '0x0', tokenBalance, decimals, }) { const amountInDec = conversionUtil(amount, { fromNumericBase: 'hex', }) const tokenBalanceIsSufficient = conversionGTE( { value: tokenBalance, fromNumericBase: 'hex', }, { value: calcTokenAmount(amountInDec, decimals), }, ) return tokenBalanceIsSufficient } function getAmountErrorObject ({ amount, balance, conversionRate, gasTotal, primaryCurrency, sendToken, tokenBalance, }) { let insufficientFunds = false if (gasTotal && conversionRate && !sendToken) { insufficientFunds = !isBalanceSufficient({ amount, balance, conversionRate, gasTotal, primaryCurrency, }) } let inSufficientTokens = false if (sendToken && tokenBalance !== null) { const { decimals } = sendToken inSufficientTokens = !isTokenBalanceSufficient({ tokenBalance, amount, decimals, }) } const amountLessThanZero = conversionGreaterThan( { value: 0, fromNumericBase: 'dec' }, { value: amount, fromNumericBase: 'hex' }, ) let amountError = null if (insufficientFunds) { amountError = INSUFFICIENT_FUNDS_ERROR } else if (inSufficientTokens) { amountError = INSUFFICIENT_TOKENS_ERROR } else if (amountLessThanZero) { amountError = NEGATIVE_ETH_ERROR } return { amount: amountError } } function getGasFeeErrorObject ({ balance, conversionRate, gasTotal, primaryCurrency, }) { let gasFeeError = null if (gasTotal && conversionRate) { const insufficientFunds = !isBalanceSufficient({ amount: '0x0', balance, conversionRate, gasTotal, primaryCurrency, }) if (insufficientFunds) { gasFeeError = INSUFFICIENT_FUNDS_ERROR } } return { gasFee: gasFeeError } } function calcTokenBalance ({ sendToken, usersToken }) { const { decimals } = sendToken || {} return calcTokenAmount(usersToken.balance.toString(), decimals).toString(16) } function doesAmountErrorRequireUpdate ({ balance, gasTotal, prevBalance, prevGasTotal, prevTokenBalance, sendToken, tokenBalance, }) { const balanceHasChanged = balance !== prevBalance const gasTotalHasChange = gasTotal !== prevGasTotal const tokenBalanceHasChanged = sendToken && tokenBalance !== prevTokenBalance const amountErrorRequiresUpdate = balanceHasChanged || gasTotalHasChange || tokenBalanceHasChanged return amountErrorRequiresUpdate } async function estimateGas ({ selectedAddress, sendToken, blockGasLimit = MIN_GAS_LIMIT_HEX, to, value, data, gasPrice, estimateGasMethod, }) { const paramsForGasEstimate = { from: selectedAddress, value, gasPrice } // if recipient has no code, gas is 21k max: if (!sendToken && !data) { const code = Boolean(to) && await global.eth.getCode(to) // Geth will return '0x', and ganache-core v2.2.1 will return '0x0' const codeIsEmpty = !code || code === '0x' || code === '0x0' if (codeIsEmpty) { return SIMPLE_GAS_COST } } else if (sendToken && !to) { return BASE_TOKEN_GAS_COST } if (sendToken) { paramsForGasEstimate.value = '0x0' paramsForGasEstimate.data = generateTokenTransferData({ toAddress: to, amount: value, sendToken }) paramsForGasEstimate.to = sendToken.address } else { if (data) { paramsForGasEstimate.data = data } if (to) { paramsForGasEstimate.to = to } if (!value || value === '0') { paramsForGasEstimate.value = '0xff' } } // if not, fall back to block gasLimit if (!blockGasLimit) { // eslint-disable-next-line no-param-reassign blockGasLimit = MIN_GAS_LIMIT_HEX } paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit, 0.95, { multiplicandBase: 16, multiplierBase: 10, roundDown: '0', toNumericBase: 'hex', })) // run tx try { const estimatedGas = await estimateGasMethod(paramsForGasEstimate) const estimateWithBuffer = addGasBuffer(estimatedGas.toString(16), blockGasLimit, 1.5) return ethUtil.addHexPrefix(estimateWithBuffer) } catch (error) { const simulationFailed = ( error.message.includes('Transaction execution error.') || error.message.includes('gas required exceeds allowance or always failing transaction') ) if (simulationFailed) { const estimateWithBuffer = addGasBuffer(paramsForGasEstimate.gas, blockGasLimit, 1.5) return ethUtil.addHexPrefix(estimateWithBuffer) } throw error } } function addGasBuffer (initialGasLimitHex, blockGasLimitHex, bufferMultiplier = 1.5) { const upperGasLimit = multiplyCurrencies(blockGasLimitHex, 0.9, { toNumericBase: 'hex', multiplicandBase: 16, multiplierBase: 10, numberOfDecimals: '0', }) const bufferedGasLimit = multiplyCurrencies(initialGasLimitHex, bufferMultiplier, { toNumericBase: 'hex', multiplicandBase: 16, multiplierBase: 10, numberOfDecimals: '0', }) // if initialGasLimit is above blockGasLimit, dont modify it if (conversionGreaterThan( { value: initialGasLimitHex, fromNumericBase: 'hex' }, { value: upperGasLimit, fromNumericBase: 'hex' }, )) { return initialGasLimitHex } // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit if (conversionLessThan( { value: bufferedGasLimit, fromNumericBase: 'hex' }, { value: upperGasLimit, fromNumericBase: 'hex' }, )) { return bufferedGasLimit } // otherwise use blockGasLimit return upperGasLimit } function generateTokenTransferData ({ toAddress = '0x0', amount = '0x0', sendToken }) { if (!sendToken) { return undefined } return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( abi.rawEncode(['address', 'uint256'], [toAddress, ethUtil.addHexPrefix(amount)]), (x) => (`00${x.toString(16)}`).slice(-2), ).join('') } function getToAddressForGasUpdate (...addresses) { return [...addresses, ''].find((str) => str !== undefined && str !== null).toLowerCase() } function removeLeadingZeroes (str) { return str.replace(/^0*(?=\d)/u, '') } function ellipsify (text, first = 6, last = 4) { return `${text.slice(0, first)}...${text.slice(-last)}` }