Skip to content

Commit ae13e11

Browse files
authored
feat: add support for blocklisted chains and tokens for MMPay (#26629)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: ## **Related issues** Fixes: MetaMask/MetaMask-planning#6982 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <img width="397" height="852" alt="Screenshot 2026-02-26 at 6 56 11 PM" src="https://github.com/user-attachments/assets/c53aba8a-938c-454f-acae-634fc933778b" /> ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I've included tests if applicable - [X] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes payment-token availability/selection logic to enforce a remote-configured blocklist, which could unintentionally disable valid tokens or alter default token selection if misconfigured. > > **Overview** > **MetaMask Pay token lists now respect a remote-configured blocklist.** A new `blockedTokens` config is added to `confirmations_pay_tokens` flags (with per-transaction-type overrides) and consumed via `useTransactionPayBlockedTokens` in both `PayWithModal` and `useTransactionPayAvailableTokens`. > > `getAvailableTokens` now marks blocklisted chains/tokens as `disabled` with a new `pay_with_modal.not_supported` message, and sorts disabled tokens to the bottom; tests were expanded/updated to cover blocklist resolution and behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2eba3c5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 648f02f commit ae13e11

10 files changed

Lines changed: 482 additions & 17 deletions

File tree

app/components/Views/confirmations/components/modals/pay-with-modal/pay-with-modal.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from '../../../types/token';
2020
import { useTransactionPayRequiredTokens } from '../../../hooks/pay/useTransactionPayData';
2121
import { getAvailableTokens } from '../../../utils/transaction-pay';
22+
import { useTransactionPayBlockedTokens } from '../../../hooks/pay/useTransactionPayBlockedTokens';
2223
import { useTransactionMetadataRequest } from '../../../hooks/transactions/useTransactionMetadataRequest';
2324
import { TransactionType } from '@metamask/transaction-controller';
2425
import {
@@ -48,6 +49,7 @@ export function PayWithModal() {
4849
usePerpsPaymentToken();
4950
const perpsBalanceTokenFilter = usePerpsBalanceTokenFilter();
5051
const withdrawTokenFilter = useWithdrawTokenFilter();
52+
const blockedTokens = useTransactionPayBlockedTokens();
5153

5254
const close = useCallback((onClosed?: () => void) => {
5355
// Called after the bottom sheet's closing animation completes.
@@ -153,6 +155,7 @@ export function PayWithModal() {
153155
payToken,
154156
requiredTokens,
155157
tokens,
158+
blockedTokens,
156159
});
157160

158161
let filteredTokens: TokenListItem[] = availableTokens;
@@ -172,6 +175,7 @@ export function PayWithModal() {
172175
return wrapHighlightedItemCallbacks(filteredTokens);
173176
},
174177
[
178+
blockedTokens,
175179
withdrawTokenFilter,
176180
musdTokenFilter,
177181
payToken,

app/components/Views/confirmations/hooks/pay/useAutomaticTransactionPayToken.test.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
import { useTransactionPayToken } from './useTransactionPayToken';
88
import { simpleSendTransactionControllerMock } from '../../__mocks__/controllers/transaction-controller-mock';
99
import { transactionApprovalControllerMock } from '../../__mocks__/controllers/approval-controller-mock';
10-
import { selectMetaMaskPayTokensFlags } from '../../../../../selectors/featureFlagController/confirmations';
10+
import {
11+
MetaMaskPayTokensFlags,
12+
selectMetaMaskPayTokensFlags,
13+
} from '../../../../../selectors/featureFlagController/confirmations';
1114
import { isHardwareAccount } from '../../../../../util/address';
1215
import { TransactionType } from '@metamask/transaction-controller';
1316
import { TransactionPayRequiredToken } from '@metamask/transaction-pay-controller';
@@ -111,7 +114,14 @@ describe('useAutomaticTransactionPayToken', () => {
111114
selectMetaMaskPayTokensFlagsMock.mockReturnValue({
112115
preferredTokens: { default: [], overrides: {} },
113116
minimumRequiredTokenBalance: 0,
114-
});
117+
blockedTokens: {
118+
default: {
119+
chainIds: [],
120+
tokens: [],
121+
},
122+
overrides: {},
123+
},
124+
} as MetaMaskPayTokensFlags);
115125
});
116126

117127
it('selects first token', () => {
@@ -341,7 +351,14 @@ describe('useAutomaticTransactionPayToken', () => {
341351
},
342352
},
343353
minimumRequiredTokenBalance: 5,
344-
});
354+
blockedTokens: {
355+
default: {
356+
chainIds: [],
357+
tokens: [],
358+
},
359+
overrides: {},
360+
},
361+
} as MetaMaskPayTokensFlags);
345362

346363
useTransactionPayAvailableTokensMock.mockReturnValue({
347364
availableTokens: [
@@ -387,7 +404,14 @@ describe('useAutomaticTransactionPayToken', () => {
387404
},
388405
},
389406
minimumRequiredTokenBalance: 15,
390-
});
407+
blockedTokens: {
408+
default: {
409+
chainIds: [],
410+
tokens: [],
411+
},
412+
overrides: {},
413+
},
414+
} as MetaMaskPayTokensFlags);
391415

