Remove global transaction state from send flow (#14777)
* remove global transaction state from send slice * fixup new testfeature/default_network_editable
parent
f4b25d7ea5
commit
94967072f7
@ -0,0 +1,295 @@ |
|||||||
|
import { addHexPrefix } from 'ethereumjs-util'; |
||||||
|
import abi from 'human-standard-token-abi'; |
||||||
|
import { GAS_LIMITS } from '../../../shared/constants/gas'; |
||||||
|
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network'; |
||||||
|
import { |
||||||
|
ASSET_TYPES, |
||||||
|
TRANSACTION_ENVELOPE_TYPES, |
||||||
|
} from '../../../shared/constants/transaction'; |
||||||
|
import { readAddressAsContract } from '../../../shared/modules/contract-utils'; |
||||||
|
import { |
||||||
|
conversionUtil, |
||||||
|
multiplyCurrencies, |
||||||
|
} from '../../../shared/modules/conversion.utils'; |
||||||
|
import { ETH, GWEI } from '../../helpers/constants/common'; |
||||||
|
import { calcTokenAmount } from '../../helpers/utils/token-util'; |
||||||
|
import { MIN_GAS_LIMIT_HEX } from '../../pages/send/send.constants'; |
||||||
|
import { |
||||||
|
addGasBuffer, |
||||||
|
generateERC20TransferData, |
||||||
|
generateERC721TransferData, |
||||||
|
getAssetTransferData, |
||||||
|
} from '../../pages/send/send.utils'; |
||||||
|
import { getGasPriceInHexWei } from '../../selectors'; |
||||||
|
import { estimateGas } from '../../store/actions'; |
||||||
|
|
||||||
|
export async function estimateGasLimitForSend({ |
||||||
|
selectedAddress, |
||||||
|
value, |
||||||
|
gasPrice, |
||||||
|
sendToken, |
||||||
|
to, |
||||||
|
data, |
||||||
|
isNonStandardEthChain, |
||||||
|
chainId, |
||||||
|
gasLimit, |
||||||
|
...options |
||||||
|
}) { |
||||||
|
let isSimpleSendOnNonStandardNetwork = false; |
||||||
|
|
||||||
|
// blockGasLimit may be a falsy, but defined, value when we receive it from
|
||||||
|
// state, so we use logical or to fall back to MIN_GAS_LIMIT_HEX. Some
|
||||||
|
// network implementations check the gas parameter supplied to
|
||||||
|
// eth_estimateGas for validity. For this reason, we set token sends
|
||||||
|
// blockGasLimit default to a higher number. Note that the current gasLimit
|
||||||
|
// on a BLOCK is 15,000,000 and will be 30,000,000 on mainnet after London.
|
||||||
|
// Meanwhile, MIN_GAS_LIMIT_HEX is 0x5208.
|
||||||
|
let blockGasLimit = MIN_GAS_LIMIT_HEX; |
||||||
|
if (options.blockGasLimit) { |
||||||
|
blockGasLimit = options.blockGasLimit; |
||||||
|
} else if (sendToken) { |
||||||
|
blockGasLimit = GAS_LIMITS.BASE_TOKEN_ESTIMATE; |
||||||
|
} |
||||||
|
|
||||||
|
// The parameters below will be sent to our background process to estimate
|
||||||
|
// how much gas will be used for a transaction. That background process is
|
||||||
|
// located in tx-gas-utils.js in the transaction controller folder.
|
||||||
|
const paramsForGasEstimate = { from: selectedAddress, value, gasPrice }; |
||||||
|
|
||||||
|
if (sendToken) { |
||||||
|
if (!to) { |
||||||
|
// If no to address is provided, we cannot generate the token transfer
|
||||||
|
// hexData. hexData in a transaction largely dictates how much gas will
|
||||||
|
// be consumed by a transaction. We must use our best guess, which is
|
||||||
|
// represented in the gas shared constants.
|
||||||
|
return GAS_LIMITS.BASE_TOKEN_ESTIMATE; |
||||||
|
} |
||||||
|
paramsForGasEstimate.value = '0x0'; |
||||||
|
|
||||||
|
// We have to generate the erc20/erc721 contract call to transfer tokens in
|
||||||
|
// order to get a proper estimate for gasLimit.
|
||||||
|
paramsForGasEstimate.data = getAssetTransferData({ |
||||||
|
sendToken, |
||||||
|
fromAddress: selectedAddress, |
||||||
|
toAddress: to, |
||||||
|
amount: value, |
||||||
|
}); |
||||||
|
|
||||||
|
paramsForGasEstimate.to = sendToken.address; |
||||||
|
} else { |
||||||
|
if (!data) { |
||||||
|
// eth.getCode will return the compiled smart contract code at the
|
||||||
|
// address. If this returns 0x, 0x0 or a nullish value then the address
|
||||||
|
// is an externally owned account (NOT a contract account). For these
|
||||||
|
// types of transactions the gasLimit will always be 21,000 or 0x5208
|
||||||
|
const { isContractAddress } = to |
||||||
|
? await readAddressAsContract(global.eth, to) |
||||||
|
: {}; |
||||||
|
if (!isContractAddress && !isNonStandardEthChain) { |
||||||
|
return GAS_LIMITS.SIMPLE; |
||||||
|
} else if (!isContractAddress && isNonStandardEthChain) { |
||||||
|
isSimpleSendOnNonStandardNetwork = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
paramsForGasEstimate.data = data; |
||||||
|
|
||||||
|
if (to) { |
||||||
|
paramsForGasEstimate.to = to; |
||||||
|
} |
||||||
|
|
||||||
|
if (!value || value === '0') { |
||||||
|
// TODO: Figure out what's going on here. According to eth_estimateGas
|
||||||
|
// docs this value can be zero, or undefined, yet we are setting it to a
|
||||||
|
// value here when the value is undefined or zero. For more context:
|
||||||
|
// https://github.com/MetaMask/metamask-extension/pull/6195
|
||||||
|
paramsForGasEstimate.value = '0xff'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!isSimpleSendOnNonStandardNetwork) { |
||||||
|
// If we do not yet have a gasLimit, we must call into our background
|
||||||
|
// process to get an estimate for gasLimit based on known parameters.
|
||||||
|
|
||||||
|
paramsForGasEstimate.gas = addHexPrefix( |
||||||
|
multiplyCurrencies(blockGasLimit, 0.95, { |
||||||
|
multiplicandBase: 16, |
||||||
|
multiplierBase: 10, |
||||||
|
roundDown: '0', |
||||||
|
toNumericBase: 'hex', |
||||||
|
}), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// The buffer multipler reduces transaction failures by ensuring that the
|
||||||
|
// estimated gas is always sufficient. Without the multiplier, estimates
|
||||||
|
// for contract interactions can become inaccurate over time. This is because
|
||||||
|
// gas estimation is non-deterministic. The gas required for the exact same
|
||||||
|
// transaction call can change based on state of a contract or changes in the
|
||||||
|
// contracts environment (blockchain data or contracts it interacts with).
|
||||||
|
// Applying the 1.5 buffer has proven to be a useful guard against this non-
|
||||||
|
// deterministic behaviour.
|
||||||
|
//
|
||||||
|
// Gas estimation of simple sends should, however, be deterministic. As such
|
||||||
|
// no buffer is needed in those cases.
|
||||||
|
let bufferMultiplier = 1.5; |
||||||
|
if (isSimpleSendOnNonStandardNetwork) { |
||||||
|
bufferMultiplier = 1; |
||||||
|
} else if (CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId]) { |
||||||
|
bufferMultiplier = CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId]; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
// Call into the background process that will simulate transaction
|
||||||
|
// execution on the node and return an estimate of gasLimit
|
||||||
|
const estimatedGasLimit = await estimateGas(paramsForGasEstimate); |
||||||
|
const estimateWithBuffer = addGasBuffer( |
||||||
|
estimatedGasLimit, |
||||||
|
blockGasLimit, |
||||||
|
bufferMultiplier, |
||||||
|
); |
||||||
|
return addHexPrefix(estimateWithBuffer); |
||||||
|
} catch (error) { |
||||||
|
const simulationFailed = |
||||||
|
error.message.includes('Transaction execution error.') || |
||||||
|
error.message.includes( |
||||||
|
'gas required exceeds allowance or always failing transaction', |
||||||
|
) || |
||||||
|
(CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId] && |
||||||
|
error.message.includes('gas required exceeds allowance')); |
||||||
|
if (simulationFailed) { |
||||||
|
const estimateWithBuffer = addGasBuffer( |
||||||
|
paramsForGasEstimate?.gas ?? gasLimit, |
||||||
|
blockGasLimit, |
||||||
|
bufferMultiplier, |
||||||
|
); |
||||||
|
return addHexPrefix(estimateWithBuffer); |
||||||
|
} |
||||||
|
throw error; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Generates a txParams from the send slice. |
||||||
|
* |
||||||
|
* @param {import('.').SendState} sendState - the state of the send slice |
||||||
|
* @returns {import( |
||||||
|
* '../../../shared/constants/transaction' |
||||||
|
* ).TxParams} A txParams object that can be used to create a transaction or |
||||||
|
* update an existing transaction. |
||||||
|
*/ |
||||||
|
export function generateTransactionParams(sendState) { |
||||||
|
const draftTransaction = |
||||||
|
sendState.draftTransactions[sendState.currentTransactionUUID]; |
||||||
|
const txParams = { |
||||||
|
// If the fromAccount has been specified we use that, if not we use the
|
||||||
|
// selected account.
|
||||||
|
from: |
||||||
|
draftTransaction.fromAccount?.address || |
||||||
|
sendState.selectedAccount.address, |
||||||
|
// gasLimit always needs to be set regardless of the asset being sent
|
||||||
|
// or the type of transaction.
|
||||||
|
gas: draftTransaction.gas.gasLimit, |
||||||
|
}; |
||||||
|
switch (draftTransaction.asset.type) { |
||||||
|
case ASSET_TYPES.TOKEN: |
||||||
|
// When sending a token the to address is the contract address of
|
||||||
|
// the token being sent. The value is set to '0x0' and the data
|
||||||
|
// is generated from the recipient address, token being sent and
|
||||||
|
// amount.
|
||||||
|
txParams.to = draftTransaction.asset.details.address; |
||||||
|
txParams.value = '0x0'; |
||||||
|
txParams.data = generateERC20TransferData({ |
||||||
|
toAddress: draftTransaction.recipient.address, |
||||||
|
amount: draftTransaction.amount.value, |
||||||
|
sendToken: draftTransaction.asset.details, |
||||||
|
}); |
||||||
|
break; |
||||||
|
case ASSET_TYPES.COLLECTIBLE: |
||||||
|
// When sending a token the to address is the contract address of
|
||||||
|
// the token being sent. The value is set to '0x0' and the data
|
||||||
|
// is generated from the recipient address, token being sent and
|
||||||
|
// amount.
|
||||||
|
txParams.to = draftTransaction.asset.details.address; |
||||||
|
txParams.value = '0x0'; |
||||||
|
txParams.data = generateERC721TransferData({ |
||||||
|
toAddress: draftTransaction.recipient.address, |
||||||
|
fromAddress: |
||||||
|
draftTransaction.fromAccount?.address ?? |
||||||
|
sendState.selectedAccount.address, |
||||||
|
tokenId: draftTransaction.asset.details.tokenId, |
||||||
|
}); |
||||||
|
break; |
||||||
|
case ASSET_TYPES.NATIVE: |
||||||
|
default: |
||||||
|
// When sending native currency the to and value fields use the
|
||||||
|
// recipient and amount values and the data key is either null or
|
||||||
|
// populated with the user input provided in hex field.
|
||||||
|
txParams.to = draftTransaction.recipient.address; |
||||||
|
txParams.value = draftTransaction.amount.value; |
||||||
|
txParams.data = draftTransaction.userInputHexData ?? undefined; |
||||||
|
} |
||||||
|
|
||||||
|
// We need to make sure that we only include the right gas fee fields
|
||||||
|
// based on the type of transaction the network supports. We will also set
|
||||||
|
// the type param here.
|
||||||
|
if (sendState.eip1559support) { |
||||||
|
txParams.type = TRANSACTION_ENVELOPE_TYPES.FEE_MARKET; |
||||||
|
|
||||||
|
txParams.maxFeePerGas = draftTransaction.gas.maxFeePerGas; |
||||||
|
txParams.maxPriorityFeePerGas = draftTransaction.gas.maxPriorityFeePerGas; |
||||||
|
|
||||||
|
if (!txParams.maxFeePerGas || txParams.maxFeePerGas === '0x0') { |
||||||
|
txParams.maxFeePerGas = draftTransaction.gas.gasPrice; |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
!txParams.maxPriorityFeePerGas || |
||||||
|
txParams.maxPriorityFeePerGas === '0x0' |
||||||
|
) { |
||||||
|
txParams.maxPriorityFeePerGas = txParams.maxFeePerGas; |
||||||
|
} |
||||||
|
} else { |
||||||
|
txParams.gasPrice = draftTransaction.gas.gasPrice; |
||||||
|
txParams.type = TRANSACTION_ENVELOPE_TYPES.LEGACY; |
||||||
|
} |
||||||
|
|
||||||
|
return txParams; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This method is used to keep the original logic from the gas.duck.js file |
||||||
|
* after receiving a gasPrice from eth_gasPrice. First, the returned gasPrice |
||||||
|
* was converted to GWEI, then it was converted to a Number, then in the send |
||||||
|
* duck (here) we would use getGasPriceInHexWei to get back to hexWei. Now that |
||||||
|
* we receive a GWEI estimate from the controller, we still need to do this |
||||||
|
* weird conversion to get the proper rounding. |
||||||
|
* |
||||||
|
* @param {string} gasPriceEstimate |
||||||
|
* @returns {string} |
||||||
|
*/ |
||||||
|
export function getRoundedGasPrice(gasPriceEstimate) { |
||||||
|
const gasPriceInDecGwei = conversionUtil(gasPriceEstimate, { |
||||||
|
numberOfDecimals: 9, |
||||||
|
toDenomination: GWEI, |
||||||
|
fromNumericBase: 'dec', |
||||||
|
toNumericBase: 'dec', |
||||||
|
fromCurrency: ETH, |
||||||
|
fromDenomination: GWEI, |
||||||
|
}); |
||||||
|
const gasPriceAsNumber = Number(gasPriceInDecGwei); |
||||||
|
return getGasPriceInHexWei(gasPriceAsNumber); |
||||||
|
} |
||||||
|
|
||||||
|
export async function getERC20Balance(token, accountAddress) { |
||||||
|
const contract = global.eth.contract(abi).at(token.address); |
||||||
|
const usersToken = (await contract.balanceOf(accountAddress)) ?? null; |
||||||
|
if (!usersToken) { |
||||||
|
return '0x0'; |
||||||
|
} |
||||||
|
const amount = calcTokenAmount( |
||||||
|
usersToken.balance.toString(), |
||||||
|
token.decimals, |
||||||
|
).toString(16); |
||||||
|
return addHexPrefix(amount); |
||||||
|
} |
@ -0,0 +1,163 @@ |
|||||||
|
import { ethers } from 'ethers'; |
||||||
|
import { GAS_LIMITS } from '../../../shared/constants/gas'; |
||||||
|
import { |
||||||
|
ASSET_TYPES, |
||||||
|
TRANSACTION_ENVELOPE_TYPES, |
||||||
|
} from '../../../shared/constants/transaction'; |
||||||
|
import { BURN_ADDRESS } from '../../../shared/modules/hexstring-utils'; |
||||||
|
import { getInitialSendStateWithExistingTxState } from '../../../test/jest/mocks'; |
||||||
|
import { TOKEN_STANDARDS } from '../../helpers/constants/common'; |
||||||
|
import { |
||||||
|
generateERC20TransferData, |
||||||
|
generateERC721TransferData, |
||||||
|
} from '../../pages/send/send.utils'; |
||||||
|
import { generateTransactionParams } from './helpers'; |
||||||
|
|
||||||
|
describe('Send Slice Helpers', () => { |
||||||
|
describe('generateTransactionParams', () => { |
||||||
|
it('should generate a txParams for a token transfer', () => { |
||||||
|
const tokenDetails = { |
||||||
|
address: '0xToken', |
||||||
|
symbol: 'SYMB', |
||||||
|
decimals: 18, |
||||||
|
}; |
||||||
|
const txParams = generateTransactionParams( |
||||||
|
getInitialSendStateWithExistingTxState({ |
||||||
|
fromAccount: { |
||||||
|
address: '0x00', |
||||||
|
}, |
||||||
|
amount: { |
||||||
|
value: '0x1', |
||||||
|
}, |
||||||
|
asset: { |
||||||
|
type: ASSET_TYPES.TOKEN, |
||||||
|
balance: '0xaf', |
||||||
|
details: tokenDetails, |
||||||
|
}, |
||||||
|
recipient: { |
||||||
|
address: BURN_ADDRESS, |
||||||
|
}, |
||||||
|
}), |
||||||
|
); |
||||||
|
expect(txParams).toStrictEqual({ |
||||||
|
from: '0x00', |
||||||
|
data: generateERC20TransferData({ |
||||||
|
toAddress: BURN_ADDRESS, |
||||||
|
amount: '0x1', |
||||||
|
sendToken: tokenDetails, |
||||||
|
}), |
||||||
|
to: '0xToken', |
||||||
|
type: '0x0', |
||||||
|
value: '0x0', |
||||||
|
gas: '0x0', |
||||||
|
gasPrice: '0x0', |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should generate a txParams for a collectible transfer', () => { |
||||||
|
const txParams = generateTransactionParams( |
||||||
|
getInitialSendStateWithExistingTxState({ |
||||||
|
fromAccount: { |
||||||
|
address: '0x00', |
||||||
|
}, |
||||||
|
amount: { |
||||||
|
value: '0x1', |
||||||
|
}, |
||||||
|
asset: { |
||||||
|
type: ASSET_TYPES.COLLECTIBLE, |
||||||
|
balance: '0xaf', |
||||||
|
details: { |
||||||
|
address: '0xToken', |
||||||
|
standard: TOKEN_STANDARDS.ERC721, |
||||||
|
tokenId: ethers.BigNumber.from(15000).toString(), |
||||||
|
}, |
||||||
|
}, |
||||||
|
recipient: { |
||||||
|
address: BURN_ADDRESS, |
||||||
|
}, |
||||||
|
}), |
||||||
|
); |
||||||
|
expect(txParams).toStrictEqual({ |
||||||
|
from: '0x00', |
||||||
|
data: generateERC721TransferData({ |
||||||
|
toAddress: BURN_ADDRESS, |
||||||
|
fromAddress: '0x00', |
||||||
|
tokenId: ethers.BigNumber.from(15000).toString(), |
||||||
|
}), |
||||||
|
to: '0xToken', |
||||||
|
type: '0x0', |
||||||
|
value: '0x0', |
||||||
|
gas: '0x0', |
||||||
|
gasPrice: '0x0', |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should generate a txParams for a native legacy transaction', () => { |
||||||
|
const txParams = generateTransactionParams( |
||||||
|
getInitialSendStateWithExistingTxState({ |
||||||
|
fromAccount: { |
||||||
|
address: '0x00', |
||||||
|
}, |
||||||
|
amount: { |
||||||
|
value: '0x1', |
||||||
|
}, |
||||||
|
asset: { |
||||||
|
type: ASSET_TYPES.NATIVE, |
||||||
|
balance: '0xaf', |
||||||
|
details: null, |
||||||
|
}, |
||||||
|
recipient: { |
||||||
|
address: BURN_ADDRESS, |
||||||
|
}, |
||||||
|
}), |
||||||
|
); |
||||||
|
expect(txParams).toStrictEqual({ |
||||||
|
from: '0x00', |
||||||
|
data: undefined, |
||||||
|
to: BURN_ADDRESS, |
||||||
|
type: '0x0', |
||||||
|
value: '0x1', |
||||||
|
gas: '0x0', |
||||||
|
gasPrice: '0x0', |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should generate a txParams for a native fee market transaction', () => { |
||||||
|
const txParams = generateTransactionParams({ |
||||||
|
...getInitialSendStateWithExistingTxState({ |
||||||
|
fromAccount: { |
||||||
|
address: '0x00', |
||||||
|
}, |
||||||
|
amount: { |
||||||
|
value: '0x1', |
||||||
|
}, |
||||||
|
asset: { |
||||||
|
type: ASSET_TYPES.NATIVE, |
||||||
|
balance: '0xaf', |
||||||
|
details: null, |
||||||
|
}, |
||||||
|
recipient: { |
||||||
|
address: BURN_ADDRESS, |
||||||
|
}, |
||||||
|
gas: { |
||||||
|
maxFeePerGas: '0x2', |
||||||
|
maxPriorityFeePerGas: '0x1', |
||||||
|
gasLimit: GAS_LIMITS.SIMPLE, |
||||||
|
}, |
||||||
|
transactionType: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET, |
||||||
|
}), |
||||||
|
eip1559support: true, |
||||||
|
}); |
||||||
|
expect(txParams).toStrictEqual({ |
||||||
|
from: '0x00', |
||||||
|
data: undefined, |
||||||
|
to: BURN_ADDRESS, |
||||||
|
type: '0x2', |
||||||
|
value: '0x1', |
||||||
|
gas: GAS_LIMITS.SIMPLE, |
||||||
|
maxFeePerGas: '0x2', |
||||||
|
maxPriorityFeePerGas: '0x1', |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue