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"