import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import abi from 'human-standard-token-abi';
import abiERC721 from 'human-standard-collectible-abi';
import BigNumber from 'bignumber.js';
import { addHexPrefix } from 'ethereumjs-util';
import { debounce } from 'lodash';
import {
  conversionGreaterThan,
  conversionUtil,
  multiplyCurrencies,
  subtractCurrencies,
} from '../../../shared/modules/conversion.utils';
import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../../shared/constants/gas';
import {
  CONTRACT_ADDRESS_ERROR,
  INSUFFICIENT_FUNDS_ERROR,
  INSUFFICIENT_TOKENS_ERROR,
  INVALID_RECIPIENT_ADDRESS_ERROR,
  INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR,
  KNOWN_RECIPIENT_ADDRESS_WARNING,
  MIN_GAS_LIMIT_HEX,
  NEGATIVE_ETH_ERROR,
} from '../../pages/send/send.constants';

import {
  addGasBuffer,
  calcGasTotal,
  generateERC20TransferData,
  generateERC721TransferData,
  getAssetTransferData,
  isBalanceSufficient,
  isTokenBalanceSufficient,
} from '../../pages/send/send.utils';
import {
  getAddressBookEntry,
  getAdvancedInlineGasShown,
  getCurrentChainId,
  getGasPriceInHexWei,
  getIsMainnet,
  getSelectedAddress,
  getTargetAccount,
  getIsNonStandardEthChain,
  checkNetworkAndAccountSupports1559,
  getUseTokenDetection,
  getTokenList,
  getAddressBookEntryOrAccountName,
} from '../../selectors';
import {
  disconnectGasFeeEstimatePoller,
  displayWarning,
  estimateGas,
  getGasFeeEstimatesAndStartPolling,
  hideLoadingIndication,
  showConfTxPage,
  showLoadingIndication,
  updateTransaction,
  addPollingTokenToAppState,
  removePollingTokenFromAppState,
  isCollectibleOwner,
  getTokenStandardAndDetails,
  showModal,
} from '../../store/actions';
import { setCustomGasLimit } from '../gas/gas.duck';
import {
  QR_CODE_DETECTED,
  SELECTED_ACCOUNT_CHANGED,
  ACCOUNT_CHANGED,
  ADDRESS_BOOK_UPDATED,
  GAS_FEE_ESTIMATES_UPDATED,
} from '../../store/actionConstants';
import {
  calcTokenAmount,
  getTokenAddressParam,
  getTokenValueParam,
} from '../../helpers/utils/token-util';
import {
  checkExistingAddresses,
  isDefaultMetaMaskChain,
  isOriginContractAddress,
  isValidDomainName,
  isEqualCaseInsensitive,
} from '../../helpers/utils/util';
import {
  getGasEstimateType,
  getTokens,
  getUnapprovedTxs,
} from '../metamask/metamask';

import { resetEnsResolution } from '../ens';
import {
  isBurnAddress,
  isValidHexAddress,
} from '../../../shared/modules/hexstring-utils';
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network';
import {
  ERC20,
  ERC721,
  ERC1155,
  ETH,
  GWEI,
} from '../../helpers/constants/common';
import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction';
import { readAddressAsContract } from '../../../shared/modules/contract-utils';
import { INVALID_ASSET_TYPE } from '../../helpers/constants/error-keys';
// typedefs
/**
 * @typedef {import('@reduxjs/toolkit').PayloadAction} PayloadAction
 */

const name = 'send';

/**
 * The Stages that the send slice can be in
 * 1. INACTIVE - The send state is idle, and hasn't yet fetched required
 *  data for gasPrice and gasLimit estimations, etc.
 * 2. ADD_RECIPIENT - The user is selecting which address to send an asset to
 * 3. DRAFT - The send form is shown for a transaction yet to be sent to the
 *  Transaction Controller.
 * 4. EDIT - The send form is shown for a transaction already submitted to the
 *  Transaction Controller but not yet confirmed. This happens when a
 *  confirmation is shown for a transaction and the 'edit' button in the header
 *  is clicked.
 */
export const SEND_STAGES = {
  INACTIVE: 'INACTIVE',
  ADD_RECIPIENT: 'ADD_RECIPIENT',
  DRAFT: 'DRAFT',
  EDIT: 'EDIT',
};

/**
 * The status that the send slice can be in is either
 * 1. VALID - the transaction is valid and can be submitted
 * 2. INVALID - the transaction is invalid and cannot be submitted
 *
 * A number of cases would result in an invalid form
 * 1. The recipient is not yet defined
 * 2. The amount + gasTotal is greater than the user's balance when sending
 *  native currency
 * 3. The gasTotal is greater than the user's *native* balance
 * 4. The amount of sent asset is greater than the user's *asset* balance
 * 5. Gas price estimates failed to load entirely
 * 6. The gasLimit is less than 21000 (0x5208)
 */
export const SEND_STATUSES = {
  VALID: 'VALID',
  INVALID: 'INVALID',
};

/**
 * Controls what is displayed in the send-gas-row component.
 * 1. BASIC - Shows the basic estimate slow/avg/fast buttons when on mainnet
 *  and the metaswaps API request is successful.
 * 2. INLINE - Shows inline gasLimit/gasPrice fields when on any other network
 *  or metaswaps API fails and we use eth_gasPrice
 * 3. CUSTOM - Shows GasFeeDisplay component that is a read only display of the
 *  values the user has set in the advanced gas modal (stored in the gas duck
 *  under the customData key).
 */
export const GAS_INPUT_MODES = {
  BASIC: 'BASIC',
  INLINE: 'INLINE',
  CUSTOM: 'CUSTOM',
};

/**
 * The types of assets that a user can send
 * 1. NATIVE - The native asset for the current network, such as ETH
 * 2. TOKEN - An ERC20 token.
 * 2. COLLECTIBLE - An ERC721 or ERC1155 token.
 */
export const ASSET_TYPES = {
  NATIVE: 'NATIVE',
  TOKEN: 'TOKEN',
  COLLECTIBLE: 'COLLECTIBLE',
};

/**
 * The modes that the amount field can be set by
 * 1. INPUT - the user provides the amount by typing in the field
 * 2. MAX - The user selects the MAX button and amount is calculated based on
 *  balance - (amount + gasTotal)
 */
export const AMOUNT_MODES = {
  INPUT: 'INPUT',
  MAX: 'MAX',
};

export const RECIPIENT_SEARCH_MODES = {
  MY_ACCOUNTS: 'MY_ACCOUNTS',
  CONTACT_LIST: 'CONTACT_LIST',
};

async function estimateGasLimitForSend({
  selectedAddress,
  value,
  gasPrice,
  sendToken,
  to,
  data,
  isNonStandardEthChain,
  chainId,
  ...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',
      );
    if (simulationFailed) {
      const estimateWithBuffer = addGasBuffer(
        paramsForGasEstimate.gas,
        blockGasLimit,
        1.5,
      );
      return addHexPrefix(estimateWithBuffer);
    }
    throw error;
  }
}

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);
}

