Skip to content

Commit

Permalink
Merge pull request #25480 from MetaMask/Version-v12.0.0-swap-send-ff-stx
Browse files Browse the repository at this point in the history
fix (cherry-pick for v12.0.0): swap+send feature flags, stx handling, nft id truncation
  • Loading branch information
danjm authored Jun 24, 2024
2 parents 8c105a0 + 3ddc5fd commit 78fb6ec
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 7 deletions.
1 change: 1 addition & 0 deletions test/data/mock-send-state.json
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,7 @@
"send": {
"amountMode": "INPUT",
"currentTransactionUUID": "1-tx",
"disabledSwapAndSendNetworks": [],
"draftTransactions": {
"1-tx": {
"amount": {
Expand Down
1 change: 1 addition & 0 deletions test/data/mock-state.json
Original file line number Diff line number Diff line change
Expand Up @@ -1947,6 +1947,7 @@
"send": {
"amountMode": "INPUT",
"currentTransactionUUID": null,
"disabledSwapAndSendNetworks": [],
"draftTransactions": {},
"eip1559support": false,
"gasEstimateIsLoading": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Provider store={store()}>
<AssetPicker asset={asset} onAssetChange={() => mockAssetChange()} />
</Provider>,
);
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(
<Provider store={store()}>
<AssetPicker asset={asset} onAssetChange={() => mockAssetChange()} />
</Provider>,
);
expect(getByText('#123456...3456')).toBeInTheDocument();
});

it('render if disabled', () => {
const asset = {
type: AssetType.token,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)}
</Text>
)}
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
acknowledgeRecipientWarning,
getBestQuote,
getCurrentDraftTransaction,
getIsSwapAndSendDisabledForNetwork,
getSendAsset,
getSwapsBlockedTokens,
} from '../../../../../ducks/send';
Expand Down Expand Up @@ -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());
Expand Down
19 changes: 17 additions & 2 deletions ui/components/multichain/pages/send/send.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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 &&
Expand All @@ -281,7 +290,13 @@ export const SendPage = () => {
[dispatch],
);

const tooltipTitle = isSwapAndSend ? t('sendSwapSubmissionWarning') : '';
let tooltipTitle = '';

if (isSwapAndSend) {
tooltipTitle = isSmartTransactionPending
? t('isSigningOrSubmitting')
: t('sendSwapSubmissionWarning');
}

return (
<Page className="multichain-send-page">
Expand Down
21 changes: 20 additions & 1 deletion ui/ducks/send/send.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -505,6 +509,7 @@ export const draftTransactionInitialState = {
export const initialState = {
amountMode: AMOUNT_MODES.INPUT,
currentTransactionUUID: null,
disabledSwapAndSendNetworks: [],
draftTransactions: {},
eip1559support: false,
gasEstimateIsLoading: true,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
Expand Down
35 changes: 35 additions & 0 deletions ui/ducks/send/send.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -81,6 +82,7 @@ import sendReducer, {
getSender,
getSwapsBlockedTokens,
updateSendQuote,
getIsSwapAndSendDisabledForNetwork,
} from './send';
import { draftTransactionInitialState, editExistingTransaction } from '.';

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
});
});
30 changes: 30 additions & 0 deletions ui/ducks/send/swap-and-send-utils.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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}`;

Expand Down Expand Up @@ -208,3 +213,28 @@ export async function getSwapAndSendQuotes(request: Request): Promise<Quote[]> {

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 [];
}
}
16 changes: 13 additions & 3 deletions ui/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -1075,17 +1076,24 @@ 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(
'resolvePendingApproval',
[String(txMeta.id), { txMeta, actionId }, { waitForResult: true }],
(err) => {
dispatch(updateTransactionParams(txMeta.id, txMeta.txParams));
dispatch(resetSendState());

if (!getIsSendActive()) {
dispatch(resetSendState());
}

if (err) {
dispatch(goHome());
Expand All @@ -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(''));
Expand Down

0 comments on commit 78fb6ec

Please sign in to comment.