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(''));