// After modification of specific fields in specific circumstances we must
// recompute the gasLimit estimate to be as accurate as possible. the cases
// that necessitate this logic are listed below:
// 1. when the amount sent changes when sending a token due to the amount being
//    part of the hex encoded data property of the transaction.
// 2. when updating the data property while sending NATIVE currency (ex: ETH)
//    because the data parameter defines function calls that the EVM will have
//    to execute which is where a large chunk of gas is potentially consumed.
// 3. when the recipient changes while sending a token due to the recipient's
//    address being included in the hex encoded data property of the
//    transaction
// 4. when the asset being sent changes due to the contract address and details
//    of the token being included in the hex encoded data property of the
//    transaction. If switching to NATIVE currency (ex: ETH), the gasLimit will
//    change due to hex data being removed (unless supplied by user).
// This method computes the gasLimit estimate which is written to state in an
// action handler in extraReducers.
export const computeEstimatedGasLimit = createAsyncThunk(
  'send/computeEstimatedGasLimit',
  async (_, thunkApi) => {
    const state = thunkApi.getState();
    const { send, metamask } = state;
    const unapprovedTxs = getUnapprovedTxs(state);
    const transaction = unapprovedTxs[send.draftTransaction.id];
    const isNonStandardEthChain = getIsNonStandardEthChain(state);
    const chainId = getCurrentChainId(state);
    if (
      send.stage !== SEND_STAGES.EDIT ||
      !transaction.dappSuggestedGasFees?.gas ||
      !transaction.userEditedGasLimit
    ) {
      const gasLimit = await estimateGasLimitForSend({
        gasPrice: send.gas.gasPrice,
        blockGasLimit: metamask.currentBlockGasLimit,
        selectedAddress: metamask.selectedAddress,
        sendToken: send.asset.details,
        to: send.recipient.address?.toLowerCase(),
        value: send.amount.value,
        data: send.draftTransaction.userInputHexData,
        isNonStandardEthChain,
        chainId,
      });
      await thunkApi.dispatch(setCustomGasLimit(gasLimit));
      return {
        gasLimit,
      };
    }
    return null;
  },
);

/**
 * 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 {T} gasPriceEstimate
 * @returns
 */
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);
}

/**
 * Responsible for initializing required state for the send slice.
 * This method is dispatched from the send page in the componentDidMount
 * method. It is also dispatched anytime the network changes to ensure that
 * the slice remains valid with changing token and account balances. To do so
 * it keys into state to get necessary values and computes a starting point for
 * the send slice. It returns the values that might change from this action and
 * those values are written to the slice in the `initializeSendState.fulfilled`
 * action handler.
 */
export const initializeSendState = createAsyncThunk(
  'send/initializeSendState',
  async (_, thunkApi) => {
    const state = thunkApi.getState();
    const isNonStandardEthChain = getIsNonStandardEthChain(state);
    const chainId = getCurrentChainId(state);
    const eip1559support = checkNetworkAndAccountSupports1559(state);
    const {
      send: { asset, stage, recipient, amount, draftTransaction },
      metamask,
    } = state;

    // First determine the correct from address. For new sends this is always
    // the currently selected account and switching accounts switches the from
    // address. If editing an existing transaction (by clicking 'edit' on the
    // send page), the fromAddress is always the address from the txParams.
    const fromAddress =
      stage === SEND_STAGES.EDIT
        ? draftTransaction.txParams.from
        : metamask.selectedAddress;
    // We need the account's balance which is calculated from cachedBalances in
    // the getMetaMaskAccounts selector. getTargetAccount consumes this
    // selector and returns the account at the specified address.
    const account = getTargetAccount(state, fromAddress);

    // Default gasPrice to 1 gwei if all estimation fails, this is only used
    // for gasLimit estimation and won't be set directly in state. Instead, we
    // will return the gasFeeEstimates and gasEstimateType so that the reducer
    // can set the appropriate gas fees in state.
    let gasPrice = '0x1';
    let gasEstimatePollToken = null;

    // Instruct the background process that polling for gas prices should begin
    gasEstimatePollToken = await getGasFeeEstimatesAndStartPolling();

    addPollingTokenToAppState(gasEstimatePollToken);

    const {
      metamask: { gasFeeEstimates, gasEstimateType },
    } = thunkApi.getState();

    // Because we are only interested in getting a gasLimit estimation we only
    // need to worry about gasPrice. So we use maxFeePerGas as gasPrice if we
    // have a fee market estimation.
    if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
      gasPrice = getGasPriceInHexWei(gasFeeEstimates.medium);
    } else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
      gasPrice = getRoundedGasPrice(gasFeeEstimates.gasPrice);
    } else if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
      gasPrice = getGasPriceInHexWei(
        gasFeeEstimates.medium.suggestedMaxFeePerGas,
      );
    } else {
      gasPrice = gasFeeEstimates.gasPrice
        ? getRoundedGasPrice(gasFeeEstimates.gasPrice)
        : '0x0';
    }

    // Set a basic gasLimit in the event that other estimation fails
    let gasLimit =
      asset.type === ASSET_TYPES.TOKEN || asset.type === ASSET_TYPES.COLLECTIBLE
        ? GAS_LIMITS.BASE_TOKEN_ESTIMATE
        : GAS_LIMITS.SIMPLE;
    if (
      gasEstimateType !== GAS_ESTIMATE_TYPES.NONE &&
      stage !== SEND_STAGES.EDIT &&
      recipient.address
    ) {
      // Run our estimateGasLimit logic to get a more accurate estimation of
      // required gas. If this value isn't nullish, set it as the new gasLimit
      const estimatedGasLimit = await estimateGasLimitForSend({
        gasPrice,
        blockGasLimit: metamask.currentBlockGasLimit,
        selectedAddress: fromAddress,
        sendToken: asset.details,
        to: recipient.address.toLowerCase(),
        value: amount.value,
        data: draftTransaction.userInputHexData,
        isNonStandardEthChain,
        chainId,
      });
      gasLimit = estimatedGasLimit || gasLimit;
    }
    // We have to keep the gas slice in sync with the draft send transaction
    // so that it'll be initialized correctly if the gas modal is opened.
    await thunkApi.dispatch(setCustomGasLimit(gasLimit));
    // We must determine the balance of the asset that the transaction will be
    // sending. This is done by referencing the native balance on the account
    // for native assets, and calling the balanceOf method on the ERC20
    // contract for token sends.
    let { balance } = account;
    if (asset.type === ASSET_TYPES.TOKEN) {
      if (asset.details === null) {
        // If we're sending a token but details have not been provided we must
        // abort and set the send slice into invalid status.
        throw new Error(
          'Send slice initialized as token send without token details',
        );
      }
      balance = await getERC20Balance(asset.details, fromAddress);
    }

    if (asset.type === ASSET_TYPES.COLLECTIBLE) {
      if (asset.details === null) {
        // If we're sending a collectible but details have not been provided we must
        // abort and set the send slice into invalid status.
        throw new Error(
          'Send slice initialized as collectibles send without token details',
        );
      }
      balance = '0x1';
    }
    return {
      address: fromAddress,
      nativeBalance: account.balance,
      assetBalance: balance,
      chainId: getCurrentChainId(state),
      tokens: getTokens(state),
      gasFeeEstimates,
      gasEstimateType,
      gasLimit,
      gasTotal: addHexPrefix(calcGasTotal(gasLimit, gasPrice)),
      gasEstimatePollToken,
      eip1559support,
      useTokenDetection: getUseTokenDetection(state),
      tokenAddressList: Object.keys(getTokenList(state)),
    };
  },
);

