mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
Remove global transaction state from send flow (#14777)
* remove global transaction state from send slice * fixup new test
This commit is contained in:
parent
f4b25d7ea5
commit
94967072f7
@ -8,3 +8,16 @@ import contractMap from '@metamask/contract-metadata';
|
|||||||
export const LISTED_CONTRACT_ADDRESSES = Object.keys(
|
export const LISTED_CONTRACT_ADDRESSES = Object.keys(
|
||||||
contractMap,
|
contractMap,
|
||||||
).map((address) => address.toLowerCase());
|
).map((address) => address.toLowerCase());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} TokenDetails
|
||||||
|
* @property {string} address - The address of the selected 'TOKEN' or
|
||||||
|
* 'COLLECTIBLE' contract.
|
||||||
|
* @property {string} [symbol] - The symbol of the token.
|
||||||
|
* @property {number} [decimals] - The number of decimals of the selected
|
||||||
|
* 'ERC20' asset.
|
||||||
|
* @property {number} [tokenId] - The id of the selected 'COLLECTIBLE' asset.
|
||||||
|
* @property {TokenStandardStrings} [standard] - The standard of the selected
|
||||||
|
* asset.
|
||||||
|
* @property {boolean} [isERC721] - True when the asset is a ERC721 token.
|
||||||
|
*/
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
import {
|
||||||
|
draftTransactionInitialState,
|
||||||
|
initialState,
|
||||||
|
} from '../../ui/ducks/send';
|
||||||
|
|
||||||
export const TOP_ASSETS_GET_RESPONSE = [
|
export const TOP_ASSETS_GET_RESPONSE = [
|
||||||
{
|
{
|
||||||
symbol: 'LINK',
|
symbol: 'LINK',
|
||||||
@ -103,3 +108,42 @@ export const createGasFeeEstimatesForFeeMarket = () => {
|
|||||||
estimatedBaseFee: '50',
|
estimatedBaseFee: '50',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const INITIAL_SEND_STATE_FOR_EXISTING_DRAFT = {
|
||||||
|
...initialState,
|
||||||
|
currentTransactionUUID: 'test-uuid',
|
||||||
|
draftTransactions: {
|
||||||
|
'test-uuid': {
|
||||||
|
...draftTransactionInitialState,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getInitialSendStateWithExistingTxState = (draftTxState) => ({
|
||||||
|
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||||
|
draftTransactions: {
|
||||||
|
'test-uuid': {
|
||||||
|
...draftTransactionInitialState,
|
||||||
|
...draftTxState,
|
||||||
|
amount: {
|
||||||
|
...draftTransactionInitialState.amount,
|
||||||
|
...draftTxState.amount,
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
...draftTransactionInitialState.asset,
|
||||||
|
...draftTxState.asset,
|
||||||
|
},
|
||||||
|
gas: {
|
||||||
|
...draftTransactionInitialState.gas,
|
||||||
|
...draftTxState.gas,
|
||||||
|
},
|
||||||
|
recipient: {
|
||||||
|
...draftTransactionInitialState.recipient,
|
||||||
|
...draftTxState.recipient,
|
||||||
|
},
|
||||||
|
history: draftTxState.history ?? [],
|
||||||
|
// Use this key if you want to console.log inside the send.js file.
|
||||||
|
test: draftTxState.test ?? 'yo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -9,7 +9,7 @@ import Tooltip from '../../ui/tooltip';
|
|||||||
import InfoIcon from '../../ui/icon/info-icon.component';
|
import InfoIcon from '../../ui/icon/info-icon.component';
|
||||||
import Button from '../../ui/button';
|
import Button from '../../ui/button';
|
||||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
import { updateSendAsset } from '../../../ducks/send';
|
import { startNewDraftTransaction } from '../../../ducks/send';
|
||||||
import { SEND_ROUTE } from '../../../helpers/constants/routes';
|
import { SEND_ROUTE } from '../../../helpers/constants/routes';
|
||||||
import { SEVERITIES } from '../../../helpers/constants/design-system';
|
import { SEVERITIES } from '../../../helpers/constants/design-system';
|
||||||
import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys';
|
import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys';
|
||||||
@ -74,7 +74,7 @@ const AssetListItem = ({
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await dispatch(
|
await dispatch(
|
||||||
updateSendAsset({
|
startNewDraftTransaction({
|
||||||
type: ASSET_TYPES.TOKEN,
|
type: ASSET_TYPES.TOKEN,
|
||||||
details: {
|
details: {
|
||||||
address: tokenAddress,
|
address: tokenAddress,
|
||||||
|
@ -45,7 +45,7 @@ import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
|||||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
|
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
|
||||||
import CollectibleOptions from '../collectible-options/collectible-options';
|
import CollectibleOptions from '../collectible-options/collectible-options';
|
||||||
import Button from '../../ui/button';
|
import Button from '../../ui/button';
|
||||||
import { updateSendAsset } from '../../../ducks/send';
|
import { startNewDraftTransaction } from '../../../ducks/send';
|
||||||
import InfoTooltip from '../../ui/info-tooltip';
|
import InfoTooltip from '../../ui/info-tooltip';
|
||||||
import { ERC721 } from '../../../helpers/constants/common';
|
import { ERC721 } from '../../../helpers/constants/common';
|
||||||
import { usePrevious } from '../../../hooks/usePrevious';
|
import { usePrevious } from '../../../hooks/usePrevious';
|
||||||
@ -120,7 +120,7 @@ export default function CollectibleDetails({ collectible }) {
|
|||||||
|
|
||||||
const onSend = async () => {
|
const onSend = async () => {
|
||||||
await dispatch(
|
await dispatch(
|
||||||
updateSendAsset({
|
startNewDraftTransaction({
|
||||||
type: ASSET_TYPES.COLLECTIBLE,
|
type: ASSET_TYPES.COLLECTIBLE,
|
||||||
details: collectible,
|
details: collectible,
|
||||||
}),
|
}),
|
||||||
|
@ -33,6 +33,8 @@ import { isHardwareKeyring } from '../../../helpers/utils/hardware';
|
|||||||
import { MetaMetricsContext } from '../../../contexts/metametrics';
|
import { MetaMetricsContext } from '../../../contexts/metametrics';
|
||||||
import { EVENT } from '../../../../shared/constants/metametrics';
|
import { EVENT } from '../../../../shared/constants/metametrics';
|
||||||
import Spinner from '../../ui/spinner';
|
import Spinner from '../../ui/spinner';
|
||||||
|
import { startNewDraftTransaction } from '../../../ducks/send';
|
||||||
|
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
|
||||||
import WalletOverview from './wallet-overview';
|
import WalletOverview from './wallet-overview';
|
||||||
|
|
||||||
const EthOverview = ({ className }) => {
|
const EthOverview = ({ className }) => {
|
||||||
@ -131,7 +133,11 @@ const EthOverview = ({ className }) => {
|
|||||||
legacy_event: true,
|
legacy_event: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
history.push(SEND_ROUTE);
|
dispatch(
|
||||||
|
startNewDraftTransaction({ type: ASSET_TYPES.NATIVE }),
|
||||||
|
).then(() => {
|
||||||
|
history.push(SEND_ROUTE);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
} from '../../../helpers/constants/routes';
|
} from '../../../helpers/constants/routes';
|
||||||
import { useTokenTracker } from '../../../hooks/useTokenTracker';
|
import { useTokenTracker } from '../../../hooks/useTokenTracker';
|
||||||
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
|
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
|
||||||
import { updateSendAsset } from '../../../ducks/send';
|
import { startNewDraftTransaction } from '../../../ducks/send';
|
||||||
import { setSwapsFromToken } from '../../../ducks/swaps/swaps';
|
import { setSwapsFromToken } from '../../../ducks/swaps/swaps';
|
||||||
import {
|
import {
|
||||||
getCurrentKeyring,
|
getCurrentKeyring,
|
||||||
@ -93,7 +93,7 @@ const TokenOverview = ({ className, token }) => {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await dispatch(
|
await dispatch(
|
||||||
updateSendAsset({
|
startNewDraftTransaction({
|
||||||
type: ASSET_TYPES.TOKEN,
|
type: ASSET_TYPES.TOKEN,
|
||||||
details: token,
|
details: token,
|
||||||
}),
|
}),
|
||||||
|
@ -118,7 +118,7 @@ export default class TokenInput extends PureComponent {
|
|||||||
isEqualCaseInsensitive(address, token.address),
|
isEqualCaseInsensitive(address, token.address),
|
||||||
);
|
);
|
||||||
|
|
||||||
const tokenExchangeRate = tokenExchangeRates?.[existingToken.address] || 0;
|
const tokenExchangeRate = tokenExchangeRates?.[existingToken?.address] ?? 0;
|
||||||
let currency, numberOfDecimals;
|
let currency, numberOfDecimals;
|
||||||
|
|
||||||
if (hideConversion) {
|
if (hideConversion) {
|
||||||
|
295
ui/ducks/send/helpers.js
Normal file
295
ui/ducks/send/helpers.js
Normal file
@ -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);
|
||||||
|
}
|
163
ui/ducks/send/helpers.test.js
Normal file
163
ui/ducks/send/helpers.test.js
Normal file
@ -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
@ -1,7 +1,7 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { compose } from 'redux';
|
import { compose } from 'redux';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { editTransaction } from '../../ducks/send';
|
import { editExistingTransaction } from '../../ducks/send';
|
||||||
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
|
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
|
||||||
import { ASSET_TYPES } from '../../../shared/constants/transaction';
|
import { ASSET_TYPES } from '../../../shared/constants/transaction';
|
||||||
import ConfirmSendEther from './confirm-send-ether.component';
|
import ConfirmSendEther from './confirm-send-ether.component';
|
||||||
@ -20,7 +20,9 @@ const mapDispatchToProps = (dispatch) => {
|
|||||||
return {
|
return {
|
||||||
editTransaction: async (txData) => {
|
editTransaction: async (txData) => {
|
||||||
const { id } = txData;
|
const { id } = txData;
|
||||||
await dispatch(editTransaction(ASSET_TYPES.NATIVE, id.toString()));
|
await dispatch(
|
||||||
|
editExistingTransaction(ASSET_TYPES.NATIVE, id.toString()),
|
||||||
|
);
|
||||||
dispatch(clearConfirmTransaction());
|
dispatch(clearConfirmTransaction());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -6,14 +6,15 @@ import { SEND_ROUTE } from '../../helpers/constants/routes';
|
|||||||
export default class ConfirmSendToken extends Component {
|
export default class ConfirmSendToken extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
history: PropTypes.object,
|
history: PropTypes.object,
|
||||||
editTransaction: PropTypes.func,
|
editExistingTransaction: PropTypes.func,
|
||||||
tokenAmount: PropTypes.string,
|
tokenAmount: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleEdit(confirmTransactionData) {
|
handleEdit(confirmTransactionData) {
|
||||||
const { editTransaction, history } = this.props;
|
const { editExistingTransaction, history } = this.props;
|
||||||
editTransaction(confirmTransactionData);
|
editExistingTransaction(confirmTransactionData).then(() => {
|
||||||
history.push(SEND_ROUTE);
|
history.push(SEND_ROUTE);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -3,7 +3,7 @@ import { compose } from 'redux';
|
|||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
|
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
|
||||||
import { showSendTokenPage } from '../../store/actions';
|
import { showSendTokenPage } from '../../store/actions';
|
||||||
import { editTransaction } from '../../ducks/send';
|
import { editExistingTransaction } from '../../ducks/send';
|
||||||
import { sendTokenTokenAmountAndToAddressSelector } from '../../selectors';
|
import { sendTokenTokenAmountAndToAddressSelector } from '../../selectors';
|
||||||
import { ASSET_TYPES } from '../../../shared/constants/transaction';
|
import { ASSET_TYPES } from '../../../shared/constants/transaction';
|
||||||
import ConfirmSendToken from './confirm-send-token.component';
|
import ConfirmSendToken from './confirm-send-token.component';
|
||||||
@ -18,18 +18,11 @@ const mapStateToProps = (state) => {
|
|||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => {
|
const mapDispatchToProps = (dispatch) => {
|
||||||
return {
|
return {
|
||||||
editTransaction: ({ txData, tokenData, tokenProps: assetDetails }) => {
|
editExistingTransaction: async ({ txData }) => {
|
||||||
const { id } = txData;
|
const { id } = txData;
|
||||||
dispatch(
|
await dispatch(editExistingTransaction(ASSET_TYPES.TOKEN, id.toString()));
|
||||||
editTransaction(
|
await dispatch(clearConfirmTransaction());
|
||||||
ASSET_TYPES.TOKEN,
|
await dispatch(showSendTokenPage());
|
||||||
id.toString(),
|
|
||||||
tokenData,
|
|
||||||
assetDetails,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
dispatch(clearConfirmTransaction());
|
|
||||||
dispatch(showSendTokenPage());
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import ConfirmTokenTransactionBase from '../confirm-token-transaction-base/confirm-token-transaction-base';
|
import ConfirmTokenTransactionBase from '../confirm-token-transaction-base/confirm-token-transaction-base';
|
||||||
import { SEND_ROUTE } from '../../helpers/constants/routes';
|
import { SEND_ROUTE } from '../../helpers/constants/routes';
|
||||||
import { editTransaction } from '../../ducks/send';
|
import { editExistingTransaction } from '../../ducks/send';
|
||||||
import {
|
import {
|
||||||
contractExchangeRateSelector,
|
contractExchangeRateSelector,
|
||||||
getCurrentCurrency,
|
getCurrentCurrency,
|
||||||
@ -35,27 +35,17 @@ export default function ConfirmSendToken({
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const handleEditTransaction = ({
|
const handleEditTransaction = async ({ txData }) => {
|
||||||
txData,
|
|
||||||
tokenData,
|
|
||||||
tokenProps: assetDetails,
|
|
||||||
}) => {
|
|
||||||
const { id } = txData;
|
const { id } = txData;
|
||||||
dispatch(
|
await dispatch(editExistingTransaction(ASSET_TYPES.TOKEN, id.toString()));
|
||||||
editTransaction(
|
|
||||||
ASSET_TYPES.TOKEN,
|
|
||||||
id.toString(),
|
|
||||||
tokenData,
|
|
||||||
assetDetails,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
dispatch(clearConfirmTransaction());
|
dispatch(clearConfirmTransaction());
|
||||||
dispatch(showSendTokenPage());
|
dispatch(showSendTokenPage());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (confirmTransactionData) => {
|
const handleEdit = (confirmTransactionData) => {
|
||||||
handleEditTransaction(confirmTransactionData);
|
handleEditTransaction(confirmTransactionData).then(() => {
|
||||||
history.push(SEND_ROUTE);
|
history.push(SEND_ROUTE);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const conversionRate = useSelector(getConversionRate);
|
const conversionRate = useSelector(getConversionRate);
|
||||||
const nativeCurrency = useSelector(getNativeCurrency);
|
const nativeCurrency = useSelector(getNativeCurrency);
|
||||||
|
@ -3,9 +3,13 @@ import configureMockStore from 'redux-mock-store';
|
|||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
|
|
||||||
import { fireEvent } from '@testing-library/react';
|
import { fireEvent } from '@testing-library/react';
|
||||||
import { initialState, SEND_STATUSES } from '../../../../../ducks/send';
|
import { AMOUNT_MODES, SEND_STATUSES } from '../../../../../ducks/send';
|
||||||
import { renderWithProvider } from '../../../../../../test/jest';
|
import { renderWithProvider } from '../../../../../../test/jest';
|
||||||
import { GAS_ESTIMATE_TYPES } from '../../../../../../shared/constants/gas';
|
import { GAS_ESTIMATE_TYPES } from '../../../../../../shared/constants/gas';
|
||||||
|
import {
|
||||||
|
getInitialSendStateWithExistingTxState,
|
||||||
|
INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||||
|
} from '../../../../../../test/jest/mocks';
|
||||||
import AmountMaxButton from './amount-max-button';
|
import AmountMaxButton from './amount-max-button';
|
||||||
|
|
||||||
const middleware = [thunk];
|
const middleware = [thunk];
|
||||||
@ -22,7 +26,7 @@ describe('AmountMaxButton Component', () => {
|
|||||||
EIPS: {},
|
EIPS: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: initialState,
|
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(getByText('Max')).toBeTruthy();
|
expect(getByText('Max')).toBeTruthy();
|
||||||
@ -36,12 +40,14 @@ describe('AmountMaxButton Component', () => {
|
|||||||
EIPS: {},
|
EIPS: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: { ...initialState, status: SEND_STATUSES.VALID },
|
send: getInitialSendStateWithExistingTxState({
|
||||||
|
status: SEND_STATUSES.VALID,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
const { getByText } = renderWithProvider(<AmountMaxButton />, store);
|
const { getByText } = renderWithProvider(<AmountMaxButton />, store);
|
||||||
|
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: 'send/updateAmountMode', payload: 'MAX' },
|
{ type: 'send/updateAmountMode', payload: AMOUNT_MODES.MAX },
|
||||||
];
|
];
|
||||||
|
|
||||||
fireEvent.click(getByText('Max'), { bubbles: true });
|
fireEvent.click(getByText('Max'), { bubbles: true });
|
||||||
@ -58,9 +64,10 @@ describe('AmountMaxButton Component', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: {
|
send: {
|
||||||
...initialState,
|
...getInitialSendStateWithExistingTxState({
|
||||||
status: SEND_STATUSES.VALID,
|
status: SEND_STATUSES.VALID,
|
||||||
amount: { ...initialState.amount, mode: 'MAX' },
|
}),
|
||||||
|
amountMode: AMOUNT_MODES.MAX,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { getByText } = renderWithProvider(<AmountMaxButton />, store);
|
const { getByText } = renderWithProvider(<AmountMaxButton />, store);
|
||||||
|
@ -3,9 +3,13 @@ import configureMockStore from 'redux-mock-store';
|
|||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
|
|
||||||
import { fireEvent } from '@testing-library/react';
|
import { fireEvent } from '@testing-library/react';
|
||||||
import { initialState, SEND_STAGES } from '../../../ducks/send';
|
import { SEND_STAGES } from '../../../ducks/send';
|
||||||
import { renderWithProvider } from '../../../../test/jest';
|
import { renderWithProvider } from '../../../../test/jest';
|
||||||
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
|
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
|
||||||
|
import {
|
||||||
|
getInitialSendStateWithExistingTxState,
|
||||||
|
INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||||
|
} from '../../../../test/jest/mocks';
|
||||||
import SendHeader from './send-header.component';
|
import SendHeader from './send-header.component';
|
||||||
|
|
||||||
const middleware = [thunk];
|
const middleware = [thunk];
|
||||||
@ -26,7 +30,7 @@ describe('SendHeader Component', () => {
|
|||||||
const { getByText, rerender } = renderWithProvider(
|
const { getByText, rerender } = renderWithProvider(
|
||||||
<SendHeader />,
|
<SendHeader />,
|
||||||
configureMockStore(middleware)({
|
configureMockStore(middleware)({
|
||||||
send: initialState,
|
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||||
gas: { basicEstimateStatus: 'LOADING' },
|
gas: { basicEstimateStatus: 'LOADING' },
|
||||||
history: { mostRecentOverviewPage: 'activity' },
|
history: { mostRecentOverviewPage: 'activity' },
|
||||||
}),
|
}),
|
||||||
@ -35,7 +39,10 @@ describe('SendHeader Component', () => {
|
|||||||
rerender(
|
rerender(
|
||||||
<SendHeader />,
|
<SendHeader />,
|
||||||
configureMockStore(middleware)({
|
configureMockStore(middleware)({
|
||||||
send: { ...initialState, stage: SEND_STAGES.ADD_RECIPIENT },
|
send: {
|
||||||
|
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||||
|
stage: SEND_STAGES.ADD_RECIPIENT,
|
||||||
|
},
|
||||||
gas: { basicEstimateStatus: 'LOADING' },
|
gas: { basicEstimateStatus: 'LOADING' },
|
||||||
history: { mostRecentOverviewPage: 'activity' },
|
history: { mostRecentOverviewPage: 'activity' },
|
||||||
}),
|
}),
|
||||||
@ -48,9 +55,12 @@ describe('SendHeader Component', () => {
|
|||||||
<SendHeader />,
|
<SendHeader />,
|
||||||
configureMockStore(middleware)({
|
configureMockStore(middleware)({
|
||||||
send: {
|
send: {
|
||||||
...initialState,
|
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||||
stage: SEND_STAGES.DRAFT,
|
stage: SEND_STAGES.DRAFT,
|
||||||
asset: { ...initialState.asset, type: ASSET_TYPES.NATIVE },
|
asset: {
|
||||||
|
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT.asset,
|
||||||
|
type: ASSET_TYPES.NATIVE,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
gas: { basicEstimateStatus: 'LOADING' },
|
gas: { basicEstimateStatus: 'LOADING' },
|
||||||
history: { mostRecentOverviewPage: 'activity' },
|
history: { mostRecentOverviewPage: 'activity' },
|
||||||
@ -64,9 +74,12 @@ describe('SendHeader Component', () => {
|
|||||||
<SendHeader />,
|
<SendHeader />,
|
||||||
configureMockStore(middleware)({
|
configureMockStore(middleware)({
|
||||||
send: {
|
send: {
|
||||||
...initialState,
|
...getInitialSendStateWithExistingTxState({
|
||||||
|
asset: {
|
||||||
|
type: ASSET_TYPES.TOKEN,
|
||||||
|
},
|
||||||
|
}),
|
||||||
stage: SEND_STAGES.DRAFT,
|
stage: SEND_STAGES.DRAFT,
|
||||||
asset: { ...initialState.asset, type: ASSET_TYPES.TOKEN },
|
|
||||||
},
|
},
|
||||||
gas: { basicEstimateStatus: 'LOADING' },
|
gas: { basicEstimateStatus: 'LOADING' },
|
||||||
history: { mostRecentOverviewPage: 'activity' },
|
history: { mostRecentOverviewPage: 'activity' },
|
||||||
@ -80,7 +93,7 @@ describe('SendHeader Component', () => {
|
|||||||
<SendHeader />,
|
<SendHeader />,
|
||||||
configureMockStore(middleware)({
|
configureMockStore(middleware)({
|
||||||
send: {
|
send: {
|
||||||
...initialState,
|
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||||
stage: SEND_STAGES.EDIT,
|
stage: SEND_STAGES.EDIT,
|
||||||
},
|
},
|
||||||
gas: { basicEstimateStatus: 'LOADING' },
|
gas: { basicEstimateStatus: 'LOADING' },
|
||||||
@ -96,7 +109,7 @@ describe('SendHeader Component', () => {
|
|||||||
const { getByText } = renderWithProvider(
|
const { getByText } = renderWithProvider(
|
||||||
<SendHeader />,
|
<SendHeader />,
|
||||||
configureMockStore(middleware)({
|
configureMockStore(middleware)({
|
||||||
send: initialState,
|
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||||
gas: { basicEstimateStatus: 'LOADING' },
|
gas: { basicEstimateStatus: 'LOADING' },
|
||||||
history: { mostRecentOverviewPage: 'activity' },
|
history: { mostRecentOverviewPage: 'activity' },
|
||||||
}),
|
}),
|
||||||
@ -108,7 +121,10 @@ describe('SendHeader Component', () => {
|
|||||||
const { getByText } = renderWithProvider(
|
const { getByText } = renderWithProvider(
|
||||||
<SendHeader />,
|
<SendHeader />,
|
||||||
configureMockStore(middleware)({
|
configureMockStore(middleware)({
|
||||||
send: { ...initialState, stage: SEND_STAGES.EDIT },
|
send: {
|
||||||
|
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||||
|
stage: SEND_STAGES.EDIT,
|
||||||
|
},
|
||||||
gas: { basicEstimateStatus: 'LOADING' },
|
gas: { basicEstimateStatus: 'LOADING' },
|
||||||
history: { mostRecentOverviewPage: 'activity' },
|
history: { mostRecentOverviewPage: 'activity' },
|
||||||
}),
|
}),
|
||||||
@ -118,7 +134,7 @@ describe('SendHeader Component', () => {
|
|||||||
|
|
||||||
it('resets send state when clicked', () => {
|
it('resets send state when clicked', () => {
|
||||||
const store = configureMockStore(middleware)({
|
const store = configureMockStore(middleware)({
|
||||||
send: initialState,
|
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||||
gas: { basicEstimateStatus: 'LOADING' },
|
gas: { basicEstimateStatus: 'LOADING' },
|
||||||
history: { mostRecentOverviewPage: 'activity' },
|
history: { mostRecentOverviewPage: 'activity' },
|
||||||
});
|
});
|
||||||
|
@ -7,14 +7,13 @@ import {
|
|||||||
getRecipient,
|
getRecipient,
|
||||||
getRecipientUserInput,
|
getRecipientUserInput,
|
||||||
getSendStage,
|
getSendStage,
|
||||||
initializeSendState,
|
|
||||||
resetRecipientInput,
|
resetRecipientInput,
|
||||||
resetSendState,
|
resetSendState,
|
||||||
SEND_STAGES,
|
SEND_STAGES,
|
||||||
updateRecipient,
|
updateRecipient,
|
||||||
updateRecipientUserInput,
|
updateRecipientUserInput,
|
||||||
} from '../../ducks/send';
|
} from '../../ducks/send';
|
||||||
import { getCurrentChainId, isCustomPriceExcessive } from '../../selectors';
|
import { isCustomPriceExcessive } from '../../selectors';
|
||||||
import { getSendHexDataFeatureFlagState } from '../../ducks/metamask/metamask';
|
import { getSendHexDataFeatureFlagState } from '../../ducks/metamask/metamask';
|
||||||
import { showQrScanner } from '../../store/actions';
|
import { showQrScanner } from '../../store/actions';
|
||||||
import { MetaMetricsContext } from '../../contexts/metametrics';
|
import { MetaMetricsContext } from '../../contexts/metametrics';
|
||||||
@ -30,7 +29,6 @@ const sendSliceIsCustomPriceExcessive = (state) =>
|
|||||||
|
|
||||||
export default function SendTransactionScreen() {
|
export default function SendTransactionScreen() {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const chainId = useSelector(getCurrentChainId);
|
|
||||||
const stage = useSelector(getSendStage);
|
const stage = useSelector(getSendStage);
|
||||||
const gasIsExcessive = useSelector(sendSliceIsCustomPriceExcessive);
|
const gasIsExcessive = useSelector(sendSliceIsCustomPriceExcessive);
|
||||||
const isUsingMyAccountsForRecipientSearch = useSelector(
|
const isUsingMyAccountsForRecipientSearch = useSelector(
|
||||||
@ -49,11 +47,8 @@ export default function SendTransactionScreen() {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chainId !== undefined) {
|
window.addEventListener('beforeunload', cleanup);
|
||||||
dispatch(initializeSendState());
|
}, [cleanup]);
|
||||||
window.addEventListener('beforeunload', cleanup);
|
|
||||||
}
|
|
||||||
}, [chainId, dispatch, cleanup]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.search === '?scan=true') {
|
if (location.search === '?scan=true') {
|
||||||
|
@ -3,11 +3,12 @@ import configureMockStore from 'redux-mock-store';
|
|||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
|
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { initialState, SEND_STAGES } from '../../ducks/send';
|
import { SEND_STAGES } from '../../ducks/send';
|
||||||
import { ensInitialState } from '../../ducks/ens';
|
import { ensInitialState } from '../../ducks/ens';
|
||||||
import { renderWithProvider } from '../../../test/jest';
|
import { renderWithProvider } from '../../../test/jest';
|
||||||
import { RINKEBY_CHAIN_ID } from '../../../shared/constants/network';
|
import { RINKEBY_CHAIN_ID } from '../../../shared/constants/network';
|
||||||
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
|
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
|
||||||
|
import { INITIAL_SEND_STATE_FOR_EXISTING_DRAFT } from '../../../test/jest/mocks';
|
||||||
import Send from './send';
|
import Send from './send';
|
||||||
|
|
||||||
const middleware = [thunk];
|
const middleware = [thunk];
|
||||||
@ -34,7 +35,7 @@ jest.mock(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const baseStore = {
|
const baseStore = {
|
||||||
send: initialState,
|
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||||
ENS: ensInitialState,
|
ENS: ensInitialState,
|
||||||
gas: {
|
gas: {
|
||||||
customData: { limit: null, price: null },
|
customData: { limit: null, price: null },
|
||||||
@ -87,7 +88,7 @@ const baseStore = {
|
|||||||
|
|
||||||
describe('Send Page', () => {
|
describe('Send Page', () => {
|
||||||
describe('Send Flow Initialization', () => {
|
describe('Send Flow Initialization', () => {
|
||||||
it('should initialize the send, ENS, and gas slices on render', () => {
|
it('should initialize the ENS slice on render', () => {
|
||||||
const store = configureMockStore(middleware)(baseStore);
|
const store = configureMockStore(middleware)(baseStore);
|
||||||
renderWithProvider(<Send />, store);
|
renderWithProvider(<Send />, store);
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
@ -96,9 +97,6 @@ describe('Send Page', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: 'ENS/enableEnsLookup',
|
type: 'ENS/enableEnsLookup',
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
|
||||||
type: 'send/initializeSendState/pending',
|
|
||||||
}),
|
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -113,9 +111,6 @@ describe('Send Page', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: 'ENS/enableEnsLookup',
|
type: 'ENS/enableEnsLookup',
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
|
||||||
type: 'send/initializeSendState/pending',
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: 'UI_MODAL_OPEN',
|
type: 'UI_MODAL_OPEN',
|
||||||
payload: { name: 'QR_SCANNER' },
|
payload: { name: 'QR_SCANNER' },
|
||||||
|
@ -8,7 +8,7 @@ import { decEthToConvertedCurrency as ethTotalToConvertedCurrency } from '../hel
|
|||||||
import { formatETHFee } from '../helpers/utils/formatters';
|
import { formatETHFee } from '../helpers/utils/formatters';
|
||||||
import { calcGasTotal } from '../pages/send/send.utils';
|
import { calcGasTotal } from '../pages/send/send.utils';
|
||||||
|
|
||||||
import { getGasPrice } from '../ducks/send';
|
import { getGasLimit, getGasPrice } from '../ducks/send';
|
||||||
import {
|
import {
|
||||||
GAS_ESTIMATE_TYPES as GAS_FEE_CONTROLLER_ESTIMATE_TYPES,
|
GAS_ESTIMATE_TYPES as GAS_FEE_CONTROLLER_ESTIMATE_TYPES,
|
||||||
GAS_LIMITS,
|
GAS_LIMITS,
|
||||||
@ -321,8 +321,9 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const showFiat = getShouldShowFiat(state);
|
const showFiat = getShouldShowFiat(state);
|
||||||
|
|
||||||
const gasLimit =
|
const gasLimit =
|
||||||
state.send.gas.gasLimit || getCustomGasLimit(state) || GAS_LIMITS.SIMPLE;
|
getGasLimit(state) ?? getCustomGasLimit(state) ?? GAS_LIMITS.SIMPLE;
|
||||||
const { conversionRate } = state.metamask;
|
const { conversionRate } = state.metamask;
|
||||||
const currentCurrency = getCurrentCurrency(state);
|
const currentCurrency = getCurrentCurrency(state);
|
||||||
const gasFeeEstimates = getGasFeeEstimates(state);
|
const gasFeeEstimates = getGasFeeEstimates(state);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../shared/constants/gas';
|
import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../shared/constants/gas';
|
||||||
|
import { getInitialSendStateWithExistingTxState } from '../../test/jest/mocks';
|
||||||
import {
|
import {
|
||||||
getCustomGasLimit,
|
getCustomGasLimit,
|
||||||
getCustomGasPrice,
|
getCustomGasPrice,
|
||||||
@ -11,7 +12,9 @@ import {
|
|||||||
describe('custom-gas selectors', () => {
|
describe('custom-gas selectors', () => {
|
||||||
describe('getCustomGasPrice()', () => {
|
describe('getCustomGasPrice()', () => {
|
||||||
it('should return gas.customData.price', () => {
|
it('should return gas.customData.price', () => {
|
||||||
const mockState = { gas: { customData: { price: 'mockPrice' } } };
|
const mockState = {
|
||||||
|
gas: { customData: { price: 'mockPrice' } },
|
||||||
|
};
|
||||||
expect(getCustomGasPrice(mockState)).toStrictEqual('mockPrice');
|
expect(getCustomGasPrice(mockState)).toStrictEqual('mockPrice');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -200,11 +203,11 @@ describe('custom-gas selectors', () => {
|
|||||||
EIPS: {},
|
EIPS: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: {
|
send: getInitialSendStateWithExistingTxState({
|
||||||
gas: {
|
gas: {
|
||||||
gasPrice: '0x28bed0160',
|
gasPrice: '0x28bed0160',
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
gas: {
|
gas: {
|
||||||
customData: { price: null },
|
customData: { price: null },
|
||||||
},
|
},
|
||||||
@ -222,11 +225,11 @@ describe('custom-gas selectors', () => {
|
|||||||
EIPS: {},
|
EIPS: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: {
|
send: getInitialSendStateWithExistingTxState({
|
||||||
gas: {
|
gas: {
|
||||||
gasPrice: '0x30e4f9b400',
|
gasPrice: '0x30e4f9b400',
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
gas: {
|
gas: {
|
||||||
customData: { price: null },
|
customData: { price: null },
|
||||||
},
|
},
|
||||||
@ -330,11 +333,11 @@ describe('custom-gas selectors', () => {
|
|||||||
chainId: '0x1',
|
chainId: '0x1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: {
|
send: getInitialSendStateWithExistingTxState({
|
||||||
gas: {
|
gas: {
|
||||||
gasLimit: GAS_LIMITS.SIMPLE,
|
gasLimit: GAS_LIMITS.SIMPLE,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -379,11 +382,11 @@ describe('custom-gas selectors', () => {
|
|||||||
chainId: '0x4',
|
chainId: '0x4',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: {
|
send: getInitialSendStateWithExistingTxState({
|
||||||
gas: {
|
gas: {
|
||||||
gasLimit: GAS_LIMITS.SIMPLE,
|
gasLimit: GAS_LIMITS.SIMPLE,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -428,11 +431,11 @@ describe('custom-gas selectors', () => {
|
|||||||
chainId: '0x4',
|
chainId: '0x4',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: {
|
send: getInitialSendStateWithExistingTxState({
|
||||||
gas: {
|
gas: {
|
||||||
gasLimit: GAS_LIMITS.SIMPLE,
|
gasLimit: GAS_LIMITS.SIMPLE,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -477,11 +480,11 @@ describe('custom-gas selectors', () => {
|
|||||||
chainId: '0x1',
|
chainId: '0x1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: {
|
send: getInitialSendStateWithExistingTxState({
|
||||||
gas: {
|
gas: {
|
||||||
gasLimit: GAS_LIMITS.SIMPLE,
|
gasLimit: GAS_LIMITS.SIMPLE,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -542,11 +545,11 @@ describe('custom-gas selectors', () => {
|
|||||||
chainId: '0x1',
|
chainId: '0x1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: {
|
send: getInitialSendStateWithExistingTxState({
|
||||||
gas: {
|
gas: {
|
||||||
gasLimit: GAS_LIMITS.SIMPLE,
|
gasLimit: GAS_LIMITS.SIMPLE,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -591,11 +594,11 @@ describe('custom-gas selectors', () => {
|
|||||||
chainId: '0x1',
|
chainId: '0x1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: {
|
send: getInitialSendStateWithExistingTxState({
|
||||||
gas: {
|
gas: {
|
||||||
gasLimit: GAS_LIMITS.SIMPLE,
|
gasLimit: GAS_LIMITS.SIMPLE,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -640,11 +643,11 @@ describe('custom-gas selectors', () => {
|
|||||||
chainId: '0x4',
|
chainId: '0x4',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: {
|
send: getInitialSendStateWithExistingTxState({
|
||||||
gas: {
|
gas: {
|
||||||
gasLimit: GAS_LIMITS.SIMPLE,
|
gasLimit: GAS_LIMITS.SIMPLE,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -689,11 +692,11 @@ describe('custom-gas selectors', () => {
|
|||||||
chainId: '0x4',
|
chainId: '0x4',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: {
|
send: getInitialSendStateWithExistingTxState({
|
||||||
gas: {
|
gas: {
|
||||||
gasLimit: GAS_LIMITS.SIMPLE,
|
gasLimit: GAS_LIMITS.SIMPLE,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -738,11 +741,11 @@ describe('custom-gas selectors', () => {
|
|||||||
chainId: '0x1',
|
chainId: '0x1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
send: {
|
send: getInitialSendStateWithExistingTxState({
|
||||||
gas: {
|
gas: {
|
||||||
gasLimit: GAS_LIMITS.SIMPLE,
|
gasLimit: GAS_LIMITS.SIMPLE,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -27,7 +27,11 @@ import {
|
|||||||
getNotifications,
|
getNotifications,
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
} from '../selectors';
|
} from '../selectors';
|
||||||
import { computeEstimatedGasLimit, resetSendState } from '../ducks/send';
|
import {
|
||||||
|
computeEstimatedGasLimit,
|
||||||
|
initializeSendState,
|
||||||
|
resetSendState,
|
||||||
|
} from '../ducks/send';
|
||||||
import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account';
|
import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account';
|
||||||
import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask';
|
import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask';
|
||||||
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
|
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
|
||||||
@ -1443,6 +1447,11 @@ export function updateMetamaskState(newState) {
|
|||||||
type: actionConstants.CHAIN_CHANGED,
|
type: actionConstants.CHAIN_CHANGED,
|
||||||
payload: newProvider.chainId,
|
payload: newProvider.chainId,
|
||||||
});
|
});
|
||||||
|
// We dispatch this action to ensure that the send state stays up to date
|
||||||
|
// after the chain changes. This async thunk will fail gracefully in the
|
||||||
|
// event that we are not yet on the send flow with a draftTransaction in
|
||||||
|
// progress.
|
||||||
|
dispatch(initializeSendState({ chainHasChanged: true }));
|
||||||
}
|
}
|
||||||
dispatch({
|
dispatch({
|
||||||
type: actionConstants.UPDATE_METAMASK_STATE,
|
type: actionConstants.UPDATE_METAMASK_STATE,
|
||||||
|
Loading…
Reference in New Issue
Block a user