Skip to content

Commit 447d6a1

Browse files
authored
release: 7.77.1 (#30309)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **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: null ## **Related issues** Fixes: ## **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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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** > Adds new Polymarket Deposit Wallet withdrawal pathways and TransactionPayController callbacks (including message signing and retry logic), which affects withdrawal execution and confirmations labeling. Risk is moderate due to changes in transaction configuration and batch submission behavior behind feature flags. > > **Overview** > **Predict withdrawals now support Polymarket Deposit Wallets (feature-flagged).** `PredictBalance` gates the previous “withdrawals unavailable” behavior behind `confirmations_pay_extended.enableDepositWalletWithdraw`, allowing direct withdrawal when enabled. > > **Withdrawal execution/configuration is adjusted for Deposit Wallets.** `PredictController.prepareWithdraw` now conditionally omits `gasFeeToken` for deposit-wallet accounts, and `useTransactionPayPostQuote` skips `refundTo` while marking deposit-wallet Predict withdrawals via `isPolymarketDepositWallet`. > > **MetaMask Pay/confirmations integration expanded for Predict withdraw.** Transaction details summary rendering now treats `predictWithdraw` as a receive-type flow and adds a dedicated `predict_withdraw` title using MetaMask Pay source chain/token metadata; new Polymarket callbacks are wired into `TransactionPayControllerInit` with signer support and “wallet busy” retries. Release metadata is bumped to `7.77.1` (CHANGELOG + `OTA_VERSION`) and dependency resolutions update `@metamask/transaction-pay-controller` to `^22.5.0`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 808af5d. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
2 parents 0193bbd + 808af5d commit 447d6a1

30 files changed

Lines changed: 1107 additions & 160 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [7.77.1]
11+
12+
### Added
13+
14+
- Added support for cross-chain withdrawals through MetaMask Pay in Predict for users with a Polymarket Deposit Wallet, while keeping the existing "withdrawals unavailable" sheet for users who do not have the feature enabled. (#29953)
15+
16+
### Fixed
17+
18+
- Fixed a bug that caused a user's first Predict deposit to fail while their Polymarket Deposit Wallet was still being registered. (#30267)
19+
1020
## [7.77.0]
1121

1222
### Added
@@ -11494,7 +11504,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1149411504
- [#957](https://github.com/MetaMask/metamask-mobile/pull/957): fix timeouts (#957)
1149511505
- [#954](https://github.com/MetaMask/metamask-mobile/pull/954): Bugfix: onboarding navigation (#954)
1149611506

11497-
[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.0...HEAD
11507+
[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.1...HEAD
11508+
[7.77.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.0...v7.77.1
1149811509
[7.77.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.76.3...v7.77.0
1149911510
[7.76.3]: https://github.com/MetaMask/metamask-mobile/compare/v7.76.0...v7.76.3
1150011511
[7.76.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.75.1...v7.76.0

app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,25 @@ const initialState = {
7676
},
7777
};
7878

79+
function stateWithDepositWalletWithdrawEnabled(enabled: boolean) {
80+
return {
81+
engine: {
82+
backgroundState: {
83+
...initialState.engine.backgroundState,
84+
RemoteFeatureFlagController: {
85+
...backgroundState.RemoteFeatureFlagController,
86+
remoteFeatureFlags: {
87+
...backgroundState.RemoteFeatureFlagController?.remoteFeatureFlags,
88+
confirmations_pay_extended: {
89+
enableDepositWalletWithdraw: enabled,
90+
},
91+
},
92+
},
93+
},
94+
},
95+
};
96+
}
97+
7998
describe('PredictBalance', () => {
8099
beforeEach(() => {
81100
jest.clearAllMocks();
@@ -387,6 +406,43 @@ describe('PredictBalance', () => {
387406
expect(mockExecuteGuardedAction).not.toHaveBeenCalled();
388407
});
389408

409+
it('calls withdraw for Deposit Wallet users when enableDepositWalletWithdraw flag is on', () => {
410+
// Arrange
411+
const mockWithdraw = jest.fn();
412+
const mockOnDepositWalletWithdrawPress = jest.fn();
413+
mockUsePredictBalance.mockReturnValue({
414+
data: 100,
415+
isLoading: false,
416+
});
417+
mockUsePredictAccountState.mockReturnValue({
418+
data: {
419+
address: '0x2222222222222222222222222222222222222222',
420+
isDeployed: true,
421+
walletType: 'deposit-wallet',
422+
},
423+
isLoading: false,
424+
});
425+
mockUsePredictWithdraw.mockReturnValue({
426+
withdraw: mockWithdraw,
427+
});
428+
429+
// Act
430+
const { getByText } = renderWithProvider(
431+
<PredictBalance
432+
onDepositWalletWithdrawPress={mockOnDepositWalletWithdrawPress}
433+
/>,
434+
{
435+
state: stateWithDepositWalletWithdrawEnabled(true),
436+
},
437+
);
438+
const withdrawButton = getByText(/Withdraw/i);
439+
fireEvent.press(withdrawButton);
440+
441+
// Assert
442+
expect(mockWithdraw).toHaveBeenCalledTimes(1);
443+
expect(mockOnDepositWalletWithdrawPress).not.toHaveBeenCalled();
444+
});
445+
390446
it('calls temporary unavailable handler instead of withdrawing for Deposit Wallet users', () => {
391447
// Arrange
392448
const mockWithdraw = jest.fn();

app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { PredictNavigationParamList } from '../../types/navigation';
4141
import { usePredictWithdraw } from '../../hooks/usePredictWithdraw';
4242
import { usePredictAccountState } from '../../hooks/usePredictAccountState';
4343
import { PredictEventValues } from '../../constants/eventNames';
44+
import { selectMetaMaskPayFlags } from '../../../../../selectors/featureFlagController/confirmations';
4445
import { PREDICT_BALANCE_TEST_IDS } from './PredictBalance.testIds';
4546

4647
// This is a temporary component that will be removed when the deposit flow is fully implemented
@@ -55,6 +56,7 @@ const PredictBalance: React.FC<PredictBalanceProps> = ({
5556
}) => {
5657
const tw = useTailwind();
5758
const privacyMode = useSelector(selectPrivacyMode);
59+
const { enableDepositWalletWithdraw } = useSelector(selectMetaMaskPayFlags);
5860

5961
const navigation =
6062
useNavigation<NavigationProp<PredictNavigationParamList>>();
@@ -103,15 +105,18 @@ const PredictBalance: React.FC<PredictBalanceProps> = ({
103105
return;
104106
}
105107

106-
// Temporary Deposit Wallet migration guard. Remove this branch and sheet
107-
// once Deposit Wallet withdrawals are implemented.
108-
if (walletType === 'deposit-wallet') {
108+
if (walletType === 'deposit-wallet' && !enableDepositWalletWithdraw) {
109109
onDepositWalletWithdrawPress?.();
110110
return;
111111
}
112112

113113
withdraw();
114-
}, [onDepositWalletWithdrawPress, walletType, withdraw]);
114+
}, [
115+
enableDepositWalletWithdraw,
116+
onDepositWalletWithdrawPress,
117+
walletType,
118+
withdraw,
119+
]);
115120

116121
if (isLoading) {
117122
return (

app/components/UI/Predict/controllers/PredictController.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,11 @@ describe('PredictController', () => {
288288
syncDepositWalletBalanceAllowanceForDepositTransaction: jest.fn(),
289289
} as unknown as jest.Mocked<PolymarketProvider>;
290290

291+
mockPolymarketProvider.getAccountState.mockResolvedValue({
292+
address: '0xProxyAddress' as `0x${string}`,
293+
isDeployed: true,
294+
walletType: 'safe' as const,
295+
});
291296
mockPolymarketProvider.beforePublishDepositWalletDeposit.mockResolvedValue(
292297
true,
293298
);
@@ -6076,6 +6081,54 @@ describe('PredictController', () => {
60766081
});
60776082
});
60786083

6084+
it('sets gasFeeToken when account walletType is not deposit-wallet', async () => {
6085+
mockPolymarketProvider.prepareWithdraw.mockResolvedValue(
6086+
mockWithdrawResponse,
6087+
);
6088+
mockPolymarketProvider.getAccountState.mockResolvedValue({
6089+
address: '0xProxyAddress' as `0x${string}`,
6090+
isDeployed: true,
6091+
walletType: 'safe' as const,
6092+
});
6093+
(addTransactionBatch as jest.Mock).mockResolvedValue({
6094+
batchId: 'batch-safe',
6095+
});
6096+
6097+
await withController(async ({ controller }) => {
6098+
await controller.prepareWithdraw({});
6099+
6100+
expect(addTransactionBatch).toHaveBeenCalledWith(
6101+
expect.objectContaining({
6102+
gasFeeToken: MATIC_CONTRACTS_V2.collateral,
6103+
}),
6104+
);
6105+
});
6106+
});
6107+
6108+
it('omits gasFeeToken when account walletType is deposit-wallet', async () => {
6109+
mockPolymarketProvider.prepareWithdraw.mockResolvedValue(
6110+
mockWithdrawResponse,
6111+
);
6112+
mockPolymarketProvider.getAccountState.mockResolvedValue({
6113+
address: '0xDepositWalletAddress' as `0x${string}`,
6114+
isDeployed: true,
6115+
walletType: 'deposit-wallet' as const,
6116+
});
6117+
(addTransactionBatch as jest.Mock).mockResolvedValue({
6118+
batchId: 'batch-deposit',
6119+
});
6120+
6121+
await withController(async ({ controller }) => {
6122+
await controller.prepareWithdraw({});
6123+
6124+
expect(addTransactionBatch).toHaveBeenCalledWith(
6125+
expect.objectContaining({
6126+
gasFeeToken: undefined,
6127+
}),
6128+
);
6129+
});
6130+
});
6131+
60796132
it('update transaction ID when batch ID is returned', async () => {
60806133
const mockBatchId = 'tx-batch-update';
60816134

app/components/UI/Predict/controllers/PredictController.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2595,6 +2595,16 @@ export class PredictController extends BaseController<
25952595
signer,
25962596
});
25972597

2598+
const accountState = await provider.getAccountState({
2599+
ownerAddress: signer.address,
2600+
});
2601+
2602+
const isDepositWallet = accountState.walletType === 'deposit-wallet';
2603+
2604+
const gasFeeToken = isDepositWallet
2605+
? undefined
2606+
: (MATIC_CONTRACTS_V2.collateral as Hex);
2607+
25982608
this.update((state) => {
25992609
state.withdrawTransaction = {
26002610
chainId: hexToNumber(chainId),
@@ -2616,9 +2626,8 @@ export class PredictController extends BaseController<
26162626
disableHook: true,
26172627
disableSequential: true,
26182628
requireApproval: true,
2619-
// Temporarily breaking abstraction, can instead be abstracted via provider.
2620-
gasFeeToken: MATIC_CONTRACTS_V2.collateral as Hex,
26212629
transactions: [transaction],
2630+
gasFeeToken,
26222631
});
26232632

26242633
this.update((state) => {

app/components/UI/Predict/providers/polymarket/depositWallet.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,10 @@ describe('depositWallet', () => {
269269

270270
it('keeps polling when completion succeeds without a hash', async () => {
271271
mockFetch
272-
.mockResolvedValueOnce(mockResponse({ state: 'STATE_MINED' }))
272+
.mockResolvedValueOnce(mockResponse({ state: 'STATE_CONFIRMED' }))
273273
.mockResolvedValueOnce(
274274
mockResponse({
275-
state: 'STATE_MINED',
275+
state: 'STATE_CONFIRMED',
276276
transactionHash:
277277
'0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
278278
}),

app/components/UI/Predict/providers/polymarket/depositWallet.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const DEPOSIT_WALLET_DOMAIN_VERSION = '1';
2626
// allowed window to reduce intermittent "deadline too soon" failures.
2727
const DEPOSIT_WALLET_BATCH_DEADLINE_SECONDS = 300;
2828

29-
const RELAYER_SUCCESS_STATES = new Set(['STATE_MINED', 'STATE_CONFIRMED']);
29+
const RELAYER_SUCCESS_STATES = new Set(['STATE_CONFIRMED']);
3030
const RELAYER_FAILURE_STATES = new Set(['STATE_FAILED', 'STATE_INVALID']);
3131

3232
/**

app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { selectBridgeHistoryForAccount } from '../../../../../../selectors/bridg
1313
import { useBridgeTxHistoryData } from '../../../../../../util/bridge/hooks/useBridgeTxHistoryData';
1414
import { useTokenAmount } from '../../../hooks/useTokenAmount';
1515
import { useTransactionDetails } from '../../../hooks/activity/useTransactionDetails';
16+
import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance';
1617
import { ReceiveSummaryLine } from './receive-summary-line';
1718

1819
jest.mock('../../../../../UI/Bridge/hooks/useMultichainBlockExplorerTxUrl');
@@ -21,6 +22,7 @@ jest.mock('../../../../../../selectors/bridgeStatusController');
2122
jest.mock('../../../../../../util/bridge/hooks/useBridgeTxHistoryData');
2223
jest.mock('../../../hooks/useTokenAmount');
2324
jest.mock('../../../hooks/activity/useTransactionDetails');
25+
jest.mock('../../../hooks/tokens/useTokenWithBalance');
2426

2527
jest.mock('@react-navigation/native', () => ({
2628
...jest.requireActual('@react-navigation/native'),
@@ -161,4 +163,36 @@ describe('ReceiveSummaryLine', () => {
161163
),
162164
).toBeDefined();
163165
});
166+
167+
it('renders predict withdraw title using source token symbol and source network', () => {
168+
useNetworkNameMock.mockImplementation((chainId?: Hex) =>
169+
chainId === '0x1' ? 'Ethereum' : 'Polygon',
170+
);
171+
jest
172+
.mocked(useTokenWithBalance)
173+
.mockReturnValue({ symbol: 'USDC' } as ReturnType<
174+
typeof useTokenWithBalance
175+
>);
176+
177+
const { getByText } = render({
178+
id: 'tx-id',
179+
chainId: '0x89' as Hex,
180+
hash: '0x123',
181+
submittedTime: 1755719285723,
182+
type: TransactionType.predictWithdraw,
183+
metamaskPay: {
184+
chainId: '0x1' as Hex,
185+
tokenAddress: '0xabc' as Hex,
186+
},
187+
} as Partial<TransactionMeta>);
188+
189+
expect(
190+
getByText(
191+
strings('transaction_details.summary_title.bridge_receive', {
192+
targetSymbol: 'USDC',
193+
targetChain: 'Ethereum',
194+
}),
195+
),
196+
).toBeDefined();
197+
});
164198
});

app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { hasTransactionType } from '../../../utils/transaction';
1010
import { useNetworkName } from '../../../hooks/useNetworkName';
1111
import { POLYGON_PUSD } from '../../../constants/predict';
1212
import { TransactionSummaryLine } from './transaction-summary-line';
13+
import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance';
1314

1415
const HYPERLIQUID_EXPLORER_URL = 'https://app.hyperliquid.xyz/explorer/tx';
1516
const HYPERLIQUID_EXPLORER_NAME = 'Hyperliquid';
@@ -19,7 +20,10 @@ export function ReceiveSummaryLine({
1920
}: {
2021
transactionMeta: TransactionMeta;
2122
}) {
22-
const { chainId } = transactionMeta;
23+
const { chainId: targetChainId, metamaskPay } = transactionMeta;
24+
const sourceChainId = metamaskPay?.chainId;
25+
const sourceTokenAddress = metamaskPay?.tokenAddress;
26+
2327
const isPerpsDeposit = hasTransactionType(transactionMeta, [
2428
TransactionType.perpsDeposit,
2529
]);
@@ -28,25 +32,39 @@ export function ReceiveSummaryLine({
2832
TransactionType.predictDeposit,
2933
]);
3034

31-
const networkName = useNetworkName(chainId);
35+
const isPredictWithdraw = hasTransactionType(transactionMeta, [
36+
TransactionType.predictWithdraw,
37+
]);
38+
39+
const targetNetworkName = useNetworkName(targetChainId);
40+
const sourceNetworkName = useNetworkName(sourceChainId ?? '0x0');
41+
42+
const sourceToken = useTokenWithBalance(
43+
sourceTokenAddress ?? '0x0',
44+
sourceChainId ?? '0x0',
45+
);
3246

3347
let targetSymbol = 'mUSD';
34-
let targetNetworkName: string | undefined = networkName;
35-
let receiveChainId: Hex = chainId;
48+
let finalTargetNetworkName: string | undefined = targetNetworkName;
49+
let receiveChainId: Hex = targetChainId;
3650

3751
if (isPerpsDeposit) {
3852
targetSymbol = 'USDC';
39-
targetNetworkName = 'Hyperliquid';
53+
finalTargetNetworkName = 'Hyperliquid';
4054
receiveChainId = CHAIN_IDS.ARBITRUM;
4155
} else if (isPredictDeposit) {
4256
targetSymbol = POLYGON_PUSD.symbol;
57+
} else if (isPredictWithdraw) {
58+
targetSymbol = sourceToken?.symbol ?? 'Unknown';
59+
finalTargetNetworkName = sourceNetworkName;
60+
receiveChainId = sourceChainId ?? '0x0';
4361
}
4462

4563
const title =
46-
targetSymbol && targetNetworkName
64+
targetSymbol && finalTargetNetworkName
4765
? strings('transaction_details.summary_title.bridge_receive', {
4866
targetSymbol,
49-
targetChain: targetNetworkName,
67+
targetChain: finalTargetNetworkName,
5068
})
5169
: strings('transaction_details.summary_title.bridge_receive_loading');
5270

0 commit comments

Comments
 (0)