1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +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:
Brad Decker 2022-07-01 08:58:35 -05:00 committed by GitHub
parent f4b25d7ea5
commit 94967072f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 3229 additions and 2085 deletions

View File

@ -8,3 +8,16 @@ import contractMap from '@metamask/contract-metadata';
export const LISTED_CONTRACT_ADDRESSES = Object.keys(
contractMap,
).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.
*/

View File

@ -1,3 +1,8 @@
import {
draftTransactionInitialState,
initialState,
} from '../../ui/ducks/send';
export const TOP_ASSETS_GET_RESPONSE = [
{
symbol: 'LINK',
@ -103,3 +108,42 @@ export const createGasFeeEstimatesForFeeMarket = () => {
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',
},
},
});

View File

@ -9,7 +9,7 @@ import Tooltip from '../../ui/tooltip';
import InfoIcon from '../../ui/icon/info-icon.component';
import Button from '../../ui/button';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { updateSendAsset } from '../../../ducks/send';
import { startNewDraftTransaction } from '../../../ducks/send';
import { SEND_ROUTE } from '../../../helpers/constants/routes';
import { SEVERITIES } from '../../../helpers/constants/design-system';
import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys';
@ -74,7 +74,7 @@ const AssetListItem = ({
});
try {
await dispatch(
updateSendAsset({
startNewDraftTransaction({
type: ASSET_TYPES.TOKEN,
details: {
address: tokenAddress,

View File

@ -45,7 +45,7 @@ import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
import CollectibleOptions from '../collectible-options/collectible-options';
import Button from '../../ui/button';
import { updateSendAsset } from '../../../ducks/send';
import { startNewDraftTransaction } from '../../../ducks/send';
import InfoTooltip from '../../ui/info-tooltip';
import { ERC721 } from '../../../helpers/constants/common';
import { usePrevious } from '../../../hooks/usePrevious';
@ -120,7 +120,7 @@ export default function CollectibleDetails({ collectible }) {
const onSend = async () => {
await dispatch(
updateSendAsset({
startNewDraftTransaction({
type: ASSET_TYPES.COLLECTIBLE,
details: collectible,
}),

View File

@ -33,6 +33,8 @@ import { isHardwareKeyring } from '../../../helpers/utils/hardware';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import { EVENT } from '../../../../shared/constants/metametrics';
import Spinner from '../../ui/spinner';
import { startNewDraftTransaction } from '../../../ducks/send';
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
import WalletOverview from './wallet-overview';
const EthOverview = ({ className }) => {
@ -131,7 +133,11 @@ const EthOverview = ({ className }) => {
legacy_event: true,
},
});
history.push(SEND_ROUTE);
dispatch(
startNewDraftTransaction({ type: ASSET_TYPES.NATIVE }),
).then(() => {
history.push(SEND_ROUTE);
});
}}
/>
<IconButton

View File

@ -14,7 +14,7 @@ import {
} from '../../../helpers/constants/routes';
import { useTokenTracker } from '../../../hooks/useTokenTracker';
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
import { updateSendAsset } from '../../../ducks/send';
import { startNewDraftTransaction } from '../../../ducks/send';
import { setSwapsFromToken } from '../../../ducks/swaps/swaps';
import {
getCurrentKeyring,
@ -93,7 +93,7 @@ const TokenOverview = ({ className, token }) => {
});
try {
await dispatch(
updateSendAsset({
startNewDraftTransaction({
type: ASSET_TYPES.TOKEN,
details: token,
}),

View File

@ -118,7 +118,7 @@ export default class TokenInput extends PureComponent {
isEqualCaseInsensitive(address, token.address),
);
const tokenExchangeRate = tokenExchangeRates?.[existingToken.address] || 0;
const tokenExchangeRate = tokenExchangeRates?.[existingToken?.address] ?? 0;
let currency, numberOfDecimals;
if (hideConversion) {

295
ui/ducks/send/helpers.js Normal file
View 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);
}

View 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

View File

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import { compose } from 'redux';
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 { ASSET_TYPES } from '../../../shared/constants/transaction';
import ConfirmSendEther from './confirm-send-ether.component';
@ -20,7 +20,9 @@ const mapDispatchToProps = (dispatch) => {
return {
editTransaction: async (txData) => {
const { id } = txData;
await dispatch(editTransaction(ASSET_TYPES.NATIVE, id.toString()));
await dispatch(
editExistingTransaction(ASSET_TYPES.NATIVE, id.toString()),
);
dispatch(clearConfirmTransaction());
},
};

View File

@ -6,14 +6,15 @@ import { SEND_ROUTE } from '../../helpers/constants/routes';
export default class ConfirmSendToken extends Component {
static propTypes = {
history: PropTypes.object,
editTransaction: PropTypes.func,
editExistingTransaction: PropTypes.func,
tokenAmount: PropTypes.string,
};
handleEdit(confirmTransactionData) {
const { editTransaction, history } = this.props;
editTransaction(confirmTransactionData);
history.push(SEND_ROUTE);
const { editExistingTransaction, history } = this.props;
editExistingTransaction(confirmTransactionData).then(() => {
history.push(SEND_ROUTE);
});
}
render() {

View File

@ -3,7 +3,7 @@ import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
import { showSendTokenPage } from '../../store/actions';
import { editTransaction } from '../../ducks/send';
import { editExistingTransaction } from '../../ducks/send';
import { sendTokenTokenAmountAndToAddressSelector } from '../../selectors';
import { ASSET_TYPES } from '../../../shared/constants/transaction';
import ConfirmSendToken from './confirm-send-token.component';
@ -18,18 +18,11 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => {
return {
editTransaction: ({ txData, tokenData, tokenProps: assetDetails }) => {
editExistingTransaction: async ({ txData }) => {
const { id } = txData;
dispatch(
editTransaction(
ASSET_TYPES.TOKEN,
id.toString(),
tokenData,
assetDetails,
),
);
dispatch(clearConfirmTransaction());
dispatch(showSendTokenPage());
await dispatch(editExistingTransaction(ASSET_TYPES.TOKEN, id.toString()));
await dispatch(clearConfirmTransaction());
await dispatch(showSendTokenPage());
},
};
};

View File

@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import ConfirmTokenTransactionBase from '../confirm-token-transaction-base/confirm-token-transaction-base';
import { SEND_ROUTE } from '../../helpers/constants/routes';
import { editTransaction } from '../../ducks/send';
import { editExistingTransaction } from '../../ducks/send';
import {
contractExchangeRateSelector,
getCurrentCurrency,
@ -35,27 +35,17 @@ export default function ConfirmSendToken({
const dispatch = useDispatch();
const history = useHistory();
const handleEditTransaction = ({
txData,
tokenData,
tokenProps: assetDetails,
}) => {
const handleEditTransaction = async ({ txData }) => {
const { id } = txData;
dispatch(
editTransaction(
ASSET_TYPES.TOKEN,
id.toString(),
tokenData,
assetDetails,
),
);
await dispatch(editExistingTransaction(ASSET_TYPES.TOKEN, id.toString()));
dispatch(clearConfirmTransaction());
dispatch(showSendTokenPage());
};
const handleEdit = (confirmTransactionData) => {
handleEditTransaction(confirmTransactionData);
history.push(SEND_ROUTE);
handleEditTransaction(confirmTransactionData).then(() => {
history.push(SEND_ROUTE);
});
};
const conversionRate = useSelector(getConversionRate);
const nativeCurrency = useSelector(getNativeCurrency);

View File

@ -3,9 +3,13 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
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 { 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';
const middleware = [thunk];
@ -22,7 +26,7 @@ describe('AmountMaxButton Component', () => {
EIPS: {},
},
},
send: initialState,
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
}),
);
expect(getByText('Max')).toBeTruthy();
@ -36,12 +40,14 @@ describe('AmountMaxButton Component', () => {
EIPS: {},
},
},
send: { ...initialState, status: SEND_STATUSES.VALID },
send: getInitialSendStateWithExistingTxState({
status: SEND_STATUSES.VALID,
}),
});
const { getByText } = renderWithProvider(<AmountMaxButton />, store);
const expectedActions = [
{ type: 'send/updateAmountMode', payload: 'MAX' },
{ type: 'send/updateAmountMode', payload: AMOUNT_MODES.MAX },
];
fireEvent.click(getByText('Max'), { bubbles: true });
@ -58,9 +64,10 @@ describe('AmountMaxButton Component', () => {
},
},
send: {
...initialState,
status: SEND_STATUSES.VALID,
amount: { ...initialState.amount, mode: 'MAX' },
...getInitialSendStateWithExistingTxState({
status: SEND_STATUSES.VALID,
}),
amountMode: AMOUNT_MODES.MAX,
},
});
const { getByText } = renderWithProvider(<AmountMaxButton />, store);

View File

@ -3,9 +3,13 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
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 { ASSET_TYPES } from '../../../../shared/constants/transaction';
import {
getInitialSendStateWithExistingTxState,
INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
} from '../../../../test/jest/mocks';
import SendHeader from './send-header.component';
const middleware = [thunk];
@ -26,7 +30,7 @@ describe('SendHeader Component', () => {
const { getByText, rerender } = renderWithProvider(
<SendHeader />,
configureMockStore(middleware)({
send: initialState,
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
gas: { basicEstimateStatus: 'LOADING' },
history: { mostRecentOverviewPage: 'activity' },
}),
@ -35,7 +39,10 @@ describe('SendHeader Component', () => {
rerender(
<SendHeader />,
configureMockStore(middleware)({
send: { ...initialState, stage: SEND_STAGES.ADD_RECIPIENT },
send: {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
stage: SEND_STAGES.ADD_RECIPIENT,
},
gas: { basicEstimateStatus: 'LOADING' },
history: { mostRecentOverviewPage: 'activity' },
}),
@ -48,9 +55,12 @@ describe('SendHeader Component', () => {
<SendHeader />,
configureMockStore(middleware)({
send: {
...initialState,
...INITIAL_SEND_STATE_FOR_EXISTING_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' },
history: { mostRecentOverviewPage: 'activity' },
@ -64,9 +74,12 @@ describe('SendHeader Component', () => {
<SendHeader />,
configureMockStore(middleware)({
send: {
...initialState,
...getInitialSendStateWithExistingTxState({
asset: {
type: ASSET_TYPES.TOKEN,
},
}),
stage: SEND_STAGES.DRAFT,
asset: { ...initialState.asset, type: ASSET_TYPES.TOKEN },
},
gas: { basicEstimateStatus: 'LOADING' },
history: { mostRecentOverviewPage: 'activity' },
@ -80,7 +93,7 @@ describe('SendHeader Component', () => {
<SendHeader />,
configureMockStore(middleware)({
send: {
...initialState,
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
stage: SEND_STAGES.EDIT,
},
gas: { basicEstimateStatus: 'LOADING' },
@ -96,7 +109,7 @@ describe('SendHeader Component', () => {
const { getByText } = renderWithProvider(
<SendHeader />,
configureMockStore(middleware)({
send: initialState,
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
gas: { basicEstimateStatus: 'LOADING' },
history: { mostRecentOverviewPage: 'activity' },
}),
@ -108,7 +121,10 @@ describe('SendHeader Component', () => {
const { getByText } = renderWithProvider(
<SendHeader />,
configureMockStore(middleware)({
send: { ...initialState, stage: SEND_STAGES.EDIT },
send: {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
stage: SEND_STAGES.EDIT,
},
gas: { basicEstimateStatus: 'LOADING' },
history: { mostRecentOverviewPage: 'activity' },
}),
@ -118,7 +134,7 @@ describe('SendHeader Component', () => {
it('resets send state when clicked', () => {
const store = configureMockStore(middleware)({
send: initialState,
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
gas: { basicEstimateStatus: 'LOADING' },
history: { mostRecentOverviewPage: 'activity' },
});

View File

@ -7,14 +7,13 @@ import {
getRecipient,
getRecipientUserInput,
getSendStage,
initializeSendState,
resetRecipientInput,
resetSendState,
SEND_STAGES,
updateRecipient,
updateRecipientUserInput,
} from '../../ducks/send';
import { getCurrentChainId, isCustomPriceExcessive } from '../../selectors';
import { isCustomPriceExcessive } from '../../selectors';
import { getSendHexDataFeatureFlagState } from '../../ducks/metamask/metamask';
import { showQrScanner } from '../../store/actions';
import { MetaMetricsContext } from '../../contexts/metametrics';
@ -30,7 +29,6 @@ const sendSliceIsCustomPriceExcessive = (state) =>
export default function SendTransactionScreen() {
const history = useHistory();
const chainId = useSelector(getCurrentChainId);
const stage = useSelector(getSendStage);
const gasIsExcessive = useSelector(sendSliceIsCustomPriceExcessive);
const isUsingMyAccountsForRecipientSearch = useSelector(
@ -49,11 +47,8 @@ export default function SendTransactionScreen() {
}, [dispatch]);
useEffect(() => {
if (chainId !== undefined) {
dispatch(initializeSendState());
window.addEventListener('beforeunload', cleanup);
}
}, [chainId, dispatch, cleanup]);
window.addEventListener('beforeunload', cleanup);
}, [cleanup]);
useEffect(() => {
if (location.search === '?scan=true') {

View File

@ -3,11 +3,12 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
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 { renderWithProvider } from '../../../test/jest';
import { RINKEBY_CHAIN_ID } from '../../../shared/constants/network';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
import { INITIAL_SEND_STATE_FOR_EXISTING_DRAFT } from '../../../test/jest/mocks';
import Send from './send';
const middleware = [thunk];
@ -34,7 +35,7 @@ jest.mock(
);
const baseStore = {
send: initialState,
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
ENS: ensInitialState,
gas: {
customData: { limit: null, price: null },
@ -87,7 +88,7 @@ const baseStore = {
describe('Send Page', () => {
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);
renderWithProvider(<Send />, store);
const actions = store.getActions();
@ -96,9 +97,6 @@ describe('Send Page', () => {
expect.objectContaining({
type: 'ENS/enableEnsLookup',
}),
expect.objectContaining({
type: 'send/initializeSendState/pending',
}),
]),
);
});
@ -113,9 +111,6 @@ describe('Send Page', () => {
expect.objectContaining({
type: 'ENS/enableEnsLookup',
}),
expect.objectContaining({
type: 'send/initializeSendState/pending',
}),
expect.objectContaining({
type: 'UI_MODAL_OPEN',
payload: { name: 'QR_SCANNER' },

View File

@ -8,7 +8,7 @@ import { decEthToConvertedCurrency as ethTotalToConvertedCurrency } from '../hel
import { formatETHFee } from '../helpers/utils/formatters';
import { calcGasTotal } from '../pages/send/send.utils';
import { getGasPrice } from '../ducks/send';
import { getGasLimit, getGasPrice } from '../ducks/send';
import {
GAS_ESTIMATE_TYPES as GAS_FEE_CONTROLLER_ESTIMATE_TYPES,
GAS_LIMITS,
@ -321,8 +321,9 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) {
return [];
}
const showFiat = getShouldShowFiat(state);
const gasLimit =
state.send.gas.gasLimit || getCustomGasLimit(state) || GAS_LIMITS.SIMPLE;
getGasLimit(state) ?? getCustomGasLimit(state) ?? GAS_LIMITS.SIMPLE;
const { conversionRate } = state.metamask;
const currentCurrency = getCurrentCurrency(state);
const gasFeeEstimates = getGasFeeEstimates(state);

View File

@ -1,4 +1,5 @@
import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../shared/constants/gas';
import { getInitialSendStateWithExistingTxState } from '../../test/jest/mocks';
import {
getCustomGasLimit,
getCustomGasPrice,
@ -11,7 +12,9 @@ import {
describe('custom-gas selectors', () => {
describe('getCustomGasPrice()', () => {
it('should return gas.customData.price', () => {
const mockState = { gas: { customData: { price: 'mockPrice' } } };
const mockState = {
gas: { customData: { price: 'mockPrice' } },
};
expect(getCustomGasPrice(mockState)).toStrictEqual('mockPrice');
});
});
@ -200,11 +203,11 @@ describe('custom-gas selectors', () => {
EIPS: {},
},
},
send: {
send: getInitialSendStateWithExistingTxState({
gas: {
gasPrice: '0x28bed0160',
},
},
}),
gas: {
customData: { price: null },
},
@ -222,11 +225,11 @@ describe('custom-gas selectors', () => {
EIPS: {},
},
},
send: {
send: getInitialSendStateWithExistingTxState({
gas: {
gasPrice: '0x30e4f9b400',
},
},
}),
gas: {
customData: { price: null },
},
@ -330,11 +333,11 @@ describe('custom-gas selectors', () => {
chainId: '0x1',
},
},
send: {
send: getInitialSendStateWithExistingTxState({
gas: {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
}),
},
},
{
@ -379,11 +382,11 @@ describe('custom-gas selectors', () => {
chainId: '0x4',
},
},
send: {
send: getInitialSendStateWithExistingTxState({
gas: {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
}),
},
},
{
@ -428,11 +431,11 @@ describe('custom-gas selectors', () => {
chainId: '0x4',
},
},
send: {
send: getInitialSendStateWithExistingTxState({
gas: {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
}),
},
},
{
@ -477,11 +480,11 @@ describe('custom-gas selectors', () => {
chainId: '0x1',
},
},
send: {
send: getInitialSendStateWithExistingTxState({
gas: {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
}),
},
},
];
@ -542,11 +545,11 @@ describe('custom-gas selectors', () => {
chainId: '0x1',
},
},
send: {
send: getInitialSendStateWithExistingTxState({
gas: {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
}),
},
},
{
@ -591,11 +594,11 @@ describe('custom-gas selectors', () => {
chainId: '0x1',
},
},
send: {
send: getInitialSendStateWithExistingTxState({
gas: {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
}),
},
},
{
@ -640,11 +643,11 @@ describe('custom-gas selectors', () => {
chainId: '0x4',
},
},
send: {
send: getInitialSendStateWithExistingTxState({
gas: {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
}),
},
},
{
@ -689,11 +692,11 @@ describe('custom-gas selectors', () => {
chainId: '0x4',
},
},
send: {
send: getInitialSendStateWithExistingTxState({
gas: {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
}),
},
},
{
@ -738,11 +741,11 @@ describe('custom-gas selectors', () => {
chainId: '0x1',
},
},
send: {
send: getInitialSendStateWithExistingTxState({
gas: {
gasLimit: GAS_LIMITS.SIMPLE,
},
},
}),
},
},
];

View File

@ -27,7 +27,11 @@ import {
getNotifications,
///: END:ONLY_INCLUDE_IN
} from '../selectors';
import { computeEstimatedGasLimit, resetSendState } from '../ducks/send';
import {
computeEstimatedGasLimit,
initializeSendState,
resetSendState,
} from '../ducks/send';
import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account';
import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
@ -1443,6 +1447,11 @@ export function updateMetamaskState(newState) {
type: actionConstants.CHAIN_CHANGED,
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({
type: actionConstants.UPDATE_METAMASK_STATE,