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