392416
useTransactionPayAvailableTokensMock.mockReturnValue({
393417
availableTokens: [
@@ -428,7 +452,14 @@ describe('useAutomaticTransactionPayToken', () => {
428452
},
429453
},
430454
minimumRequiredTokenBalance: 100,
431-
});
455+
blockedTokens: {
456+
default: {
457+
chainIds: [],
458+
tokens: [],
459+
},
460+
overrides: {},
461+
},
462+
} as MetaMaskPayTokensFlags);
432463

433464
useTransactionPayAvailableTokensMock.mockReturnValue({
434465
availableTokens: [
@@ -495,7 +526,14 @@ describe('useAutomaticTransactionPayToken', () => {
495526
},
496527
},
497528
minimumRequiredTokenBalance: 0,
498-
});
529+
blockedTokens: {
530+
default: {
531+
chainIds: [],
532+
tokens: [],
533+
},
534+
overrides: {},
535+
},
536+
} as MetaMaskPayTokensFlags);
499537

500538
useTransactionPayAvailableTokensMock.mockReturnValue({
501539
availableTokens: [
@@ -543,7 +581,14 @@ describe('useAutomaticTransactionPayToken', () => {
543581
},
544582
},
545583
minimumRequiredTokenBalance: 5,
546-
});
584+
blockedTokens: {
585+
default: {
586+
chainIds: [],
587+
tokens: [],
588+
},
589+
overrides: {},
590+
},
591+
} as MetaMaskPayTokensFlags);
547592

