diff --git a/app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx b/app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx
index 6729e67f2de..d72ba723e55 100644
--- a/app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx
+++ b/app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx
@@ -76,6 +76,25 @@ const initialState = {
},
};
+function stateWithDepositWalletWithdrawEnabled(enabled: boolean) {
+ return {
+ engine: {
+ backgroundState: {
+ ...initialState.engine.backgroundState,
+ RemoteFeatureFlagController: {
+ ...backgroundState.RemoteFeatureFlagController,
+ remoteFeatureFlags: {
+ ...backgroundState.RemoteFeatureFlagController?.remoteFeatureFlags,
+ confirmations_pay_extended: {
+ enableDepositWalletWithdraw: enabled,
+ },
+ },
+ },
+ },
+ },
+ };
+}
+
describe('PredictBalance', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -387,6 +406,43 @@ describe('PredictBalance', () => {
expect(mockExecuteGuardedAction).not.toHaveBeenCalled();
});
+ it('calls withdraw for Deposit Wallet users when enableDepositWalletWithdraw flag is on', () => {
+ // Arrange
+ const mockWithdraw = jest.fn();
+ const mockOnDepositWalletWithdrawPress = jest.fn();
+ mockUsePredictBalance.mockReturnValue({
+ data: 100,
+ isLoading: false,
+ });
+ mockUsePredictAccountState.mockReturnValue({
+ data: {
+ address: '0x2222222222222222222222222222222222222222',
+ isDeployed: true,
+ walletType: 'deposit-wallet',
+ },
+ isLoading: false,
+ });
+ mockUsePredictWithdraw.mockReturnValue({
+ withdraw: mockWithdraw,
+ });
+
+ // Act
+ const { getByText } = renderWithProvider(
+ ,
+ {
+ state: stateWithDepositWalletWithdrawEnabled(true),
+ },
+ );
+ const withdrawButton = getByText(/Withdraw/i);
+ fireEvent.press(withdrawButton);
+
+ // Assert
+ expect(mockWithdraw).toHaveBeenCalledTimes(1);
+ expect(mockOnDepositWalletWithdrawPress).not.toHaveBeenCalled();
+ });
+
it('calls temporary unavailable handler instead of withdrawing for Deposit Wallet users', () => {
// Arrange
const mockWithdraw = jest.fn();
diff --git a/app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx b/app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx
index bc68f1121a7..bbb71ca05d2 100644
--- a/app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx
+++ b/app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx
@@ -41,6 +41,7 @@ import { PredictNavigationParamList } from '../../types/navigation';
import { usePredictWithdraw } from '../../hooks/usePredictWithdraw';
import { usePredictAccountState } from '../../hooks/usePredictAccountState';
import { PredictEventValues } from '../../constants/eventNames';
+import { selectMetaMaskPayFlags } from '../../../../../selectors/featureFlagController/confirmations';
import { PREDICT_BALANCE_TEST_IDS } from './PredictBalance.testIds';
// This is a temporary component that will be removed when the deposit flow is fully implemented
@@ -55,6 +56,7 @@ const PredictBalance: React.FC = ({
}) => {
const tw = useTailwind();
const privacyMode = useSelector(selectPrivacyMode);
+ const { enableDepositWalletWithdraw } = useSelector(selectMetaMaskPayFlags);
const navigation =
useNavigation>();
@@ -103,15 +105,18 @@ const PredictBalance: React.FC = ({
return;
}
- // Temporary Deposit Wallet migration guard. Remove this branch and sheet
- // once Deposit Wallet withdrawals are implemented.
- if (walletType === 'deposit-wallet') {
+ if (walletType === 'deposit-wallet' && !enableDepositWalletWithdraw) {
onDepositWalletWithdrawPress?.();
return;
}
withdraw();
- }, [onDepositWalletWithdrawPress, walletType, withdraw]);
+ }, [
+ enableDepositWalletWithdraw,
+ onDepositWalletWithdrawPress,
+ walletType,
+ withdraw,
+ ]);
if (isLoading) {
return (
diff --git a/app/components/UI/Predict/controllers/PredictController.test.ts b/app/components/UI/Predict/controllers/PredictController.test.ts
index e26ec86de63..2c3db2effb9 100644
--- a/app/components/UI/Predict/controllers/PredictController.test.ts
+++ b/app/components/UI/Predict/controllers/PredictController.test.ts
@@ -288,6 +288,11 @@ describe('PredictController', () => {
syncDepositWalletBalanceAllowanceForDepositTransaction: jest.fn(),
} as unknown as jest.Mocked;
+ mockPolymarketProvider.getAccountState.mockResolvedValue({
+ address: '0xProxyAddress' as `0x${string}`,
+ isDeployed: true,
+ walletType: 'safe' as const,
+ });
mockPolymarketProvider.beforePublishDepositWalletDeposit.mockResolvedValue(
true,
);
@@ -6076,6 +6081,54 @@ describe('PredictController', () => {
});
});
+ it('sets gasFeeToken when account walletType is not deposit-wallet', async () => {
+ mockPolymarketProvider.prepareWithdraw.mockResolvedValue(
+ mockWithdrawResponse,
+ );
+ mockPolymarketProvider.getAccountState.mockResolvedValue({
+ address: '0xProxyAddress' as `0x${string}`,
+ isDeployed: true,
+ walletType: 'safe' as const,
+ });
+ (addTransactionBatch as jest.Mock).mockResolvedValue({
+ batchId: 'batch-safe',
+ });
+
+ await withController(async ({ controller }) => {
+ await controller.prepareWithdraw({});
+
+ expect(addTransactionBatch).toHaveBeenCalledWith(
+ expect.objectContaining({
+ gasFeeToken: MATIC_CONTRACTS_V2.collateral,
+ }),
+ );
+ });
+ });
+
+ it('omits gasFeeToken when account walletType is deposit-wallet', async () => {
+ mockPolymarketProvider.prepareWithdraw.mockResolvedValue(
+ mockWithdrawResponse,
+ );
+ mockPolymarketProvider.getAccountState.mockResolvedValue({
+ address: '0xDepositWalletAddress' as `0x${string}`,
+ isDeployed: true,
+ walletType: 'deposit-wallet' as const,
+ });
+ (addTransactionBatch as jest.Mock).mockResolvedValue({
+ batchId: 'batch-deposit',
+ });
+
+ await withController(async ({ controller }) => {
+ await controller.prepareWithdraw({});
+
+ expect(addTransactionBatch).toHaveBeenCalledWith(
+ expect.objectContaining({
+ gasFeeToken: undefined,
+ }),
+ );
+ });
+ });
+
it('update transaction ID when batch ID is returned', async () => {
const mockBatchId = 'tx-batch-update';
diff --git a/app/components/UI/Predict/controllers/PredictController.ts b/app/components/UI/Predict/controllers/PredictController.ts
index dff17733a08..cb7c140a2fc 100644
--- a/app/components/UI/Predict/controllers/PredictController.ts
+++ b/app/components/UI/Predict/controllers/PredictController.ts
@@ -2595,6 +2595,16 @@ export class PredictController extends BaseController<
signer,
});
+ const accountState = await provider.getAccountState({
+ ownerAddress: signer.address,
+ });
+
+ const isDepositWallet = accountState.walletType === 'deposit-wallet';
+
+ const gasFeeToken = isDepositWallet
+ ? undefined
+ : (MATIC_CONTRACTS_V2.collateral as Hex);
+
this.update((state) => {
state.withdrawTransaction = {
chainId: hexToNumber(chainId),
@@ -2616,9 +2626,8 @@ export class PredictController extends BaseController<
disableHook: true,
disableSequential: true,
requireApproval: true,
- // Temporarily breaking abstraction, can instead be abstracted via provider.
- gasFeeToken: MATIC_CONTRACTS_V2.collateral as Hex,
transactions: [transaction],
+ gasFeeToken,
});
this.update((state) => {
diff --git a/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.test.tsx b/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.test.tsx
index 129a93dfa89..83d293d4385 100644
--- a/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.test.tsx
+++ b/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.test.tsx
@@ -13,6 +13,7 @@ import { selectBridgeHistoryForAccount } from '../../../../../../selectors/bridg
import { useBridgeTxHistoryData } from '../../../../../../util/bridge/hooks/useBridgeTxHistoryData';
import { useTokenAmount } from '../../../hooks/useTokenAmount';
import { useTransactionDetails } from '../../../hooks/activity/useTransactionDetails';
+import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance';
import { ReceiveSummaryLine } from './receive-summary-line';
jest.mock('../../../../../UI/Bridge/hooks/useMultichainBlockExplorerTxUrl');
@@ -21,6 +22,7 @@ jest.mock('../../../../../../selectors/bridgeStatusController');
jest.mock('../../../../../../util/bridge/hooks/useBridgeTxHistoryData');
jest.mock('../../../hooks/useTokenAmount');
jest.mock('../../../hooks/activity/useTransactionDetails');
+jest.mock('../../../hooks/tokens/useTokenWithBalance');
jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
@@ -161,4 +163,36 @@ describe('ReceiveSummaryLine', () => {
),
).toBeDefined();
});
+
+ it('renders predict withdraw title using source token symbol and source network', () => {
+ useNetworkNameMock.mockImplementation((chainId?: Hex) =>
+ chainId === '0x1' ? 'Ethereum' : 'Polygon',
+ );
+ jest
+ .mocked(useTokenWithBalance)
+ .mockReturnValue({ symbol: 'USDC' } as ReturnType<
+ typeof useTokenWithBalance
+ >);
+
+ const { getByText } = render({
+ id: 'tx-id',
+ chainId: '0x89' as Hex,
+ hash: '0x123',
+ submittedTime: 1755719285723,
+ type: TransactionType.predictWithdraw,
+ metamaskPay: {
+ chainId: '0x1' as Hex,
+ tokenAddress: '0xabc' as Hex,
+ },
+ } as Partial);
+
+ expect(
+ getByText(
+ strings('transaction_details.summary_title.bridge_receive', {
+ targetSymbol: 'USDC',
+ targetChain: 'Ethereum',
+ }),
+ ),
+ ).toBeDefined();
+ });
});
diff --git a/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.tsx b/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.tsx
index 8aacc51634c..11a12001461 100644
--- a/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.tsx
+++ b/app/components/Views/confirmations/components/activity/transaction-details-summary/receive-summary-line.tsx
@@ -10,6 +10,7 @@ import { hasTransactionType } from '../../../utils/transaction';
import { useNetworkName } from '../../../hooks/useNetworkName';
import { POLYGON_PUSD } from '../../../constants/predict';
import { TransactionSummaryLine } from './transaction-summary-line';
+import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance';
const HYPERLIQUID_EXPLORER_URL = 'https://app.hyperliquid.xyz/explorer/tx';
const HYPERLIQUID_EXPLORER_NAME = 'Hyperliquid';
@@ -19,7 +20,10 @@ export function ReceiveSummaryLine({
}: {
transactionMeta: TransactionMeta;
}) {
- const { chainId } = transactionMeta;
+ const { chainId: targetChainId, metamaskPay } = transactionMeta;
+ const sourceChainId = metamaskPay?.chainId;
+ const sourceTokenAddress = metamaskPay?.tokenAddress;
+
const isPerpsDeposit = hasTransactionType(transactionMeta, [
TransactionType.perpsDeposit,
]);
@@ -28,25 +32,39 @@ export function ReceiveSummaryLine({
TransactionType.predictDeposit,
]);
- const networkName = useNetworkName(chainId);
+ const isPredictWithdraw = hasTransactionType(transactionMeta, [
+ TransactionType.predictWithdraw,
+ ]);
+
+ const targetNetworkName = useNetworkName(targetChainId);
+ const sourceNetworkName = useNetworkName(sourceChainId ?? '0x0');
+
+ const sourceToken = useTokenWithBalance(
+ sourceTokenAddress ?? '0x0',
+ sourceChainId ?? '0x0',
+ );
let targetSymbol = 'mUSD';
- let targetNetworkName: string | undefined = networkName;
- let receiveChainId: Hex = chainId;
+ let finalTargetNetworkName: string | undefined = targetNetworkName;
+ let receiveChainId: Hex = targetChainId;
if (isPerpsDeposit) {
targetSymbol = 'USDC';
- targetNetworkName = 'Hyperliquid';
+ finalTargetNetworkName = 'Hyperliquid';
receiveChainId = CHAIN_IDS.ARBITRUM;
} else if (isPredictDeposit) {
targetSymbol = POLYGON_PUSD.symbol;
+ } else if (isPredictWithdraw) {
+ targetSymbol = sourceToken?.symbol ?? 'Unknown';
+ finalTargetNetworkName = sourceNetworkName;
+ receiveChainId = sourceChainId ?? '0x0';
}
const title =
- targetSymbol && targetNetworkName
+ targetSymbol && finalTargetNetworkName
? strings('transaction_details.summary_title.bridge_receive', {
targetSymbol,
- targetChain: targetNetworkName,
+ targetChain: finalTargetNetworkName,
})
: strings('transaction_details.summary_title.bridge_receive_loading');
diff --git a/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.test.tsx b/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.test.tsx
index 696513c3528..a9d215105b9 100644
--- a/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.test.tsx
+++ b/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.test.tsx
@@ -1,6 +1,9 @@
import React from 'react';
import { fireEvent } from '@testing-library/react-native';
-import { TransactionMeta } from '@metamask/transaction-controller';
+import {
+ TransactionMeta,
+ TransactionType,
+} from '@metamask/transaction-controller';
import { Hex } from '@metamask/utils';
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
import { strings } from '../../../../../../../locales/i18n';
@@ -31,11 +34,11 @@ jest.mock('@react-navigation/native', () => ({
}),
}));
-function render() {
+function render(parentTransaction?: Partial) {
return renderWithProvider(
,
@@ -118,4 +121,30 @@ describe('SourceHashSummaryLine', () => {
},
});
});
+
+ it('renders predict-withdraw title with pUSD and target network', () => {
+ useNetworkNameMock.mockImplementation((chainId?: Hex) =>
+ chainId === '0x89' ? 'Polygon' : 'Ethereum',
+ );
+
+ const { getByText } = render({
+ id: 'parent-id',
+ chainId: '0x89' as Hex,
+ submittedTime: 1755719285723,
+ type: TransactionType.predictWithdraw,
+ metamaskPay: {
+ tokenAddress: '0x123' as Hex,
+ chainId: '0x1' as Hex,
+ },
+ } as Partial);
+
+ expect(
+ getByText(
+ strings('transaction_details.summary_title.predict_withdraw', {
+ sourceSymbol: 'pUSD',
+ sourceChain: 'Polygon',
+ }),
+ ),
+ ).toBeDefined();
+ });
});
diff --git a/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.tsx b/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.tsx
index 87733f43eab..e5e62fe251b 100644
--- a/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.tsx
+++ b/app/components/Views/confirmations/components/activity/transaction-details-summary/source-hash-summary-line.tsx
@@ -1,10 +1,15 @@
import React from 'react';
-import { TransactionMeta } from '@metamask/transaction-controller';
+import {
+ TransactionMeta,
+ TransactionType,
+} from '@metamask/transaction-controller';
import { Hex } from '@metamask/utils';
import { strings } from '../../../../../../../locales/i18n';
import { useNetworkName } from '../../../hooks/useNetworkName';
import { useTokenWithBalance } from '../../../hooks/tokens/useTokenWithBalance';
import { TransactionSummaryLine } from './transaction-summary-line';
+import { hasTransactionType } from '../../../utils/transaction';
+import { POLYGON_PUSD } from '../../../constants/predict';
export function SourceHashSummaryLine({
parentTransaction,
@@ -13,24 +18,39 @@ export function SourceHashSummaryLine({
parentTransaction: TransactionMeta;
sourceHash: Hex;
}) {
- const tokenAddress = parentTransaction.metamaskPay?.tokenAddress;
- const tokenChainId = parentTransaction.metamaskPay?.chainId;
+ const { chainId: targetChainId, metamaskPay } = parentTransaction;
+ const sourceTokenAddress = metamaskPay?.tokenAddress;
+ const sourceTokenChainId = metamaskPay?.chainId;
const sourceToken = useTokenWithBalance(
- tokenAddress ?? '0x0',
- tokenChainId ?? '0x0',
+ sourceTokenAddress ?? '0x0',
+ sourceTokenChainId ?? '0x0',
);
- const sourceNetworkName = useNetworkName(tokenChainId);
- const chainId = tokenChainId ?? parentTransaction.chainId;
+ const sourceNetworkName = useNetworkName(sourceTokenChainId);
+ const targetNetworkName = useNetworkName(targetChainId);
- const title =
- sourceToken?.symbol && sourceNetworkName
- ? strings('transaction_details.summary_title.bridge_send', {
- sourceSymbol: sourceToken.symbol,
- sourceChain: sourceNetworkName,
- })
- : strings('transaction_details.summary_title.bridge_send_loading');
+ const isPredictWithdraw = hasTransactionType(parentTransaction, [
+ TransactionType.predictWithdraw,
+ ]);
+
+ const chainId = isPredictWithdraw ? targetChainId : sourceTokenChainId;
+
+ let title = strings('transaction_details.summary_title.bridge_send_loading');
+
+ if (sourceToken?.symbol && sourceNetworkName) {
+ title = strings('transaction_details.summary_title.bridge_send', {
+ sourceSymbol: sourceToken.symbol,
+ sourceChain: sourceNetworkName,
+ });
+
+ if (isPredictWithdraw) {
+ title = strings('transaction_details.summary_title.predict_withdraw', {
+ sourceSymbol: POLYGON_PUSD.symbol,
+ sourceChain: targetNetworkName,
+ });
+ }
+ }
return (
;
diff --git a/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.test.tsx b/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.test.tsx
index 1847378f80b..0941e6d8583 100644
--- a/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.test.tsx
+++ b/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.test.tsx
@@ -360,12 +360,13 @@ describe('TransactionDetails', () => {
TransactionType.moneyAccountWithdraw,
TransactionType.perpsDeposit,
TransactionType.predictDeposit,
+ TransactionType.predictWithdraw,
])('includes %s', (type) => {
expect(SUMMARY_SECTION_TYPES).toContain(type);
});
- it('contains exactly 6 transaction types', () => {
- expect(SUMMARY_SECTION_TYPES).toHaveLength(6);
+ it('contains exactly 7 transaction types', () => {
+ expect(SUMMARY_SECTION_TYPES).toHaveLength(7);
});
});
});
diff --git a/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.tsx b/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.tsx
index 4fbe94fef1e..1901102cbc7 100644
--- a/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.tsx
+++ b/app/components/Views/confirmations/components/activity/transaction-details/transaction-details.tsx
@@ -32,6 +32,7 @@ export const SUMMARY_SECTION_TYPES = [
TransactionType.moneyAccountWithdraw,
TransactionType.perpsDeposit,
TransactionType.predictDeposit,
+ TransactionType.predictWithdraw,
];
export function TransactionDetails() {
diff --git a/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx b/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx
index 33ac381a605..f933a750ce3 100644
--- a/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx
+++ b/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx
@@ -54,6 +54,12 @@ jest.mock('../../../context/alert-system-context');
jest.mock('../../../hooks/transactions/useTransactionCustomAmountAlerts');
jest.mock('../../../hooks/pay/useTransactionPayMetrics');
jest.mock('../../../hooks/send/useAccountTokens');
+jest.mock('../../../../../UI/Predict/hooks/usePredictAccountState', () => ({
+ usePredictAccountState: () => ({
+ data: undefined,
+ isLoading: false,
+ }),
+}));
jest.mock('../../../hooks/pay/useTransactionPayAvailableTokens');
jest.mock('../../../hooks/pay/useTransactionPayData');
jest.mock('../../../hooks/pay/useTransactionPayHasSourceAmount');
diff --git a/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.test.ts b/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.test.ts
index 03f6313da56..a740b930b21 100644
--- a/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.test.ts
+++ b/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.test.ts
@@ -6,6 +6,7 @@ import { useTransactionMetadataRequest } from '../transactions/useTransactionMet
import { useTransactionPayWithdraw } from './useTransactionPayWithdraw';
import Engine from '../../../../../core/Engine';
import { computeProxyAddress } from '../../../../UI/Predict/providers/polymarket/safe/utils';
+import { usePredictAccountState } from '../../../../UI/Predict/hooks/usePredictAccountState';
jest.mock('../transactions/useTransactionMetadataRequest');
jest.mock('./useTransactionPayWithdraw');
@@ -19,6 +20,7 @@ jest.mock('../../../../../core/Engine', () => ({
jest.mock('../../../../UI/Predict/providers/polymarket/safe/utils', () => ({
computeProxyAddress: jest.fn(),
}));
+jest.mock('../../../../UI/Predict/hooks/usePredictAccountState');
const TRANSACTION_ID_MOCK = 'transaction-123';
const FROM_MOCK = '0x1234567890123456789012345678901234567890' as Hex;
@@ -32,11 +34,24 @@ describe('useTransactionPayPostQuote', () => {
const setTransactionConfigMock = jest.mocked(
Engine.context.TransactionPayController.setTransactionConfig,
);
+ const usePredictAccountStateMock = jest.mocked(usePredictAccountState);
const computeProxyAddressMock = jest.mocked(computeProxyAddress);
+ function mockAccountState(walletType: 'safe' | 'deposit-wallet'): void {
+ usePredictAccountStateMock.mockReturnValue({
+ data: {
+ address: '0xProxyAddress',
+ isDeployed: true,
+ walletType,
+ },
+ isLoading: false,
+ } as never);
+ }
+
beforeEach(() => {
jest.clearAllMocks();
computeProxyAddressMock.mockReturnValue(PROXY_ADDRESS_MOCK);
+ mockAccountState('safe');
useTransactionPayWithdrawMock.mockReturnValue({
isWithdraw: false,
canSelectWithdrawToken: false,
@@ -263,4 +278,84 @@ describe('useTransactionPayPostQuote', () => {
expect(config.isHyperliquidSource).toBeUndefined();
expect(computeProxyAddressMock).not.toHaveBeenCalled();
});
+
+ describe('Polymarket deposit-wallet predictWithdraw', () => {
+ beforeEach(() => {
+ setTransactionConfigMock.mockReset();
+ useTransactionMetadataRequestMock.mockReturnValue({
+ id: TRANSACTION_ID_MOCK,
+ txParams: { from: FROM_MOCK },
+ type: TransactionType.predictWithdraw,
+ } as never);
+ useTransactionPayWithdrawMock.mockReturnValue({
+ isWithdraw: true,
+ canSelectWithdrawToken: true,
+ });
+ });
+
+ it('flags transaction and skips refundTo when walletType is deposit-wallet', () => {
+ mockAccountState('deposit-wallet');
+
+ renderHook(() => useTransactionPayPostQuote());
+
+ expect(setTransactionConfigMock).toHaveBeenCalledTimes(1);
+
+ const callback = setTransactionConfigMock.mock.calls[0][1];
+ const config = {} as {
+ isPostQuote?: boolean;
+ isPolymarketDepositWallet?: boolean;
+ refundTo?: Hex;
+ };
+ callback(config);
+
+ expect(config.isPostQuote).toBe(true);
+ expect(config.isPolymarketDepositWallet).toBe(true);
+ expect(config.refundTo).toBeUndefined();
+ expect(computeProxyAddressMock).not.toHaveBeenCalled();
+ });
+
+ it('does not set deposit-wallet flag when walletType is safe', () => {
+ mockAccountState('safe');
+
+ renderHook(() => useTransactionPayPostQuote());
+
+ expect(setTransactionConfigMock).toHaveBeenCalledTimes(1);
+
+ const callback = setTransactionConfigMock.mock.calls[0][1];
+ const config = {} as {
+ isPostQuote?: boolean;
+ isPolymarketDepositWallet?: boolean;
+ refundTo?: Hex;
+ };
+ callback(config);
+
+ expect(config.isPolymarketDepositWallet).toBeUndefined();
+ });
+
+ it('defers setTransactionConfig until predict account state resolves', () => {
+ usePredictAccountStateMock.mockReturnValue({
+ data: undefined,
+ isLoading: true,
+ } as never);
+
+ renderHook(() => useTransactionPayPostQuote());
+
+ expect(setTransactionConfigMock).not.toHaveBeenCalled();
+ });
+
+ it('does not resolve account state for non-predictWithdraw flows', () => {
+ useTransactionMetadataRequestMock.mockReturnValue({
+ id: TRANSACTION_ID_MOCK,
+ txParams: { from: FROM_MOCK },
+ type: TransactionType.perpsWithdraw,
+ } as never);
+
+ renderHook(() => useTransactionPayPostQuote());
+
+ expect(usePredictAccountStateMock).toHaveBeenCalledWith({
+ enabled: false,
+ });
+ expect(setTransactionConfigMock).toHaveBeenCalledTimes(1);
+ });
+ });
});
diff --git a/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.ts b/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.ts
index fd8a9ebe0f6..5eb34b2c678 100644
--- a/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.ts
+++ b/app/components/Views/confirmations/hooks/pay/useTransactionPayPostQuote.ts
@@ -6,6 +6,7 @@ import { useTransactionPayWithdraw } from './useTransactionPayWithdraw';
import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest';
import { computeProxyAddress } from '../../../../UI/Predict/providers/polymarket/safe/utils';
import { hasTransactionType } from '../../utils/transaction';
+import { usePredictAccountState } from '../../../../UI/Predict/hooks/usePredictAccountState';
const log = createProjectLogger('transaction-pay-post-quote');
@@ -33,6 +34,16 @@ export function useTransactionPayPostQuote(): void {
const isMoneyAccountWithdraw = hasTransactionType(transactionMeta, [
TransactionType.moneyAccountWithdraw,
]);
+ const isPredictWithdraw = hasTransactionType(transactionMeta, [
+ TransactionType.predictWithdraw,
+ ]);
+
+ const { data: accountState } = usePredictAccountState({
+ enabled: isPredictWithdraw,
+ });
+
+ const isDepositWalletWithdraw =
+ isPredictWithdraw && accountState?.walletType === 'deposit-wallet';
useEffect(() => {
if (
@@ -43,6 +54,10 @@ export function useTransactionPayPostQuote(): void {
return;
}
+ if (isPredictWithdraw && !accountState) {
+ return;
+ }
+
try {
const { TransactionPayController } = Engine.context;
const from = transactionMeta?.txParams?.from as Hex | undefined;
@@ -52,7 +67,7 @@ export function useTransactionPayPostQuote(): void {
// on the user's address directly (HyperCore -> Relay for perps; vault
// teller -> user for money account).
const refundTo =
- isPerpsWithdraw || isMoneyAccountWithdraw
+ isPerpsWithdraw || isMoneyAccountWithdraw || isDepositWalletWithdraw
? undefined
: from
? computeProxyAddress(from)
@@ -68,6 +83,10 @@ export function useTransactionPayPostQuote(): void {
if (isPerpsWithdraw) {
config.isHyperliquidSource = true;
}
+
+ if (isDepositWalletWithdraw) {
+ config.isPolymarketDepositWallet = true;
+ }
});
isSet.current = transactionId;
@@ -77,6 +96,7 @@ export function useTransactionPayPostQuote(): void {
refundTo,
isPerpsWithdraw,
isMoneyAccountWithdraw,
+ isDepositWalletWithdraw,
});
} catch (error) {
log('Error initializing post-quote transaction', {
@@ -85,9 +105,12 @@ export function useTransactionPayPostQuote(): void {
});
}
}, [
+ accountState,
canSelectWithdrawToken,
+ isDepositWalletWithdraw,
isMoneyAccountWithdraw,
isPerpsWithdraw,
+ isPredictWithdraw,
transactionId,
transactionMeta?.txParams?.from,
]);
diff --git a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts
index d811d27d661..6e83cb4a441 100644
--- a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts
+++ b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts
@@ -235,6 +235,7 @@ describe('Transaction Controller Init', () => {
bufferSubsequent: 0.05,
slippage: 0.005,
stxDisabled: false,
+ enableDepositWalletWithdraw: false,
});
payHookClassMock.mockReturnValue({
@@ -454,6 +455,7 @@ describe('Transaction Controller Init', () => {
bufferSubsequent: 0.05,
slippage: 0.005,
stxDisabled: true,
+ enableDepositWalletWithdraw: false,
});
const hooks = testConstructorOption('hooks');
@@ -471,6 +473,7 @@ describe('Transaction Controller Init', () => {
bufferSubsequent: 0.05,
slippage: 0.005,
stxDisabled: false,
+ enableDepositWalletWithdraw: false,
});
const hooks = testConstructorOption('hooks');
diff --git a/app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.test.ts b/app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.test.ts
new file mode 100644
index 00000000000..fc0d99dac25
--- /dev/null
+++ b/app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.test.ts
@@ -0,0 +1,205 @@
+import {
+ SignTypedDataVersion,
+ type PersonalMessageParams,
+ type TypedMessageParams,
+} from '@metamask/keyring-controller';
+import type { Hex } from '@metamask/utils';
+
+import {
+ deriveDepositWalletAddress,
+ executeDepositWalletBatchAndWaitForCompletion,
+} from '../../../../components/UI/Predict/providers/polymarket/depositWallet';
+import type { TransactionPayControllerInitMessenger } from '../../messengers/transaction-pay-controller-messenger';
+import { createPolymarketCallbacks } from './polymarket-callbacks';
+
+jest.mock(
+ '../../../../components/UI/Predict/providers/polymarket/depositWallet',
+);
+
+const EOA_MOCK = '0x1111111111111111111111111111111111111111' as Hex;
+const DEPOSIT_WALLET_MOCK = '0x2222222222222222222222222222222222222222' as Hex;
+const SOURCE_HASH_MOCK = `0x${'aa'.repeat(32)}` as Hex;
+
+const CALLS_MOCK = [
+ {
+ target: '0x3333333333333333333333333333333333333333' as Hex,
+ data: '0x' as Hex,
+ value: '0',
+ },
+];
+
+function buildInitMessenger() {
+ return {
+ call: jest.fn(),
+ } as unknown as jest.Mocked;
+}
+
+describe('createPolymarketCallbacks', () => {
+ const deriveDepositWalletAddressMock = jest.mocked(
+ deriveDepositWalletAddress,
+ );
+ const executeDepositWalletBatchAndWaitForCompletionMock = jest.mocked(
+ executeDepositWalletBatchAndWaitForCompletion,
+ );
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.useFakeTimers();
+ deriveDepositWalletAddressMock.mockReturnValue(DEPOSIT_WALLET_MOCK);
+ executeDepositWalletBatchAndWaitForCompletionMock.mockResolvedValue(
+ SOURCE_HASH_MOCK,
+ );
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ describe('getDepositWalletAddress', () => {
+ it('returns the derived deposit-wallet address for the given EOA', async () => {
+ const callbacks = createPolymarketCallbacks(buildInitMessenger());
+
+ const result = await callbacks.getDepositWalletAddress({ eoa: EOA_MOCK });
+
+ expect(result).toBe(DEPOSIT_WALLET_MOCK);
+ expect(deriveDepositWalletAddressMock).toHaveBeenCalledWith(EOA_MOCK);
+ });
+ });
+
+ describe('submitDepositWalletBatch', () => {
+ it('returns the relayer source hash on success', async () => {
+ const initMessenger = buildInitMessenger();
+ const callbacks = createPolymarketCallbacks(initMessenger);
+
+ const result = await callbacks.submitDepositWalletBatch({
+ eoa: EOA_MOCK,
+ depositWallet: DEPOSIT_WALLET_MOCK,
+ calls: CALLS_MOCK,
+ });
+
+ expect(result).toStrictEqual({ sourceHash: SOURCE_HASH_MOCK });
+ expect(
+ executeDepositWalletBatchAndWaitForCompletionMock,
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ executeDepositWalletBatchAndWaitForCompletionMock.mock.calls[0][0],
+ ).toMatchObject({
+ walletAddress: DEPOSIT_WALLET_MOCK,
+ calls: CALLS_MOCK,
+ signer: expect.objectContaining({ address: EOA_MOCK }),
+ });
+ });
+
+ it('signs typed and personal messages via the init messenger', async () => {
+ const initMessenger = buildInitMessenger();
+ initMessenger.call.mockImplementation(((action: string) => {
+ if (action === 'KeyringController:signTypedMessage') {
+ return Promise.resolve('0xtyped');
+ }
+ if (action === 'KeyringController:signPersonalMessage') {
+ return Promise.resolve('0xpersonal');
+ }
+ return undefined;
+ }) as never);
+ const callbacks = createPolymarketCallbacks(initMessenger);
+
+ await callbacks.submitDepositWalletBatch({
+ eoa: EOA_MOCK,
+ depositWallet: DEPOSIT_WALLET_MOCK,
+ calls: CALLS_MOCK,
+ });
+
+ const signer =
+ executeDepositWalletBatchAndWaitForCompletionMock.mock.calls[0][0]
+ .signer;
+
+ const typedParams = {} as TypedMessageParams;
+ const typedResult = await signer.signTypedMessage(
+ typedParams,
+ SignTypedDataVersion.V4,
+ );
+ const personalParams = {} as PersonalMessageParams;
+ const personalResult = await signer.signPersonalMessage(personalParams);
+
+ expect(typedResult).toBe('0xtyped');
+ expect(personalResult).toBe('0xpersonal');
+ expect(initMessenger.call).toHaveBeenCalledWith(
+ 'KeyringController:signTypedMessage',
+ typedParams,
+ SignTypedDataVersion.V4,
+ );
+ expect(initMessenger.call).toHaveBeenCalledWith(
+ 'KeyringController:signPersonalMessage',
+ personalParams,
+ );
+ });
+
+ it('retries when the relayer reports "wallet busy" and eventually succeeds', async () => {
+ executeDepositWalletBatchAndWaitForCompletionMock
+ .mockRejectedValueOnce(new Error('wallet busy: try again'))
+ .mockRejectedValueOnce(new Error('Wallet Busy: still pending'))
+ .mockResolvedValueOnce(SOURCE_HASH_MOCK);
+
+ const callbacks = createPolymarketCallbacks(buildInitMessenger());
+
+ const promise = callbacks.submitDepositWalletBatch({
+ eoa: EOA_MOCK,
+ depositWallet: DEPOSIT_WALLET_MOCK,
+ calls: CALLS_MOCK,
+ });
+
+ await jest.runAllTimersAsync();
+
+ await expect(promise).resolves.toStrictEqual({
+ sourceHash: SOURCE_HASH_MOCK,
+ });
+ expect(
+ executeDepositWalletBatchAndWaitForCompletionMock,
+ ).toHaveBeenCalledTimes(3);
+ });
+
+ it('rethrows non wallet-busy errors immediately without retrying', async () => {
+ executeDepositWalletBatchAndWaitForCompletionMock.mockRejectedValue(
+ new Error('relayer rejected'),
+ );
+
+ const callbacks = createPolymarketCallbacks(buildInitMessenger());
+
+ await expect(
+ callbacks.submitDepositWalletBatch({
+ eoa: EOA_MOCK,
+ depositWallet: DEPOSIT_WALLET_MOCK,
+ calls: CALLS_MOCK,
+ }),
+ ).rejects.toThrow('relayer rejected');
+
+ expect(
+ executeDepositWalletBatchAndWaitForCompletionMock,
+ ).toHaveBeenCalledTimes(1);
+ });
+
+ it('gives up after exhausting wallet-busy retries', async () => {
+ executeDepositWalletBatchAndWaitForCompletionMock.mockRejectedValue(
+ new Error('wallet busy: persistent'),
+ );
+
+ const callbacks = createPolymarketCallbacks(buildInitMessenger());
+
+ const promise = callbacks.submitDepositWalletBatch({
+ eoa: EOA_MOCK,
+ depositWallet: DEPOSIT_WALLET_MOCK,
+ calls: CALLS_MOCK,
+ });
+
+ const captured = promise.catch((error) => error);
+ await jest.runAllTimersAsync();
+ const error = await captured;
+
+ expect(error).toBeInstanceOf(Error);
+ expect((error as Error).message).toBe('wallet busy: persistent');
+ expect(
+ executeDepositWalletBatchAndWaitForCompletionMock,
+ ).toHaveBeenCalledTimes(5);
+ });
+ });
+});
diff --git a/app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.ts b/app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.ts
new file mode 100644
index 00000000000..b4aa6cb1798
--- /dev/null
+++ b/app/core/Engine/controllers/transaction-pay-controller/polymarket-callbacks.ts
@@ -0,0 +1,101 @@
+import {
+ SignTypedDataVersion,
+ type PersonalMessageParams,
+ type TypedMessageParams,
+} from '@metamask/keyring-controller';
+import type { PolymarketCallbacks } from '@metamask/transaction-pay-controller';
+import type { Hex } from '@metamask/utils';
+
+import {
+ deriveDepositWalletAddress,
+ executeDepositWalletBatchAndWaitForCompletion,
+} from '../../../../components/UI/Predict/providers/polymarket/depositWallet';
+import type { Signer } from '../../../../components/UI/Predict/providers/types';
+import type { TransactionPayControllerInitMessenger } from '../../messengers/transaction-pay-controller-messenger';
+
+const WALLET_BUSY_RETRY_ATTEMPTS = 5;
+const WALLET_BUSY_RETRY_DELAY_MS = 3000;
+const WALLET_BUSY_ERROR_MARKER = 'wallet busy';
+
+export function createPolymarketCallbacks(
+ initMessenger: TransactionPayControllerInitMessenger,
+): PolymarketCallbacks {
+ return {
+ getDepositWalletAddress: ({ eoa }) => getDepositWalletAddress(eoa),
+
+ submitDepositWalletBatch: ({ eoa, depositWallet, calls }) =>
+ submitDepositWalletBatch(initMessenger, { eoa, depositWallet, calls }),
+ };
+}
+
+async function getDepositWalletAddress(eoa: Hex): Promise {
+ return deriveDepositWalletAddress(eoa) as Hex;
+}
+
+async function submitDepositWalletBatch(
+ initMessenger: TransactionPayControllerInitMessenger,
+ {
+ eoa,
+ depositWallet,
+ calls,
+ }: {
+ eoa: Hex;
+ depositWallet: Hex;
+ calls: { target: Hex; data: Hex; value: string }[];
+ },
+): Promise<{ sourceHash: Hex }> {
+ return withWalletBusyRetry(async () => {
+ const sourceHash = await executeDepositWalletBatchAndWaitForCompletion({
+ signer: createSigner(initMessenger, eoa),
+ walletAddress: depositWallet,
+ calls,
+ });
+ return { sourceHash };
+ });
+}
+
+function createSigner(
+ initMessenger: TransactionPayControllerInitMessenger,
+ address: Hex,
+): Signer {
+ return {
+ address,
+ signTypedMessage: (
+ params: TypedMessageParams,
+ version: SignTypedDataVersion,
+ ) =>
+ initMessenger.call('KeyringController:signTypedMessage', params, version),
+ signPersonalMessage: (params: PersonalMessageParams) =>
+ initMessenger.call('KeyringController:signPersonalMessage', params),
+ };
+}
+
+async function withWalletBusyRetry(action: () => Promise): Promise {
+ let lastError: unknown;
+ for (let attempt = 0; attempt < WALLET_BUSY_RETRY_ATTEMPTS; attempt++) {
+ try {
+ return await action();
+ } catch (error) {
+ lastError = error;
+ if (
+ !isWalletBusyError(error) ||
+ attempt === WALLET_BUSY_RETRY_ATTEMPTS - 1
+ ) {
+ throw error;
+ }
+ await sleep(WALLET_BUSY_RETRY_DELAY_MS);
+ }
+ }
+ throw lastError;
+}
+
+function isWalletBusyError(error: unknown): boolean {
+ if (!(error instanceof Error)) {
+ return false;
+ }
+ return error.message.toLowerCase().includes(WALLET_BUSY_ERROR_MARKER);
+}
+
+function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
diff --git a/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.test.ts b/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.test.ts
index 79bcbeb8628..b0c5c2b542b 100644
--- a/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.test.ts
+++ b/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.test.ts
@@ -9,8 +9,10 @@ import {
} from '@metamask/transaction-pay-controller';
import { TransactionPayControllerInit } from './transaction-pay-controller-init';
import { TransactionPayControllerInitMessenger } from '../../messengers/transaction-pay-controller-messenger';
+import { createPolymarketCallbacks } from './polymarket-callbacks';
jest.mock('@metamask/transaction-pay-controller');
+jest.mock('./polymarket-callbacks');
function buildInitRequestMock(
initRequestProperties: Record = {},
@@ -99,4 +101,20 @@ describe('Transaction Pay Controller Init', () => {
expect(getStrategy).toBeUndefined();
expect(getStrategies).toBeUndefined();
});
+
+ it('wires Polymarket callbacks into the controller', () => {
+ const polymarketCallbacksMock = { __polymarketCallbacks: true };
+ jest
+ .mocked(createPolymarketCallbacks)
+ .mockReturnValue(
+ polymarketCallbacksMock as unknown as ReturnType<
+ typeof createPolymarketCallbacks
+ >,
+ );
+
+ const polymarket = testConstructorOption('polymarket');
+
+ expect(createPolymarketCallbacks).toHaveBeenCalledTimes(1);
+ expect(polymarket).toBe(polymarketCallbacksMock);
+ });
});
diff --git a/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.ts b/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.ts
index b42d20c38a4..9fededcc906 100644
--- a/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.ts
+++ b/app/core/Engine/controllers/transaction-pay-controller/transaction-pay-controller-init.ts
@@ -6,6 +6,7 @@ import {
} from '@metamask/transaction-pay-controller';
import { TransactionPayControllerInitMessenger } from '../../messengers/transaction-pay-controller-messenger';
import { getDelegationTransaction } from '../../../../util/transactions/delegation';
+import { createPolymarketCallbacks } from './polymarket-callbacks';
export const TransactionPayControllerInit: MessengerClientInitFunction<
TransactionPayController,
@@ -19,6 +20,7 @@ export const TransactionPayControllerInit: MessengerClientInitFunction<
getDelegationTransaction: ({ transaction }) =>
getDelegationTransaction(initMessenger, transaction),
messenger: controllerMessenger,
+ polymarket: createPolymarketCallbacks(initMessenger),
state: persistedState.TransactionPayController,
});
diff --git a/app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts b/app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts
index 9d029afd548..ceaceb669fd 100644
--- a/app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts
+++ b/app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts
@@ -47,6 +47,8 @@ import {
TransactionPayControllerGetDelegationTransactionAction,
TransactionPayControllerGetStateAction,
TransactionPayControllerGetStrategyAction,
+ TransactionPayControllerPolymarketGetDepositWalletAddressAction,
+ TransactionPayControllerPolymarketSubmitDepositWalletBatchAction,
} from '@metamask/transaction-pay-controller';
import { RootMessenger } from '../../types';
import { AnalyticsControllerActions } from '@metamask/analytics-controller';
@@ -118,6 +120,8 @@ type InitMessengerActions =
| TransactionPayControllerGetDelegationTransactionAction
| TransactionPayControllerGetStateAction
| TransactionPayControllerGetStrategyAction
+ | TransactionPayControllerPolymarketGetDepositWalletAddressAction
+ | TransactionPayControllerPolymarketSubmitDepositWalletBatchAction
| AnalyticsControllerActions
| PredictControllerBeforePublishAction
| PredictControllerPublishAction;
@@ -178,6 +182,8 @@ export function getTransactionControllerInitMessenger(
'TransactionPayController:getDelegationTransaction',
'TransactionPayController:getState',
'TransactionPayController:getStrategy',
+ 'TransactionPayController:polymarketGetDepositWalletAddress',
+ 'TransactionPayController:polymarketSubmitDepositWalletBatch',
'AnalyticsController:trackEvent',
'PredictController:beforePublish',
'PredictController:publish',
diff --git a/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts b/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts
index 62f26d46b5c..52ad9f30c6d 100644
--- a/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts
+++ b/app/core/Engine/messengers/transaction-pay-controller-messenger/transaction-pay-controller-messenger.ts
@@ -6,7 +6,11 @@ import {
MessengerEvents,
} from '@metamask/messenger';
import { DelegationControllerSignDelegationAction } from '@metamask/delegation-controller';
-import { KeyringControllerSignEip7702AuthorizationAction } from '@metamask/keyring-controller';
+import {
+ KeyringControllerSignEip7702AuthorizationAction,
+ KeyringControllerSignPersonalMessageAction,
+ KeyringControllerSignTypedMessageAction,
+} from '@metamask/keyring-controller';
export function getTransactionPayControllerMessenger(
rootMessenger: RootMessenger,
@@ -59,7 +63,9 @@ export function getTransactionPayControllerMessenger(
type InitMessengerActions =
| DelegationControllerSignDelegationAction
- | KeyringControllerSignEip7702AuthorizationAction;
+ | KeyringControllerSignEip7702AuthorizationAction
+ | KeyringControllerSignPersonalMessageAction
+ | KeyringControllerSignTypedMessageAction;
type InitMessengerEvents = never;
export type TransactionPayControllerInitMessenger = ReturnType<
@@ -83,6 +89,8 @@ export function getTransactionPayControllerInitMessenger(
actions: [
'DelegationController:signDelegation',
'KeyringController:signEip7702Authorization',
+ 'KeyringController:signPersonalMessage',
+ 'KeyringController:signTypedMessage',
],
events: [],
messenger,
diff --git a/app/selectors/featureFlagController/confirmations/index.test.ts b/app/selectors/featureFlagController/confirmations/index.test.ts
index dd7c7906f8d..f2f79b55792 100644
--- a/app/selectors/featureFlagController/confirmations/index.test.ts
+++ b/app/selectors/featureFlagController/confirmations/index.test.ts
@@ -16,6 +16,7 @@ import {
PAY_FIAT_ENABLED_TRANSACTION_TYPES,
PAY_FIAT_MAX_DELAY_MINUTES_FOR_PAYMENT_METHODS,
selectMetaMaskPayHardwareFlags,
+ PAY_ENABLE_DEPOSIT_WALLET_WITHDRAW_DEFAULT,
PAY_HARDWARE_ENABLED_DEFAULT,
PreferredToken,
getPreferredTokensForTransactionType,
@@ -563,3 +564,23 @@ describe('selectMetaMaskPayHardwareFlags', () => {
expect(selectMetaMaskPayHardwareFlags(state)).toEqual({ enabled: true });
});
});
+
+describe('selectMetaMaskPayFlags extended flags', () => {
+ it('returns default enableDepositWalletWithdraw when flag is absent', () => {
+ expect(
+ selectMetaMaskPayFlags(mockedEmptyFlagsState).enableDepositWalletWithdraw,
+ ).toEqual(PAY_ENABLE_DEPOSIT_WALLET_WITHDRAW_DEFAULT);
+ });
+
+ it('returns enableDepositWalletWithdraw from flag value', () => {
+ const state = cloneDeep(mockedEmptyFlagsState);
+ state.engine.backgroundState.RemoteFeatureFlagController.remoteFeatureFlags =
+ {
+ confirmations_pay_extended: { enableDepositWalletWithdraw: true },
+ };
+
+ expect(selectMetaMaskPayFlags(state).enableDepositWalletWithdraw).toEqual(
+ true,
+ );
+ });
+});
diff --git a/app/selectors/featureFlagController/confirmations/index.ts b/app/selectors/featureFlagController/confirmations/index.ts
index ba5e16f951d..f1915b20bdf 100644
--- a/app/selectors/featureFlagController/confirmations/index.ts
+++ b/app/selectors/featureFlagController/confirmations/index.ts
@@ -11,6 +11,7 @@ export const BUFFER_SUBSEQUENT_DEFAULT = 0.05;
export const PAY_FIAT_ENABLED_TRANSACTION_TYPES = [];
export const PAY_FIAT_MAX_DELAY_MINUTES_FOR_PAYMENT_METHODS = 10;
export const PAY_HARDWARE_ENABLED_DEFAULT = false;
+export const PAY_ENABLE_DEPOSIT_WALLET_WITHDRAW_DEFAULT = false;
export const SLIPPAGE_DEFAULT = 0.005;
export const STX_DISABLED_DEFAULT = false;
@@ -49,6 +50,10 @@ export interface MetaMaskPayFlags {
stxDisabled: boolean;
}
+export interface MetaMaskPayExtendedFlags {
+ enableDepositWalletWithdraw: boolean;
+}
+
export interface MetaMaskPayTokensFlags {
preferredTokens: PreferredTokensConfig;
blockedTokens: BlockedTokensConfig;
@@ -88,11 +93,16 @@ export interface MetaMaskPayHardwareFlags {
export const selectMetaMaskPayFlags = createSelector(
selectRemoteFeatureFlags,
- (featureFlags): MetaMaskPayFlags => {
+ (featureFlags): MetaMaskPayFlags & MetaMaskPayExtendedFlags => {
const metaMaskPayFlags = featureFlags?.confirmations_pay as
| Record
| undefined;
+ const metaMaskPayExtendedFlags =
+ featureFlags?.confirmations_pay_extended as
+ | Record
+ | undefined;
+
const attemptsMax =
(metaMaskPayFlags?.attemptsMax as number) ?? ATTEMPTS_MAX_DEFAULT;
@@ -111,6 +121,10 @@ export const selectMetaMaskPayFlags = createSelector(
const stxDisabled =
(metaMaskPayFlags?.stxDisabled as boolean) ?? STX_DISABLED_DEFAULT;
+ const enableDepositWalletWithdraw =
+ (metaMaskPayExtendedFlags?.enableDepositWalletWithdraw as boolean) ??
+ PAY_ENABLE_DEPOSIT_WALLET_WITHDRAW_DEFAULT;
+
return {
attemptsMax,
bufferInitial,
@@ -118,6 +132,7 @@ export const selectMetaMaskPayFlags = createSelector(
bufferSubsequent,
slippage,
stxDisabled,
+ enableDepositWalletWithdraw,
};
},
);
diff --git a/locales/languages/en.json b/locales/languages/en.json
index 8e99265ab3c..99a4494c2ba 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -8911,6 +8911,7 @@
"musd_claim": "Claim mUSD",
"perps_deposit": "Add funds",
"perps_withdraw": "Withdrawal",
+ "predict_withdraw": "Withdraw {{sourceSymbol}} from {{sourceChain}}",
"predict_deposit": "Add funds",
"swap": "Swap tokens",
"swap_approval": "Approve tokens",
diff --git a/package.json b/package.json
index b00ebf64351..2d1c08419dc 100644
--- a/package.json
+++ b/package.json
@@ -349,7 +349,7 @@
"@metamask/superstruct": "^3.2.1",
"@metamask/swappable-obj-proxy": "^2.1.0",
"@metamask/transaction-controller": "^65.4.0",
- "@metamask/transaction-pay-controller": "^22.4.0",
+ "@metamask/transaction-pay-controller": "^22.5.0",
"@metamask/tron-wallet-snap": "^1.25.3",
"@metamask/utils": "^11.11.0",
"@myx-trade/sdk": "^0.1.265",
diff --git a/yarn.lock b/yarn.lock
index ffa65483b04..11c9ccab726 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10427,9 +10427,9 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/transaction-pay-controller@npm:^22.4.0":
- version: 22.4.0
- resolution: "@metamask/transaction-pay-controller@npm:22.4.0"
+"@metamask/transaction-pay-controller@npm:^22.5.0":
+ version: 22.5.0
+ resolution: "@metamask/transaction-pay-controller@npm:22.5.0"
dependencies:
"@ethersproject/abi": "npm:^5.7.0"
"@ethersproject/contracts": "npm:^5.7.0"
@@ -10452,7 +10452,7 @@ __metadata:
bn.js: "npm:^5.2.1"
immer: "npm:^9.0.6"
lodash: "npm:^4.17.21"
- checksum: 10/3de949b9525531bedcbc7fb24ab3470f38f014f3239b1e32d47654a2d25bffa67aab6ad3da39784a6321fdf36757a7f9c0e85946dfb33bb53d00a2a8ba728d34
+ checksum: 10/611103e0f4a8c2783f8196302830d9befee8973f64e0fdb050f658de3519c6dc01c1bf104788b2d726e8f22e22ed529ac19b7b3e9d53aea945e6676d71bef7e2
languageName: node
linkType: hard
@@ -35442,7 +35442,7 @@ __metadata:
"@metamask/test-dapp-multichain": "npm:^0.17.1"
"@metamask/test-dapp-solana": "npm:^0.3.0"
"@metamask/transaction-controller": "npm:^65.4.0"
- "@metamask/transaction-pay-controller": "npm:^22.4.0"
+ "@metamask/transaction-pay-controller": "npm:^22.5.0"
"@metamask/tron-wallet-snap": "npm:^1.25.3"
"@metamask/utils": "npm:^11.11.0"
"@myx-trade/sdk": "npm:^0.1.265"