export const initialState = {
  // which stage of the send flow is the user on
  stage: SEND_STAGES.INACTIVE,
  // status of the send slice, either VALID or INVALID
  status: SEND_STATUSES.VALID,
  // Determines type of transaction being sent, defaulted to 0x0 (legacy)
  transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY,
  // tracks whether the current network supports EIP 1559 transactions
  eip1559support: false,
  account: {
    // from account address, defaults to selected account. will be the account
    // the original transaction was sent from in the case of the EDIT stage
    address: null,
    // balance of the from account
    balance: '0x0',
  },
  gas: {
    // indicate whether the gas estimate is loading
    isGasEstimateLoading: true,
    // String token identifying a listener for polling on the gasFeeController
    gasEstimatePollToken: null,
    // has the user set custom gas in the custom gas modal
    isCustomGasSet: false,
    // maximum gas needed for tx
    gasLimit: '0x0',
    // price in wei to pay per gas
    gasPrice: '0x0',
    // maximum price in wei to pay per gas
    maxFeePerGas: '0x0',
    // maximum priority fee in wei to pay per gas
    maxPriorityFeePerGas: '0x0',
    // expected price in wei necessary to pay per gas used for a transaction
    // to be included in a reasonable timeframe. Comes from GasFeeController.
    gasPriceEstimate: '0x0',
    // maximum total price in wei to pay
    gasTotal: '0x0',
    // minimum supported gasLimit
    minimumGasLimit: GAS_LIMITS.SIMPLE,
    // error to display for gas fields
    error: null,
  },
  amount: {
    // The mode to use when determining new amounts. For INPUT mode the
    // provided payload is always used. For MAX it is calculated based on avail
    // asset balance
    mode: AMOUNT_MODES.INPUT,
    // Current value of the transaction, how much of the asset are we sending
    value: '0x0',
    // error to display for amount field
    error: null,
  },
  asset: {
    // type can be either NATIVE such as ETH or TOKEN for ERC20 tokens
    type: ASSET_TYPES.NATIVE,
    // the balance the user holds at the from address for this asset
    balance: '0x0',
    // In the case of tokens, the address, decimals and symbol of the token
    // will be included in details
    details: null,
    // error to display when there is an issue with the asset
    error: null,
  },
  draftTransaction: {
    // The metamask internal id of the transaction. Only populated in the EDIT
    // stage.
    id: null,
    // The hex encoded data provided by the user who has enabled hex data field
    // in advanced settings
    userInputHexData: null,
    // The txParams that should be submitted to the network once this
    // transaction is confirmed. This object is computed on every write to the
    // slice of fields that would result in the txParams changing
    txParams: {
      to: '',
      from: '',
      data: undefined,
      value: '0x0',
      gas: '0x0',
      gasPrice: '0x0',
      type: TRANSACTION_ENVELOPE_TYPES.LEGACY,
    },
  },
  recipient: {
    // Defines which mode to use for searching for matches in the input field
    mode: RECIPIENT_SEARCH_MODES.CONTACT_LIST,
    // Partial, not yet validated, entry into the address field. Used to share
    // user input amongst the AddRecipient and EnsInput components.
    userInput: '',
    // The address of the recipient
    address: '',
    // The nickname stored in the user's address book for the recipient address
    nickname: '',
    // Error to display on the address field
    error: null,
    // Warning to display on the address field
    warning: null,
  },
};