548593
useTransactionPayAvailableTokensMock.mockReturnValue({
549594
availableTokens: [

app/components/Views/confirmations/hooks/pay/useTransactionPayAvailableTokens.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,23 @@ import { AssetType, TokenStandard } from '../../types/token';
77
import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider';
88
import { getAvailableTokens } from '../../utils/transaction-pay';
99
import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest';
10+
import {
11+
selectMetaMaskPayTokensFlags,
12+
MetaMaskPayTokensFlags,
13+
} from '../../../../../selectors/featureFlagController/confirmations';
1014

1115
jest.mock('../send/useAccountTokens');
12-
jest.mock('../../utils/transaction-pay');
16+
jest.mock('../../utils/transaction-pay', () => ({
17+
...jest.requireActual('../../utils/transaction-pay'),
18+
getAvailableTokens: jest.fn(),
19+
}));
1320
jest.mock('../transactions/useTransactionMetadataRequest');
21+
jest.mock(
22+
'../../../../../selectors/featureFlagController/confirmations',
23+
() => ({
24+
selectMetaMaskPayTokensFlags: jest.fn(),
25+
}),
26+
);
1427

1528
const TOKEN_MOCK = {
1629
accountType: EthAccountType.Eoa,
@@ -30,12 +43,26 @@ describe('useTransactionPayAvailableTokens', () => {
3043
useTransactionMetadataRequest,
3144
);
3245

46+
const selectMetaMaskPayTokensFlagsMock = jest.mocked(
47+
selectMetaMaskPayTokensFlags,
48+
);
49+
50+
const defaultPayTokensFlags: MetaMaskPayTokensFlags = {
51+
preferredTokens: { default: [], overrides: {} },
52+
blockedTokens: {
53+
default: { chainIds: [], tokens: [] },
54+
overrides: {},
55+
},
56+
minimumRequiredTokenBalance: 0,
57+
};
58+
3359
beforeEach(() => {
3460
jest.resetAllMocks();
3561

3662
useAccountTokensMock.mockReturnValue([]);
3763
useTransactionMetadataRequestMock.mockReturnValue(undefined);
3864
jest.mocked(getAvailableTokens).mockReturnValue([TOKEN_MOCK]);
65+
selectMetaMaskPayTokensFlagsMock.mockReturnValue(defaultPayTokensFlags);
3966
});
4067

4168
it('returns available tokens and hasTokens true when tokens exist', () => {
@@ -61,4 +88,56 @@ describe('useTransactionPayAvailableTokens', () => {
6188
expect(result.current.availableTokens).toEqual([]);
6289
expect(result.current.hasTokens).toBe(true);
6390
});
91+
92+
it('passes resolved blocklist to getAvailableTokens', () => {
93+
const blockedList = {
94+
chainIds: ['0xa4b1'],
95+
tokens: [{ address: '0xabc', chainId: '0x1' }],
96+
};
97+
98+
selectMetaMaskPayTokensFlagsMock.mockReturnValue({
99+
...defaultPayTokensFlags,
100+
blockedTokens: {
101+
default: { chainIds: [], tokens: [] },
102+
overrides: {
103+
perpsDeposit: blockedList,
104+
},
105+
},
106+
});
107+
108+
useTransactionMetadataRequestMock.mockReturnValue({
109+
type: TransactionType.perpsDeposit,
110+
} as ReturnType<typeof useTransactionMetadataRequest>);
111+
112+
renderHookWithProvider(useTransactionPayAvailableTokens);
113+
114+
expect(getAvailableTokens).toHaveBeenCalledWith(
115+
expect.objectContaining({
116+
blockedTokens: blockedList,
117+
}),
118+
);
119+
});
120+
121+
it('passes default blocklist when transaction type has no override', () => {
122+
const defaultBlocked = {
123+
chainIds: ['0x1'],
124+
tokens: [],
125+
};
126+
127+
selectMetaMaskPayTokensFlagsMock.mockReturnValue({
128+
...defaultPayTokensFlags,
129+
blockedTokens: {
130+
default: defaultBlocked,
131+
overrides: {},
132+
},
133+
});
134+
135+
renderHookWithProvider(useTransactionPayAvailableTokens);
136+
137+
expect(getAvailableTokens).toHaveBeenCalledWith(
138+
expect.objectContaining({
139+
blockedTokens: defaultBlocked,
140+
}),
141+
);
142+
});
64143
});

app/components/Views/confirmations/hooks/pay/useTransactionPayAvailableTokens.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import { useMemo } from 'react';
2+
23
import { useAccountTokens } from '../send/useAccountTokens';
34
import { getAvailableTokens } from '../../utils/transaction-pay';
45
import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest';
56
import { isTransactionPayWithdraw } from '../../utils/transaction';
7+
import { useTransactionPayBlockedTokens } from './useTransactionPayBlockedTokens';
68

79
export function useTransactionPayAvailableTokens() {
810
const tokens = useAccountTokens({ includeNoBalance: true });
911
const transactionMeta = useTransactionMetadataRequest();
1012
const isPostQuote = isTransactionPayWithdraw(transactionMeta);
13+
const blockedTokens = useTransactionPayBlockedTokens();
1114

1215
const availableTokens = useMemo(
1316
() =>
1417
getAvailableTokens({
1518
tokens,
19+
blockedTokens,
1620
}),
17-
[tokens],
21+
[tokens, blockedTokens],
1822
);
1923

2024
// For post-quote transactions, tokens are always available
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useMemo } from 'react';
2+
import { useSelector } from 'react-redux';
3+
import { selectMetaMaskPayTokensFlags } from '../../../../../selectors/featureFlagController/confirmations';
4+
import { getBlockedTokensForTransactionType } from '../../utils/transaction-pay';
5+
import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest';
6+
7+
export function useTransactionPayBlockedTokens() {
8+
const transactionMeta = useTransactionMetadataRequest();
9+
const payTokensFlags = useSelector(selectMetaMaskPayTokensFlags);
10+
11+
return useMemo(
12+
() =>
13+
getBlockedTokensForTransactionType(
14+
payTokensFlags.blockedTokens,
15+
transactionMeta?.type,
16+
),
17+
[payTokensFlags.blockedTokens, transactionMeta?.type],
18+
);
19+
}

0 commit comments

Comments
 (0)