|
|
|
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'
|
|
|
|
|
|
|
|
import abi from 'ethereumjs-abi'
|
|
|
|
import ethUtil from 'ethereumjs-util'
|
|
|
|
|
|
|
|
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: 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) {
|
|
|
|
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)
|
|
|
|
} else {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
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)/, '')
|
|
|
|
}
|
|
|
|
|
|
|
|
function ellipsify (text, first = 6, last = 4) {
|
|
|
|
return `${text.slice(0, first)}...${text.slice(-last)}`
|
|
|
|
}
|