const slice = createSlice({
  name,
  initialState,
  reducers: {
    /**
     * update current amount.value in state and run post update validation of
     * the amount field and the send state. Recomputes the draftTransaction
     *
     * @param state
     * @param action
     */
    updateSendAmount: (state, action) => {
      state.amount.value = addHexPrefix(action.payload);
      // Once amount has changed, validate the field
      slice.caseReducers.validateAmountField(state);
      if (state.asset.type === ASSET_TYPES.NATIVE) {
        // if sending the native asset the amount being sent will impact the
        // gas field as well because the gas validation takes into
        // consideration the available balance minus amount sent before
        // checking if there is enough left to cover the gas fee.
        slice.caseReducers.validateGasField(state);
      }
      // validate send state
      slice.caseReducers.validateSendState(state);
    },
    /**
     * computes the maximum amount of asset that can be sent and then calls
     * the updateSendAmount action above with the computed value, which will
     * revalidate the field and form and recomputes the draftTransaction
     *
     * @param state
     */
    updateAmountToMax: (state) => {
      let amount = '0x0';
      if (state.asset.type === ASSET_TYPES.TOKEN) {
        const decimals = state.asset.details?.decimals ?? 0;
        const multiplier = Math.pow(10, Number(decimals));

        amount = multiplyCurrencies(state.asset.balance, multiplier, {
          toNumericBase: 'hex',
          multiplicandBase: 16,
          multiplierBase: 10,
        });
      } else {
        amount = subtractCurrencies(
          addHexPrefix(state.asset.balance),
          addHexPrefix(state.gas.gasTotal),
          {
            toNumericBase: 'hex',
            aBase: 16,
            bBase: 16,
          },
        );
      }
      slice.caseReducers.updateSendAmount(state, {
        payload: amount,
      });
      // draftTransaction update happens in updateSendAmount
    },
    /**
     * updates the draftTransaction.userInputHexData state key and then
     * recomputes the draftTransaction if the user is currently sending the
     * native asset. When sending ERC20 assets, this is unnecessary because the
     * hex data used in the transaction will be that for interacting with the
     * ERC20 contract
     *
     * @param state
     * @param action
     */
    updateUserInputHexData: (state, action) => {
      state.draftTransaction.userInputHexData = action.payload;
      if (state.asset.type === ASSET_TYPES.NATIVE) {
        slice.caseReducers.updateDraftTransaction(state);
      }
    },
    /**
     * Initiates the edit transaction flow by setting the stage to 'EDIT' and
     * then pulling the details of the previously submitted transaction from
     * the action payload. It also computes a new draftTransaction that will be
     * used when updating the transaction in the provider
     *
     * @param state
     * @param action
     */
    editTransaction: (state, action) => {
      state.stage = SEND_STAGES.EDIT;
      state.gas.gasLimit = action.payload.gasLimit;
      state.gas.gasPrice = action.payload.gasPrice;
      state.amount.value = action.payload.amount;
      state.gas.error = null;
      state.amount.error = null;
      state.asset.error = null;
      state.recipient.address = action.payload.address;
      state.recipient.nickname = action.payload.nickname;
      state.draftTransaction.id = action.payload.id;
      state.draftTransaction.txParams.from = action.payload.from;
      state.draftTransaction.userInputHexData = action.payload.data;
      slice.caseReducers.updateDraftTransaction(state);
    },
    /**
     * gasTotal is computed based on gasPrice and gasLimit and set in state
     * recomputes the maximum amount if the current amount mode is 'MAX' and
     * sending the native token. ERC20 assets max amount is unaffected by
     * gasTotal so does not need to be recomputed. Finally, validates the gas
     * field and send state, then updates the draft transaction.
     *
     * @param state
     */
    calculateGasTotal: (state) => {
      // use maxFeePerGas as the multiplier if working with a FEE_MARKET transaction
      // otherwise use gasPrice
      if (state.transactionType === TRANSACTION_ENVELOPE_TYPES.FEE_MARKET) {
        state.gas.gasTotal = addHexPrefix(
          calcGasTotal(state.gas.gasLimit, state.gas.maxFeePerGas),
        );
      } else {
        state.gas.gasTotal = addHexPrefix(
          calcGasTotal(state.gas.gasLimit, state.gas.gasPrice),
        );
      }
      if (
        state.amount.mode === AMOUNT_MODES.MAX &&
        state.asset.type === ASSET_TYPES.NATIVE
      ) {
        slice.caseReducers.updateAmountToMax(state);
      }
      slice.caseReducers.validateAmountField(state);
      slice.caseReducers.validateGasField(state);
      // validate send state
      slice.caseReducers.validateSendState(state);
    },
    /**
     * sets the provided gasLimit in state and then recomputes the gasTotal.
     *
     * @param state
     * @param action
     */
    updateGasLimit: (state, action) => {
      state.gas.gasLimit = addHexPrefix(action.payload);
      slice.caseReducers.calculateGasTotal(state);
    },
    /**
     * Sets the appropriate gas fees in state and determines and sets the
     * appropriate transactionType based on gas fee fields received.
     *
     * @param state
     * @param action
     */
    updateGasFees: (state, action) => {
      if (
        action.payload.transactionType === TRANSACTION_ENVELOPE_TYPES.FEE_MARKET
      ) {
        state.gas.maxFeePerGas = addHexPrefix(action.payload.maxFeePerGas);
        state.gas.maxPriorityFeePerGas = addHexPrefix(
          action.payload.maxPriorityFeePerGas,
        );
        state.transactionType = TRANSACTION_ENVELOPE_TYPES.FEE_MARKET;
      } else {
        // Until we remove the old UI we don't want to automatically update
        // gasPrice if the user has already manually changed the field value.
        // When receiving a new estimate the isAutomaticUpdate property will be
        // on the payload (and set to true). If isAutomaticUpdate is true,
        // then we check if the previous estimate was '0x0' or if the previous
        // gasPrice equals the previous gasEstimate. if either of those cases
        // are true then we update the gasPrice otherwise we skip it because
        // it indicates the user has ejected from the estimates by modifying
        // the field.
        if (
          action.payload.isAutomaticUpdate !== true ||
          state.gas.gasPriceEstimate === '0x0' ||
          state.gas.gasPrice === state.gas.gasPriceEstimate
        ) {
          state.gas.gasPrice = addHexPrefix(action.payload.gasPrice);
        }
        state.transactionType = TRANSACTION_ENVELOPE_TYPES.LEGACY;
      }
      slice.caseReducers.calculateGasTotal(state);
    },
    /**
     * Sets the appropriate gas fees in state after receiving new estimates.
     *
     * @param state
     * @param action
     */
    updateGasFeeEstimates: (state, action) => {
      const { gasFeeEstimates, gasEstimateType } = action.payload;
      let gasPriceEstimate = '0x0';
      switch (gasEstimateType) {
        case GAS_ESTIMATE_TYPES.FEE_MARKET:
          slice.caseReducers.updateGasFees(state, {
            payload: {
              transactionType: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET,
              maxFeePerGas: getGasPriceInHexWei(
                gasFeeEstimates.medium.suggestedMaxFeePerGas,
              ),
              maxPriorityFeePerGas: getGasPriceInHexWei(
                gasFeeEstimates.medium.suggestedMaxPriorityFeePerGas,
              ),
            },
          });
          break;
        case GAS_ESTIMATE_TYPES.LEGACY:
          gasPriceEstimate = getRoundedGasPrice(gasFeeEstimates.medium);
          slice.caseReducers.updateGasFees(state, {
            payload: {
              gasPrice: gasPriceEstimate,
              type: TRANSACTION_ENVELOPE_TYPES.LEGACY,
              isAutomaticUpdate: true,
            },
          });
          break;
        case GAS_ESTIMATE_TYPES.ETH_GASPRICE:
          gasPriceEstimate = getRoundedGasPrice(gasFeeEstimates.gasPrice);
          slice.caseReducers.updateGasFees(state, {
            payload: {
              gasPrice: getRoundedGasPrice(gasFeeEstimates.gasPrice),
              type: TRANSACTION_ENVELOPE_TYPES.LEGACY,
              isAutomaticUpdate: true,
            },
          });
          break;
        case GAS_ESTIMATE_TYPES.NONE:
        default:
          break;
      }
      // Record the latest gasPriceEstimate for future comparisons
      state.gas.gasPriceEstimate = addHexPrefix(gasPriceEstimate);
    },
    /**
     * sets the amount mode to the provided value as long as it is one of the
     * supported modes (MAX|INPUT)
     *
     * @param state
     * @param action
     */
    updateAmountMode: (state, action) => {
      if (Object.values(AMOUNT_MODES).includes(action.payload)) {
        state.amount.mode = action.payload;
      }
    },
    updateAsset: (state, action) => {
      state.asset.type = action.payload.type;
      state.asset.balance = action.payload.balance;
      state.asset.error = action.payload.error;
      if (
        state.asset.type === ASSET_TYPES.TOKEN ||
        state.asset.type === ASSET_TYPES.COLLECTIBLE
      ) {
        state.asset.details = action.payload.details;
      } else {
        // clear the details object when sending native currency
        state.asset.details = null;
        if (state.recipient.error === CONTRACT_ADDRESS_ERROR) {
          // Errors related to sending tokens to their own contract address
          // are no longer valid when sending native currency.
          state.recipient.error = null;
        }

        if (state.recipient.warning === KNOWN_RECIPIENT_ADDRESS_WARNING) {
          // Warning related to sending tokens to a known contract address
          // are no longer valid when sending native currency.
          state.recipient.warning = null;
        }
      }
      // if amount mode is MAX update amount to max of new asset, otherwise set
      // to zero. This will revalidate the send amount field.
      if (state.amount.mode === AMOUNT_MODES.MAX) {
        slice.caseReducers.updateAmountToMax(state);
      } else {
        slice.caseReducers.updateSendAmount(state, { payload: '0x0' });
      }
      // validate send state
      slice.caseReducers.validateSendState(state);
    },
    updateRecipient: (state, action) => {
      state.recipient.error = null;
      state.recipient.userInput = '';
      state.recipient.address = action.payload.address ?? '';
      state.recipient.nickname = action.payload.nickname ?? '';

      if (state.recipient.address === '') {
        // If address is null we are clearing the recipient and must return
        // to the ADD_RECIPIENT stage.
        state.stage = SEND_STAGES.ADD_RECIPIENT;
      } else {
        // if and address is provided and an id exists on the draft transaction,
        // we progress to the EDIT stage, otherwise we progress to the DRAFT
        // stage. We also reset the search mode for recipient search.
        state.stage =
          state.draftTransaction.id === null
            ? SEND_STAGES.DRAFT
            : SEND_STAGES.EDIT;
        state.recipient.mode = RECIPIENT_SEARCH_MODES.CONTACT_LIST;
      }

      // validate send state
      slice.caseReducers.validateSendState(state);
    },
    updateDraftTransaction: (state) => {
      // We keep a copy of txParams in state that could be submitted to the
      // network if the form state is valid.
      if (state.status === SEND_STATUSES.VALID) {
        // We don't/shouldn't modify the from address when editing an
        // existing transaction.
        if (state.stage !== SEND_STAGES.EDIT) {
          state.draftTransaction.txParams.from = state.account.address;
        }

        // gasLimit always needs to be set regardless of the asset being sent
        // or the type of transaction.
        state.draftTransaction.txParams.gas = state.gas.gasLimit;
        switch (state.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.
            state.draftTransaction.txParams.to = state.asset.details.address;
            state.draftTransaction.txParams.value = '0x0';
            state.draftTransaction.txParams.data = generateERC20TransferData({
              toAddress: state.recipient.address,
              amount: state.amount.value,
              sendToken: state.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.
            state.draftTransaction.txParams.to = state.asset.details.address;
            state.draftTransaction.txParams.value = '0x0';
            state.draftTransaction.txParams.data = generateERC721TransferData({
              toAddress: state.recipient.address,
              fromAddress: state.account.address,
              tokenId: state.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.
            state.draftTransaction.txParams.to = state.recipient.address;
            state.draftTransaction.txParams.value = state.amount.value;
            state.draftTransaction.txParams.data =
              state.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. We must delete the opposite fields to avoid
        // stale data in txParams.
        if (state.eip1559support) {
          state.draftTransaction.txParams.type =
            TRANSACTION_ENVELOPE_TYPES.FEE_MARKET;

          state.draftTransaction.txParams.maxFeePerGas = state.gas.maxFeePerGas;
          state.draftTransaction.txParams.maxPriorityFeePerGas =
            state.gas.maxPriorityFeePerGas;

          if (
            !state.draftTransaction.txParams.maxFeePerGas ||
            state.draftTransaction.txParams.maxFeePerGas === '0x0'
          ) {
            state.draftTransaction.txParams.maxFeePerGas = state.gas.gasPrice;
          }

          if (
            !state.draftTransaction.txParams.maxPriorityFeePerGas ||
            state.draftTransaction.txParams.maxPriorityFeePerGas === '0x0'
          ) {
            state.draftTransaction.txParams.maxPriorityFeePerGas =
              state.draftTransaction.txParams.maxFeePerGas;
          }

          delete state.draftTransaction.txParams.gasPrice;
        } else {
          delete state.draftTransaction.txParams.maxFeePerGas;
          delete state.draftTransaction.txParams.maxPriorityFeePerGas;

          state.draftTransaction.txParams.gasPrice = state.gas.gasPrice;
          state.draftTransaction.txParams.type =
            TRANSACTION_ENVELOPE_TYPES.LEGACY;
        }
      }
    },
    useDefaultGas: (state) => {
      // Show the default gas price/limit fields in the send page
      state.gas.isCustomGasSet = false;
    },
    useCustomGas: (state) => {
      // Show the gas fees set in the custom gas modal (state.gas.customData)
      state.gas.isCustomGasSet = true;
    },
    updateRecipientUserInput: (state, action) => {
      // Update the value in state to match what the user is typing into the
      // input field
      state.recipient.userInput = action.payload;
    },
    validateRecipientUserInput: (state, action) => {
      const { asset, recipient } = state;

      if (
        recipient.mode === RECIPIENT_SEARCH_MODES.MY_ACCOUNTS ||
        recipient.userInput === '' ||
        recipient.userInput === null
      ) {
        recipient.error = null;
        recipient.warning = null;
      } else {
        const isSendingToken =
          asset.type === ASSET_TYPES.TOKEN ||
          asset.type === ASSET_TYPES.COLLECTIBLE;
        const { chainId, tokens, tokenAddressList } = action.payload;
        if (
          isBurnAddress(recipient.userInput) ||
          (!isValidHexAddress(recipient.userInput, {
            mixedCaseUseChecksum: true,
          }) &&
            !isValidDomainName(recipient.userInput))
        ) {
          recipient.error = isDefaultMetaMaskChain(chainId)
            ? INVALID_RECIPIENT_ADDRESS_ERROR
            : INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR;
        } else if (
          isSendingToken &&
          isOriginContractAddress(recipient.userInput, asset.details.address)
        ) {
          recipient.error = CONTRACT_ADDRESS_ERROR;
        } else {
          recipient.error = null;
        }
        if (
          isSendingToken &&
          isValidHexAddress(recipient.userInput) &&
          (tokenAddressList.find((address) =>
            isEqualCaseInsensitive(address, recipient.userInput),
          ) ||
            checkExistingAddresses(recipient.userInput, tokens))
        ) {
          recipient.warning = KNOWN_RECIPIENT_ADDRESS_WARNING;
        } else {
          recipient.warning = null;
        }
      }
    },
    updateRecipientSearchMode: (state, action) => {
      state.recipient.userInput = '';
      state.recipient.mode = action.payload;
    },
    resetSendState: () => initialState,
    validateAmountField: (state) => {
      switch (true) {
        // set error to INSUFFICIENT_FUNDS_ERROR if the account balance is lower
        // than the total price of the transaction inclusive of gas fees.
        case state.asset.type === ASSET_TYPES.NATIVE &&
          !isBalanceSufficient({
            amount: state.amount.value,
            balance: state.asset.balance,
            gasTotal: state.gas.gasTotal ?? '0x0',
          }):
          state.amount.error = INSUFFICIENT_FUNDS_ERROR;
          break;
        // set error to INSUFFICIENT_FUNDS_ERROR if the token balance is lower
        // than the amount of token the user is attempting to send.
        case state.asset.type === ASSET_TYPES.TOKEN &&
          !isTokenBalanceSufficient({
            tokenBalance: state.asset.balance ?? '0x0',
            amount: state.amount.value,
            decimals: state.asset.details.decimals,
          }):
          state.amount.error = INSUFFICIENT_TOKENS_ERROR;
          break;
        // if the amount is negative, set error to NEGATIVE_ETH_ERROR
        // TODO: change this to NEGATIVE_ERROR and remove the currency bias.
        case conversionGreaterThan(
          { value: 0, fromNumericBase: 'dec' },
          { value: state.amount.value, fromNumericBase: 'hex' },
        ):
          state.amount.error = NEGATIVE_ETH_ERROR;
          break;
        // If none of the above are true, set error to null
        default:
          state.amount.error = null;
      }
    },
    validateGasField: (state) => {
      // Checks if the user has enough funds to cover the cost of gas, always
      // uses the native currency and does not take into account the amount
      // being sent. If the user has enough to cover cost of gas but not gas
      // + amount then the error will be displayed on the amount field.
      const insufficientFunds = !isBalanceSufficient({
        amount:
          state.asset.type === ASSET_TYPES.NATIVE ? state.amount.value : '0x0',
        balance: state.account.balance,
        gasTotal: state.gas.gasTotal ?? '0x0',
      });

      state.gas.error = insufficientFunds ? INSUFFICIENT_FUNDS_ERROR : null;
    },
    validateSendState: (state) => {
      switch (true) {
        // 1 + 2. State is invalid when either gas or amount or asset fields have errors
        // 3. State is invalid if asset type is a token and the token details
        //  are unknown.
        // 4. State is invalid if no recipient has been added
        // 5. State is invalid if the send state is uninitialized
        // 6. State is invalid if gas estimates are loading
        // 7. State is invalid if gasLimit is less than the minimumGasLimit
        // 8. State is invalid if the selected asset is a ERC721
        case Boolean(state.amount.error):
        case Boolean(state.gas.error):
        case Boolean(state.asset.error):
        case state.asset.type === ASSET_TYPES.TOKEN &&
          state.asset.details === null:
        case state.stage === SEND_STAGES.ADD_RECIPIENT:
        case state.stage === SEND_STAGES.INACTIVE:
        case state.gas.isGasEstimateLoading:
        case new BigNumber(state.gas.gasLimit, 16).lessThan(
          new BigNumber(state.gas.minimumGasLimit),
        ):
          state.status = SEND_STATUSES.INVALID;
          break;
        default:
          state.status = SEND_STATUSES.VALID;
          // Recompute the draftTransaction object
          slice.caseReducers.updateDraftTransaction(state);
      }
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(QR_CODE_DETECTED, (state, action) => {
        // When data is received from the QR Code Scanner we set the recipient
        // as long as a valid address can be pulled from the data. If an
        // address is pulled but it is invalid, we display an error.
        const qrCodeData = action.value;
        if (qrCodeData) {
          if (qrCodeData.type === 'address') {
            const scannedAddress = qrCodeData.values.address.toLowerCase();
            if (
              isValidHexAddress(scannedAddress, { allowNonPrefixed: false })
            ) {
              if (state.recipient.address !== scannedAddress) {
                slice.caseReducers.updateRecipient(state, {
                  payload: { address: scannedAddress },
                });
              }
            } else {
              state.recipient.error = INVALID_RECIPIENT_ADDRESS_ERROR;
            }
          }
        }
      })
      .addCase(SELECTED_ACCOUNT_CHANGED, (state, action) => {
        // If we are on the edit flow the account we are keyed into will be the
        // original 'from' account, which may differ from the selected account
        if (state.stage !== SEND_STAGES.EDIT) {
          // This event occurs when the user selects a new account from the
          // account menu, or the currently active account's balance updates.
          state.account.balance = action.payload.account.balance;
          state.account.address = action.payload.account.address;
          // We need to update the asset balance if the asset is the native
          // network asset. Once we update the balance we recompute error state.
          if (state.asset.type === ASSET_TYPES.NATIVE) {
            state.asset.balance = action.payload.account.balance;
          }
          slice.caseReducers.validateAmountField(state);
          slice.caseReducers.validateGasField(state);
          slice.caseReducers.validateSendState(state);
        }
      })
      .addCase(ACCOUNT_CHANGED, (state, action) => {
        // If we are on the edit flow then we need to watch for changes to the
        // current account.address in state and keep balance updated
        // appropriately
        if (
          state.stage === SEND_STAGES.EDIT &&
          action.payload.account.address === state.account.address
        ) {
          // This event occurs when the user's account details update due to
          // background state changes. If the account that is being updated is
          // the current from account on the edit flow we need to update
          // the balance for the account and revalidate the send state.
          state.account.balance = action.payload.account.balance;
          // We need to update the asset balance if the asset is the native
          // network asset. Once we update the balance we recompute error state.
          if (state.asset.type === ASSET_TYPES.NATIVE) {
            state.asset.balance = action.payload.account.balance;
          }
          slice.caseReducers.validateAmountField(state);
          slice.caseReducers.validateGasField(state);
          slice.caseReducers.validateSendState(state);
        }
      })
      .addCase(ADDRESS_BOOK_UPDATED, (state, action) => {
        // When the address book updates from background state changes we need
        // to check to see if an entry exists for the current address or if the
        // entry changed.
        const { addressBook } = action.payload;
        if (addressBook[state.recipient.address]?.name) {
          state.recipient.nickname = addressBook[state.recipient.address].name;
        }
      })
      .addCase(initializeSendState.pending, (state) => {
        // when we begin initializing state, which can happen when switching
        // chains even after loading the send flow, we set
        // gas.isGasEstimateLoading as initialization will trigger a fetch
        // for gasPrice estimates.
        state.gas.isGasEstimateLoading = true;
      })
      .addCase(initializeSendState.fulfilled, (state, action) => {
        // writes the computed initialized state values into the slice and then
        // calculates slice validity using the caseReducers.
        state.eip1559support = action.payload.eip1559support;
        state.account.address = action.payload.address;
        state.account.balance = action.payload.nativeBalance;
        state.asset.balance = action.payload.assetBalance;
        state.gas.gasLimit = action.payload.gasLimit;
        slice.caseReducers.updateGasFeeEstimates(state, {
          payload: {
            gasFeeEstimates: action.payload.gasFeeEstimates,
            gasEstimateType: action.payload.gasEstimateType,
          },
        });
        state.gas.gasTotal = action.payload.gasTotal;
        state.gas.gasEstimatePollToken = action.payload.gasEstimatePollToken;
        if (action.payload.gasEstimatePollToken) {
          state.gas.isGasEstimateLoading = false;
        }
        if (state.stage !== SEND_STAGES.INACTIVE) {
          slice.caseReducers.validateRecipientUserInput(state, {
            payload: {
              chainId: action.payload.chainId,
              tokens: action.payload.tokens,
              useTokenDetection: action.payload.useTokenDetection,
              tokenAddressList: action.payload.tokenAddressList,
            },
          });
        }
        state.stage =
          state.stage === SEND_STAGES.INACTIVE
            ? SEND_STAGES.ADD_RECIPIENT
            : state.stage;
        slice.caseReducers.validateAmountField(state);
        slice.caseReducers.validateGasField(state);
        slice.caseReducers.validateSendState(state);
      })
      .addCase(computeEstimatedGasLimit.pending, (state) => {
        // When we begin to fetch gasLimit we should indicate we are loading
        // a gas estimate.
        state.gas.isGasEstimateLoading = true;
      })
      .addCase(computeEstimatedGasLimit.fulfilled, (state, action) => {
        // When we receive a new gasLimit from the computeEstimatedGasLimit
        // thunk we need to update our gasLimit in the slice. We call into the
        // caseReducer updateGasLimit to tap into the appropriate follow up
        // checks and gasTotal calculation. First set isGasEstimateLoading to
        // false.
        state.gas.isGasEstimateLoading = false;
        if (action.payload?.gasLimit) {
          slice.caseReducers.updateGasLimit(state, {
            payload: action.payload.gasLimit,
          });
        }
      })
      .addCase(computeEstimatedGasLimit.rejected, (state) => {
        // If gas estimation fails, we should set the loading state to false,
        // because it is no longer loading
        state.gas.isGasEstimateLoading = false;
      })
      .addCase(GAS_FEE_ESTIMATES_UPDATED, (state, action) => {
        // When the gasFeeController updates its gas fee estimates we need to
        // update and validate state based on those new values
        slice.caseReducers.updateGasFeeEstimates(state, {
          payload: action.payload,
        });
      });
  },
});

const { actions, reducer } = slice;

export default reducer;

const {
  useDefaultGas,
  useCustomGas,
  updateGasLimit,
  validateRecipientUserInput,
  updateRecipientSearchMode,
} = actions;

export { useDefaultGas, useCustomGas, updateGasLimit };

// Action Creators

/**
 * This method is a temporary placeholder to support the old UI in both the
 * gas modal and the send flow. Soon we won't need to modify gasPrice from the
 * send flow based on user input, it'll just be a shallow copy of the current
 * estimate. This method is necessary because the internal structure of this
 * slice has been changed such that it is agnostic to transaction envelope
 * type, and this method calls into the new structure in the appropriate way.
 *
 * @deprecated - don't extend the usage of this temporary method
 * @param {string} gasPrice - new gas price in hex wei
 */
export function updateGasPrice(gasPrice) {
  return (dispatch) => {
    dispatch(
      actions.updateGasFees({
        gasPrice,
        transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY,
      }),
    );
  };
}

export function resetSendState() {
  return async (dispatch, getState) => {
    const state = getState();
    dispatch(actions.resetSendState());

    if (state[name].gas.gasEstimatePollToken) {
      await disconnectGasFeeEstimatePoller(
        state[name].gas.gasEstimatePollToken,
      );
      removePollingTokenFromAppState(state[name].gas.gasEstimatePollToken);
    }
  };
}
/**
 * Updates the amount the user intends to send and performs side effects.
 * 1. If the current mode is MAX change to INPUT
 * 2. If sending a token, recompute the gasLimit estimate
 *
 * @param {string} amount - hex string representing value
 */
export function updateSendAmount(amount) {
  return async (dispatch, getState) => {
    await dispatch(actions.updateSendAmount(amount));
    const state = getState();
    if (state.send.amount.mode === AMOUNT_MODES.MAX) {
      await dispatch(actions.updateAmountMode(AMOUNT_MODES.INPUT));
    }
    await dispatch(computeEstimatedGasLimit());
  };
}

/**
 * Defines the shape for the details input parameter for updateSendAsset
 *
 * @typedef {Object} TokenDetails
 * @property {string} address - The contract address for the ERC20 token.
 * @property {string} decimals - The number of token decimals.
 * @property {string} symbol - The asset symbol to display.
 */

/**
 * updates the asset to send to one of NATIVE or TOKEN and ensures that the
 * asset balance is set. If sending a TOKEN also updates the asset details
 * object with the appropriate ERC20 details including address, symbol and
 * decimals.
 *
 * @param {Object} payload - action payload
 * @param {string} payload.type - type of asset to send
 * @param {TokenDetails} [payload.details] - ERC20 details if sending TOKEN asset
 */
export function updateSendAsset({ type, details }) {
  return async (dispatch, getState) => {
    const state = getState();
    let { balance, error } = state.send.asset;
    const userAddress = state.send.account.address ?? getSelectedAddress(state);
    if (type === ASSET_TYPES.TOKEN) {
      if (details) {
        if (details.standard === undefined) {
          await dispatch(showLoadingIndication());
          const { standard } = await getTokenStandardAndDetails(
            details.address,
            userAddress,
          );
          if (
            process.env.COLLECTIBLES_V1 &&
            (standard === ERC721 || standard === ERC1155)
          ) {
            await dispatch(hideLoadingIndication());
            dispatch(
              showModal({
                name: 'CONVERT_TOKEN_TO_NFT',
                tokenAddress: details.address,
              }),
            );
            error = INVALID_ASSET_TYPE;
            throw new Error(error);
          }
          details.standard = standard;
        }

        // if changing to a token, get the balance from the network. The asset
        // overview page and asset list on the wallet overview page contain
        // send buttons that call this method before initialization occurs.
        // When this happens we don't yet have an account.address so default to
        // the currently active account. In addition its possible for the balance
        // check to take a decent amount of time, so we display a loading
        // indication so that that immediate feedback is displayed to the user.
        if (details.standard === ERC20) {
          error = null;
          balance = await getERC20Balance(details, userAddress);
        }
        await dispatch(hideLoadingIndication());
      }
    } else if (type === ASSET_TYPES.COLLECTIBLE) {
      let isCurrentOwner = true;
      try {
        isCurrentOwner = await isCollectibleOwner(
          getSelectedAddress(state),
          details.address,
          details.tokenId,
        );
      } catch (err) {
        if (err.message.includes('Unable to verify ownership.')) {
          // this would indicate that either our attempts to verify ownership failed because of network issues,
          // or, somehow a token has been added to collectibles state with an incorrect chainId.
        } else {
          // Any other error is unexpected and should be surfaced.
          dispatch(displayWarning(err.message));
        }
      }

      if (details.standard === undefined) {
        const { standard } = await getTokenStandardAndDetails(
          details.address,
          userAddress,
        );
        details.standard = standard;
      }

      if (details.standard === ERC1155) {
        throw new Error('Sends of ERC1155 tokens are not currently supported');
      }

      if (isCurrentOwner) {
        error = null;
        balance = '0x1';
      } else {
        throw new Error(
          'Send slice initialized as collectible send with a collectible not currently owned by the select account',
        );
      }
    } else {
      error = null;
      // if changing to native currency, get it from the account key in send
      // state which is kept in sync when accounts change.
      balance = state.send.account.balance;
    }
    // update the asset in state which will re-run amount and gas validation
    await dispatch(actions.updateAsset({ type, details, balance, error }));
    await dispatch(computeEstimatedGasLimit());
  };
}

/**
 * This method is for usage when validating user input so that validation
 * is only run after a delay in typing of 300ms. Usage at callsites requires
 * passing in both the dispatch method and the payload to dispatch, which makes
 * it only applicable for use within action creators.
 */
const debouncedValidateRecipientUserInput = debounce((dispatch, payload) => {
  dispatch(validateRecipientUserInput(payload));
}, 300);

/**
 * This method is called to update the user's input into the ENS input field.
 * Once the field is updated, the field will be validated using a debounced
 * version of the validateRecipientUserInput action. This way validation only
 * occurs once the user has stopped typing.
 *
 * @param {string} userInput - the value that the user is typing into the field
 */
export function updateRecipientUserInput(userInput) {
  return async (dispatch, getState) => {
    await dispatch(actions.updateRecipientUserInput(userInput));
    const state = getState();
    const chainId = getCurrentChainId(state);
    const tokens = getTokens(state);
    const useTokenDetection = getUseTokenDetection(state);
    const tokenAddressList = Object.keys(getTokenList(state));
    debouncedValidateRecipientUserInput(dispatch, {
      chainId,
      tokens,
      useTokenDetection,
      tokenAddressList,
    });
  };
}

export function useContactListForRecipientSearch() {
  return (dispatch) => {
    dispatch(updateRecipientSearchMode(RECIPIENT_SEARCH_MODES.CONTACT_LIST));
  };
}

export function useMyAccountsForRecipientSearch() {
  return (dispatch) => {
    dispatch(updateRecipientSearchMode(RECIPIENT_SEARCH_MODES.MY_ACCOUNTS));
  };
}

/**
 * Updates the recipient in state based on the input provided, and then will
 * recompute gas limit when sending a TOKEN asset type. Changing the recipient
 * address results in hex data changing because the recipient address is
 * encoded in the data instead of being in the 'to' field. The to field in a
 * token send will always be the token contract address.
 * If no nickname is provided, the address book state will be checked to see if
 * a nickname for the passed address has already been saved. This ensures the
 * (temporary) send state recipient nickname is consistent with the address book
 * nickname which has already been persisted to state.
 *
 * @param {Object} recipient - Recipient information
 * @param {string} recipient.address - hex address to send the transaction to
 * @param {string} [recipient.nickname] - Alias for the address to display
 *  to the user
 */
export function updateRecipient({ address, nickname }) {
  return async (dispatch, getState) => {
    const state = getState();
    const nicknameFromAddressBookEntryOrAccountName =
      getAddressBookEntryOrAccountName(state, address) ?? '';
    await dispatch(
      actions.updateRecipient({
        address,
        nickname: nickname || nicknameFromAddressBookEntryOrAccountName,
      }),
    );
    await dispatch(computeEstimatedGasLimit());
  };
}

/**
 * Clears out the recipient user input, ENS resolution and recipient validation.
 */
export function resetRecipientInput() {
  return async (dispatch) => {
    await dispatch(updateRecipientUserInput(''));
    await dispatch(updateRecipient({ address: '', nickname: '' }));
    await dispatch(resetEnsResolution());
    await dispatch(validateRecipientUserInput());
  };
}

/**
 * When a user has enabled hex data field in advanced settings they will be
 * able to supply hex data on a transaction. This method updates the user
 * supplied data. Note, when sending native assets this will result in
 * recomputing estimated gasLimit. When sending a ERC20 asset this is not done
 * because the data sent in the transaction will be determined by the asset,
 * recipient and value, NOT what the user has supplied.
 *
 * @param {string} hexData - hex encoded string representing transaction data.
 */
export function updateSendHexData(hexData) {
  return async (dispatch, getState) => {
    await dispatch(actions.updateUserInputHexData(hexData));
    const state = getState();
    if (state.send.asset.type === ASSET_TYPES.NATIVE) {
      await dispatch(computeEstimatedGasLimit());
    }
  };
}

/**
 * Toggles the amount.mode between INPUT and MAX modes.
 * As a result, the amount.value will change to either '0x0' when moving from
 * MAX to INPUT, or to the maximum allowable amount based on current asset when
 * moving from INPUT to MAX.
 */
export function toggleSendMaxMode() {
  return async (dispatch, getState) => {
    const state = getState();
    if (state.send.amount.mode === AMOUNT_MODES.MAX) {
      await dispatch(actions.updateAmountMode(AMOUNT_MODES.INPUT));
      await dispatch(actions.updateSendAmount('0x0'));
    } else {
      await dispatch(actions.updateAmountMode(AMOUNT_MODES.MAX));
      await dispatch(actions.updateAmountToMax());
    }
    await dispatch(computeEstimatedGasLimit());
  };
}

/**
 * Signs a transaction or updates a transaction in state if editing.
 * This method is called when a user clicks the next button in the footer of
 * the send page, signaling that a transaction should be executed. This method
 * will create the transaction in state (by way of the various global provider
 * constructs) which will eventually (and fairly quickly from user perspective)
 * result in a confirmation window being displayed for the transaction.
 */
export function signTransaction() {
  return async (dispatch, getState) => {
    const state = getState();
    const {
      asset,
      stage,
      draftTransaction: { id, txParams },
      recipient: { address },
      amount: { value },
      account: { address: selectedAddress },
      eip1559support,
    } = state[name];
    if (stage === SEND_STAGES.EDIT) {
      // When dealing with the edit flow there is already a transaction in
      // state that we must update, this branch is responsible for that logic.
      // We first must grab the previous transaction object from state and then
      // merge in the modified txParams. Once the transaction has been modified
      // we can send that to the background to update the transaction in state.
      const unapprovedTxs = getUnapprovedTxs(state);
      const unapprovedTx = unapprovedTxs[id];
      // We only update the tx params that can be changed via the edit flow UX
      const eip1559OnlyTxParamsToUpdate = {
        data: txParams.data,
        from: txParams.from,
        to: txParams.to,
        value: txParams.value,
        gas: unapprovedTx.userEditedGasLimit
          ? unapprovedTx.txParams.gas
          : txParams.gas,
      };
      unapprovedTx.originalGasEstimate = eip1559OnlyTxParamsToUpdate.gas;
      const editingTx = {
        ...unapprovedTx,
        txParams: Object.assign(
          unapprovedTx.txParams,
          eip1559support ? eip1559OnlyTxParamsToUpdate : txParams,
        ),
      };
      dispatch(updateTransaction(editingTx));
    } else if (asset.type === ASSET_TYPES.TOKEN) {
      // When sending a token transaction we have to the token.transfer method
      // on the token contract to construct the transaction. This results in
      // the proper transaction data and properties being set and a new
      // transaction being added to background state. Once the new transaction
      // is added to state a subsequent confirmation will be queued.
      try {
        const token = global.eth.contract(abi).at(asset.details.address);
        token.transfer(address, value, {
          ...txParams,
          to: undefined,
          data: undefined,
        });
        dispatch(showConfTxPage());
        dispatch(hideLoadingIndication());
      } catch (error) {
        dispatch(hideLoadingIndication());
        dispatch(displayWarning(error.message));
      }
    } else if (asset.type === ASSET_TYPES.COLLECTIBLE) {
      // When sending a collectible transaction we have to use the collectible.transferFrom method
      // on the collectible contract to construct the transaction. This results in
      // the proper transaction data and properties being set and a new
      // transaction being added to background state. Once the new transaction
      // is added to state a subsequent confirmation will be queued.
      try {
        const collectibleContract = global.eth
          .contract(abiERC721)
          .at(asset.details.address);

        collectibleContract.transferFrom(
          selectedAddress,
          address,
          asset.details.tokenId,
          {
            ...txParams,
            to: undefined,
            data: undefined,
          },
        );

        dispatch(showConfTxPage());
        dispatch(hideLoadingIndication());
      } catch (error) {
        dispatch(hideLoadingIndication());
        dispatch(displayWarning(error.message));
      }
    } else {
      // When sending a native asset we use the ethQuery.sendTransaction method
      // which will result in the transaction being added to background state
      // and a subsequent confirmation will be queued.
      global.ethQuery.sendTransaction(txParams, (err) => {
        if (err) {
          dispatch(displayWarning(err.message));
        }
      });
      dispatch(showConfTxPage());
    }
  };
}

export function editTransaction(
  assetType,
  transactionId,
  tokenData,
  assetDetails,
) {
  return async (dispatch, getState) => {
    const state = getState();
    const unapprovedTransactions = getUnapprovedTxs(state);
    const transaction = unapprovedTransactions[transactionId];
    const { txParams } = transaction;
    if (assetType === ASSET_TYPES.NATIVE) {
      const {
        data,
        from,
        gas: gasLimit,
        gasPrice,
        to: address,
        value: amount,
      } = txParams;
      const nickname = getAddressBookEntry(state, address)?.name ?? '';
      await dispatch(
        actions.editTransaction({
          data,
          id: transactionId,
          gasLimit,
          gasPrice,
          from,
          amount,
          address,
          nickname,
        }),
      );
    } else if (!tokenData || !assetDetails) {
      throw new Error(
        `send/editTransaction dispatched with assetType 'TOKEN' but missing assetData or assetDetails parameter`,
      );
    } else if (assetType === ASSET_TYPES.TOKEN) {
      const {
        data,
        from,
        to: tokenAddress,
        gas: gasLimit,
        gasPrice,
      } = txParams;
      const tokenAmountInDec = getTokenValueParam(tokenData);
      const address = getTokenAddressParam(tokenData);
      const nickname = getAddressBookEntry(state, address)?.name ?? '';

      const tokenAmountInHex = addHexPrefix(
        conversionUtil(tokenAmountInDec, {
          fromNumericBase: 'dec',
          toNumericBase: 'hex',
        }),
      );

      await dispatch(
        updateSendAsset({
          type: ASSET_TYPES.TOKEN,
          details: { ...assetDetails, address: tokenAddress },
        }),
      );

      await dispatch(
        actions.editTransaction({
          data,
          id: transactionId,
          gasLimit,
          gasPrice,
          from,
          amount: tokenAmountInHex,
          address,
          nickname,
        }),
      );
    } else if (assetType === ASSET_TYPES.COLLECTIBLE) {
      const {
        data,
        from,
        to: tokenAddress,
        gas: gasLimit,
        gasPrice,
      } = txParams;
      const address = getTokenAddressParam(tokenData);
      const nickname = getAddressBookEntry(state, address)?.name ?? '';

      await dispatch(
        updateSendAsset({
          type: ASSET_TYPES.COLLECTIBLE,
          details: { ...assetDetails, address: tokenAddress },
        }),
      );

      await dispatch(
        actions.editTransaction({
          data,
          id: transactionId,
          gasLimit,
          gasPrice,
          from,
          amount: '0x1',
          address,
          nickname,
        }),
      );
    }
  };
}

// Selectors

// Gas selectors
export function getGasLimit(state) {
  return state[name].gas.gasLimit;
}

export function getGasPrice(state) {
  return state[name].gas.gasPrice;
}

export function getGasTotal(state) {
  return state[name].gas.gasTotal;
}

export function gasFeeIsInError(state) {
  return Boolean(state[name].gas.error);
}

export function getMinimumGasLimitForSend(state) {
  return state[name].gas.minimumGasLimit;
}

export function getGasInputMode(state) {
  const isMainnet = getIsMainnet(state);
  const gasEstimateType = getGasEstimateType(state);
  const showAdvancedGasFields = getAdvancedInlineGasShown(state);
  if (state[name].gas.isCustomGasSet) {
    return GAS_INPUT_MODES.CUSTOM;
  }
  if ((!isMainnet && !process.env.IN_TEST) || showAdvancedGasFields) {
    return GAS_INPUT_MODES.INLINE;
  }

  // We get eth_gasPrice estimation if the legacy API fails but we need to
  // instruct the UI to render the INLINE inputs in this case, only on
  // mainnet or IN_TEST.
  if (
    (isMainnet || process.env.IN_TEST) &&
    gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE
  ) {
    return GAS_INPUT_MODES.INLINE;
  }
  return GAS_INPUT_MODES.BASIC;
}

// Asset Selectors
export function getSendAsset(state) {
  return state[name].asset;
}

export function getSendAssetAddress(state) {
  return getSendAsset(state)?.details?.address;
}

export function getIsAssetSendable(state) {
  if (state[name].asset.type === ASSET_TYPES.NATIVE) {
    return true;
  }
  return state[name].asset.details.isERC721 === false;
}

export function getAssetError(state) {
  return state[name].asset.error;
}

// Amount Selectors
export function getSendAmount(state) {
  return state[name].amount.value;
}

export function getIsBalanceInsufficient(state) {
  return state[name].gas.error === INSUFFICIENT_FUNDS_ERROR;
}
export function getSendMaxModeState(state) {
  return state[name].amount.mode === AMOUNT_MODES.MAX;
}

export function getSendHexData(state) {
  return state[name].draftTransaction.userInputHexData;
}

export function getDraftTransactionID(state) {
  return state[name].draftTransaction.id;
}

export function sendAmountIsInError(state) {
  return Boolean(state[name].amount.error);
}

// Recipient Selectors

export function getSendTo(state) {
  return state[name].recipient.address;
}

export function getIsUsingMyAccountForRecipientSearch(state) {
  return state[name].recipient.mode === RECIPIENT_SEARCH_MODES.MY_ACCOUNTS;
}

export function getRecipientUserInput(state) {
  return state[name].recipient.userInput;
}

export function getRecipient(state) {
  return state[name].recipient;
}

// Overall validity and stage selectors

export function getSendErrors(state) {
  return {
    gasFee: state.send.gas.error,
    amount: state.send.amount.error,
  };
}

export function isSendStateInitialized(state) {
  return state[name].stage !== SEND_STAGES.INACTIVE;
}

export function isSendFormInvalid(state) {
  return state[name].status === SEND_STATUSES.INVALID;
}

export function getSendStage(state) {
  return state[name].stage;
}
