mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-29 23:58:06 +01:00
432d6508ba
* Stringify gas estimate in backend, before it is serialized and sent to ui * Fix send.utils.test.js
355 lines
8.0 KiB
JavaScript
355 lines
8.0 KiB
JavaScript
import abi from 'ethereumjs-abi';
|
|
import {
|
|
addCurrencies,
|
|
conversionUtil,
|
|
conversionGTE,
|
|
multiplyCurrencies,
|
|
conversionGreaterThan,
|
|
conversionLessThan,
|
|
} from '../../helpers/utils/conversion-util';
|
|
|
|
import { calcTokenAmount } from '../../helpers/utils/token-util';
|
|
import { addHexPrefix } from '../../../app/scripts/lib/util';
|
|
|
|
import { GAS_LIMITS } from '../../../shared/constants/gas';
|
|
import {
|
|
INSUFFICIENT_FUNDS_ERROR,
|
|
INSUFFICIENT_TOKENS_ERROR,
|
|
MIN_GAS_LIMIT_HEX,
|
|
NEGATIVE_ETH_ERROR,
|
|
TOKEN_TRANSFER_FUNCTION_SIGNATURE,
|
|
} from './send.constants';
|
|
|
|
export {
|
|
addGasBuffer,
|
|
calcGasTotal,
|
|
calcTokenBalance,
|
|
doesAmountErrorRequireUpdate,
|
|
estimateGasForSend,
|
|
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 estimateGasForSend({
|
|
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 GAS_LIMITS.SIMPLE;
|
|
}
|
|
} else if (sendToken && !to) {
|
|
return GAS_LIMITS.BASE_TOKEN_ESTIMATE;
|
|
}
|
|
|
|
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 = 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, blockGasLimit, 1.5);
|
|
return 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 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, 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)}`;
|
|
}
|