diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index f1caf9b7eeee..c8604c96c055 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -1245,6 +1245,7 @@ "send": { "amountMode": "INPUT", "currentTransactionUUID": "1-tx", + "disabledSwapAndSendNetworks": [], "draftTransactions": { "1-tx": { "amount": { diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 88fee4835864..fe39047c7667 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -1947,6 +1947,7 @@ "send": { "amountMode": "INPUT", "currentTransactionUUID": null, + "disabledSwapAndSendNetworks": [], "draftTransactions": {}, "eip1559support": false, "gasEstimateIsLoading": true, diff --git a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.test.tsx b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.test.tsx index 08864f10edf8..208395b2228b 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.test.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.test.tsx @@ -157,6 +157,46 @@ describe('AssetPicker', () => { expect(getByText('?')).toBeInTheDocument(); }); + it('nft: does not truncates if token ID is under length 13', () => { + const asset = { + type: AssetType.NFT, + details: { + address: 'token address', + decimals: 2, + tokenId: 1234567890, + }, + balance: '100', + }; + const mockAssetChange = jest.fn(); + + const { getByText } = render( + + mockAssetChange()} /> + , + ); + expect(getByText('#1234567890')).toBeInTheDocument(); + }); + + it('nft: truncates if token ID is too long', () => { + const asset = { + type: AssetType.NFT, + details: { + address: 'token address', + decimals: 2, + tokenId: 1234567890123456, + }, + balance: '100', + }; + const mockAssetChange = jest.fn(); + + const { getByText } = render( + + mockAssetChange()} /> + , + ); + expect(getByText('#123456...3456')).toBeInTheDocument(); + }); + it('render if disabled', () => { const asset = { type: AssetType.token, diff --git a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx index 5867fc86b870..a3b1f965cb34 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker/asset-picker.tsx @@ -36,6 +36,9 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../../shared/constants/metametrics'; +import { ellipsify } from '../../../../pages/confirmations/send/send.utils'; + +const ELLIPSIFY_LENGTH = 13; // 6 (start) + 4 (end) + 3 (...) export type AssetPickerProps = { asset: Asset; @@ -168,7 +171,10 @@ export function AssetPicker({ variant={TextVariant.bodySm} color={TextColor.textAlternative} > - #{asset.details.tokenId} + # + {String(asset.details.tokenId).length < ELLIPSIFY_LENGTH + ? asset.details.tokenId + : ellipsify(String(asset.details.tokenId), 6, 4)} )} diff --git a/ui/components/multichain/pages/send/components/recipient-content.tsx b/ui/components/multichain/pages/send/components/recipient-content.tsx index 90a73a932c4b..097214d7cd25 100644 --- a/ui/components/multichain/pages/send/components/recipient-content.tsx +++ b/ui/components/multichain/pages/send/components/recipient-content.tsx @@ -11,6 +11,7 @@ import { acknowledgeRecipientWarning, getBestQuote, getCurrentDraftTransaction, + getIsSwapAndSendDisabledForNetwork, getSendAsset, getSwapsBlockedTokens, } from '../../../../../ducks/send'; @@ -46,12 +47,16 @@ export const SendPageRecipientContent = ({ const isBasicFunctionality = useSelector(getUseExternalServices); const isSwapsChain = useSelector(getIsSwapsChain); + const isSwapAndSendDisabledForNetwork = useSelector( + getIsSwapAndSendDisabledForNetwork, + ); const swapsBlockedTokens = useSelector(getSwapsBlockedTokens); const memoizedSwapsBlockedTokens = useMemo(() => { return new Set(swapsBlockedTokens); }, [swapsBlockedTokens]); const isSwapAllowed = isSwapsChain && + !isSwapAndSendDisabledForNetwork && [AssetType.token, AssetType.native].includes(sendAsset.type) && isBasicFunctionality && !memoizedSwapsBlockedTokens.has(sendAsset.details?.address?.toLowerCase()); diff --git a/ui/components/multichain/pages/send/send.js b/ui/components/multichain/pages/send/send.js index c47a22e6ef69..c724a042b1d3 100644 --- a/ui/components/multichain/pages/send/send.js +++ b/ui/components/multichain/pages/send/send.js @@ -40,6 +40,7 @@ import { import { TokenStandard, AssetType, + SmartTransactionStatus, } from '../../../../../shared/constants/transaction'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { INSUFFICIENT_FUNDS_ERROR } from '../../../../pages/confirmations/send/send.constants'; @@ -56,6 +57,7 @@ import { getMostRecentOverviewPage } from '../../../../ducks/history/history'; import { AssetPickerAmount } from '../..'; import useUpdateSwapsState from '../../../../hooks/useUpdateSwapsState'; import { getIsDraftSwapAndSend } from '../../../../ducks/send/helpers'; +import { smartTransactionsListSelector } from '../../../../selectors'; import { SendPageAccountPicker, SendPageRecipientContent, @@ -256,13 +258,20 @@ export const SendPage = () => { const sendErrors = useSelector(getSendErrors); const isInvalidSendForm = useSelector(isSendFormInvalid); + const smartTransactions = useSelector(smartTransactionsListSelector); + + const isSmartTransactionPending = smartTransactions?.find( + ({ status }) => status === SmartTransactionStatus.pending, + ); + const isGasTooLow = sendErrors.gasFee === INSUFFICIENT_FUNDS_ERROR && sendErrors.amount !== INSUFFICIENT_FUNDS_ERROR; const submitDisabled = (isInvalidSendForm && !isGasTooLow) || - requireContractAddressAcknowledgement; + requireContractAddressAcknowledgement || + (isSwapAndSend && isSmartTransactionPending); const isSendFormShown = draftTransactionExists && @@ -281,7 +290,13 @@ export const SendPage = () => { [dispatch], ); - const tooltipTitle = isSwapAndSend ? t('sendSwapSubmissionWarning') : ''; + let tooltipTitle = ''; + + if (isSwapAndSend) { + tooltipTitle = isSmartTransactionPending + ? t('isSigningOrSubmitting') + : t('sendSwapSubmissionWarning'); + } return ( diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index 9c45334a9625..39e5974c8060 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -140,7 +140,10 @@ import { DEFAULT_ROUTE, } from '../../helpers/constants/routes'; import { fetchBlockedTokens } from '../../pages/swaps/swaps.util'; -import { getSwapAndSendQuotes } from './swap-and-send-utils'; +import { + getDisabledSwapAndSendNetworksFromAPI, + getSwapAndSendQuotes, +} from './swap-and-send-utils'; import { estimateGasLimitForSend, generateTransactionParams, @@ -463,6 +466,7 @@ export const draftTransactionInitialState = { * clean up AND during initialization. When a transaction is edited a new UUID * is generated for it and the state of that transaction is copied into a new * entry in the draftTransactions object. + * @property {string[]} disabledSwapAndSendNetworks - list of networks that are disabled for swap and send * @property {{[key: string]: DraftTransaction}} draftTransactions - An object keyed * by UUID with draftTransactions as the values. * @property {boolean} eip1559support - tracks whether the current network @@ -505,6 +509,7 @@ export const draftTransactionInitialState = { export const initialState = { amountMode: AMOUNT_MODES.INPUT, currentTransactionUUID: null, + disabledSwapAndSendNetworks: [], draftTransactions: {}, eip1559support: false, gasEstimateIsLoading: true, @@ -763,11 +768,15 @@ export const initializeSendState = createAsyncThunk( ? (await fetchBlockedTokens(chainId)).map((t) => t.toLowerCase()) : []; + const disabledSwapAndSendNetworks = + await getDisabledSwapAndSendNetworksFromAPI(); + return { account, chainId: getCurrentChainId(state), tokens: getTokens(state), chainHasChanged, + disabledSwapAndSendNetworks, gasFeeEstimates, gasEstimateType, gasLimit, @@ -1980,6 +1989,8 @@ const slice = createSlice({ }); } state.swapsBlockedTokens = action.payload.swapsBlockedTokens; + state.disabledSwapAndSendNetworks = + action.payload.disabledSwapAndSendNetworks; if (state.amountMode === AMOUNT_MODES.MAX) { slice.caseReducers.updateAmountToMax(state); } @@ -3520,6 +3531,14 @@ export function getSwapsBlockedTokens(state) { return state[name].swapsBlockedTokens; } +export const getIsSwapAndSendDisabledForNetwork = createSelector( + (state) => state.metamask.providerConfig, + (state) => state[name]?.disabledSwapAndSendNetworks ?? [], + ({ chainId }, disabledSwapAndSendNetworks) => { + return disabledSwapAndSendNetworks.includes(chainId); + }, +); + export const getSendAnalyticProperties = createSelector( (state) => state.metamask.providerConfig, getCurrentDraftTransaction, diff --git a/ui/ducks/send/send.test.js b/ui/ducks/send/send.test.js index 62d43eba56e6..6eac025e8f0a 100644 --- a/ui/ducks/send/send.test.js +++ b/ui/ducks/send/send.test.js @@ -35,6 +35,7 @@ import { INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, } from '../../../test/jest/mocks'; import { ETH_EOA_METHODS } from '../../../shared/constants/eth-methods'; +import * as Utils from './swap-and-send-utils'; import sendReducer, { initialState, initializeSendState, @@ -81,6 +82,7 @@ import sendReducer, { getSender, getSwapsBlockedTokens, updateSendQuote, + getIsSwapAndSendDisabledForNetwork, } from './send'; import { draftTransactionInitialState, editExistingTransaction } from '.'; @@ -171,6 +173,9 @@ describe('Send Slice', () => { jest .spyOn(Actions, 'getLayer1GasFee') .mockReturnValue({ type: 'GET_LAYER_1_GAS_FEE' }); + jest + .spyOn(Utils, 'getDisabledSwapAndSendNetworksFromAPI') + .mockReturnValue([]); }); describe('Reducers', () => { @@ -4485,6 +4490,36 @@ describe('Send Slice', () => { }), ).toStrictEqual(['target']); }); + + it('has a selector to get if swap+send is disabled for that network', () => { + expect( + getIsSwapAndSendDisabledForNetwork({ + metamask: { + providerConfig: { + chainId: 'disabled network', + }, + }, + send: { + ...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, + disabledSwapAndSendNetworks: ['disabled network'], + }, + }), + ).toStrictEqual(true); + + expect( + getIsSwapAndSendDisabledForNetwork({ + metamask: { + providerConfig: { + chainId: 'enabled network', + }, + }, + send: { + ...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT, + disabledSwapAndSendNetworks: ['disabled network'], + }, + }), + ).toStrictEqual(false); + }); }); }); }); diff --git a/ui/ducks/send/swap-and-send-utils.ts b/ui/ducks/send/swap-and-send-utils.ts index b6bd4e479d9f..9ab6486a7ed1 100644 --- a/ui/ducks/send/swap-and-send-utils.ts +++ b/ui/ducks/send/swap-and-send-utils.ts @@ -1,5 +1,6 @@ import { isNumber } from 'lodash'; import { + ALLOWED_PROD_SWAPS_CHAIN_IDS, SWAPS_API_V2_BASE_URL, SWAPS_CLIENT_ID, SWAPS_DEV_API_V2_BASE_URL, @@ -18,6 +19,10 @@ import { hexToDecimal, } from '../../../shared/modules/conversion.utils'; import { isValidHexAddress } from '../../../shared/modules/hexstring-utils'; +import { + fetchSwapsFeatureFlags, + getNetworkNameByChainId, +} from '../../pages/swaps/swaps.util'; type Address = `0x${string}`; @@ -208,3 +213,28 @@ export async function getSwapAndSendQuotes(request: Request): Promise { return newQuotes; } + +export async function getDisabledSwapAndSendNetworksFromAPI(): Promise< + string[] +> { + try { + const blockedChains: string[] = []; + + const featureFlagResponse = await fetchSwapsFeatureFlags(); + + ALLOWED_PROD_SWAPS_CHAIN_IDS.forEach((chainId) => { + // explicitly look for disabled so that chains aren't turned off accidentally + if ( + featureFlagResponse[getNetworkNameByChainId(chainId)]?.v2?.swapAndSend + ?.enabled === false + ) { + blockedChains.push(chainId); + } + }); + + return blockedChains; + } catch (error) { + // assume no networks are blocked since the quotes will not be fetched on an unavailable network anyways + return []; + } +} diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 7921e5579a0c..26a78f504a20 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -72,6 +72,7 @@ import { // that does not have an explicit export statement. lets see if it breaks the // compiler DraftTransaction, + SEND_STAGES, } from '../ducks/send'; import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account'; import { @@ -1075,9 +1076,13 @@ export function updateAndApproveTx( unknown, AnyAction > { - return (dispatch: MetaMaskReduxDispatch) => { + return (dispatch: MetaMaskReduxDispatch, getState) => { !dontShowLoadingIndicator && dispatch(showLoadingIndication(loadingIndicatorMessage)); + + const getIsSendActive = () => + Boolean(getState().send.stage !== SEND_STAGES.INACTIVE); + return new Promise((resolve, reject) => { const actionId = generateActionId(); callBackgroundMethod( @@ -1085,7 +1090,10 @@ export function updateAndApproveTx( [String(txMeta.id), { txMeta, actionId }, { waitForResult: true }], (err) => { dispatch(updateTransactionParams(txMeta.id, txMeta.txParams)); - dispatch(resetSendState()); + + if (!getIsSendActive()) { + dispatch(resetSendState()); + } if (err) { dispatch(goHome()); @@ -1101,7 +1109,9 @@ export function updateAndApproveTx( .then(() => updateMetamaskStateFromBackground()) .then((newState) => dispatch(updateMetamaskState(newState))) .then(() => { - dispatch(resetSendState()); + if (!getIsSendActive()) { + dispatch(resetSendState()); + } dispatch(completedTx(txMeta.id)); dispatch(hideLoadingIndication()); dispatch(updateCustomNonce(''));