Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
689ff1c
feat(predict): route deposit-wallet withdraws through PolymarketBridg…
matthewwalsh0 May 11, 2026
a2f2225
fix(predict): bypass gasFeeToken for deposit-wallet withdraws
matthewwalsh0 May 11, 2026
f0c2b9b
feat(predict): render predictWithdraw in activity transaction details
matthewwalsh0 May 12, 2026
0bb4cec
feat(predict): wire Polymarket relayer callbacks into TransactionPayC…
matthewwalsh0 May 12, 2026
51c4b07
fix(predict): delegate polymarket pay-controller actions to Transacti…
matthewwalsh0 May 12, 2026
2c2c928
fix(predict): match Polymarket relayer 'wallet busy' error marker
matthewwalsh0 May 13, 2026
716f7b2
chore(predict): pin transaction-pay-controller preview build after re…
matthewwalsh0 May 14, 2026
662a77a
refactor(predict): extract gasFeeToken variable in prepareWithdraw
matthewwalsh0 May 14, 2026
88a8fad
refactor(predict): isolate deposit-wallet follow-up in local async fu…
matthewwalsh0 May 14, 2026
d4afb25
test(predict): cover Polymarket callbacks and controller wiring
matthewwalsh0 May 14, 2026
d708ee2
test(predict): cover predictWithdraw summary lines and dedupe selector
matthewwalsh0 May 14, 2026
3b6a075
chore(predict): bump transaction-pay-controller to ^22.5.0
matthewwalsh0 May 14, 2026
fbfd515
refactor(predict): clean gasFeeToken comment and extract deposit-wall…
matthewwalsh0 May 14, 2026
e352c38
style(predict): prettier-format source-hash-summary-line
matthewwalsh0 May 14, 2026
17273cc
test(predict): fix lint:tsc type errors in new test files
matthewwalsh0 May 14, 2026
5070c06
fix(predict): drop gasFeeToken type annotation and conditional spread
matthewwalsh0 May 14, 2026
a6411d6
feat(predict): add enableDepositWalletWithdraw flag to gate withdraw …
matthewwalsh0 May 14, 2026
d9d0d29
refactor(predict): read deposit-wallet state via usePredictAccountState
matthewwalsh0 May 14, 2026
15c5d48
style(predict): drop stray blank line in transactionController.test.ts
matthewwalsh0 May 14, 2026
054697c
test(predict): cover predictWithdraw in SUMMARY_SECTION_TYPES and moc…
matthewwalsh0 May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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(
<PredictBalance
onDepositWalletWithdrawPress={mockOnDepositWalletWithdrawPress}
/>,
{
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -55,6 +56,7 @@ const PredictBalance: React.FC<PredictBalanceProps> = ({
}) => {
const tw = useTailwind();
const privacyMode = useSelector(selectPrivacyMode);
const { enableDepositWalletWithdraw } = useSelector(selectMetaMaskPayFlags);

const navigation =
useNavigation<NavigationProp<PredictNavigationParamList>>();
Expand Down Expand Up @@ -103,15 +105,18 @@ const PredictBalance: React.FC<PredictBalanceProps> = ({
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 (
Expand Down
53 changes: 53 additions & 0 deletions app/components/UI/Predict/controllers/PredictController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ describe('PredictController', () => {
syncDepositWalletBalanceAllowanceForDepositTransaction: jest.fn(),
} as unknown as jest.Mocked<PolymarketProvider>;

mockPolymarketProvider.getAccountState.mockResolvedValue({
address: '0xProxyAddress' as `0x${string}`,
isDeployed: true,
walletType: 'safe' as const,
});
mockPolymarketProvider.beforePublishDepositWalletDeposit.mockResolvedValue(
true,
);
Expand Down Expand Up @@ -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';

Expand Down
13 changes: 11 additions & 2 deletions app/components/UI/Predict/controllers/PredictController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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'),
Expand Down Expand Up @@ -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<TransactionMeta>);

expect(
getByText(
strings('transaction_details.summary_title.bridge_receive', {
targetSymbol: 'USDC',
targetChain: 'Ethereum',
}),
),
).toBeDefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
]);
Expand All @@ -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');

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -31,19 +34,19 @@ jest.mock('@react-navigation/native', () => ({
}),
}));

function render() {
function render(parentTransaction?: Partial<TransactionMeta>) {
return renderWithProvider(
<SourceHashSummaryLine
parentTransaction={
{
(parentTransaction ?? {
id: 'parent-id',
chainId: '0x1',
submittedTime: 1755719285723,
metamaskPay: {
tokenAddress: '0x123',
chainId: '0x1',
},
} as unknown as TransactionMeta
}) as unknown as TransactionMeta
}
sourceHash={'0xabc' as Hex}
/>,
Expand Down Expand Up @@ -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<TransactionMeta>);

expect(
getByText(
strings('transaction_details.summary_title.predict_withdraw', {
sourceSymbol: 'pUSD',
sourceChain: 'Polygon',
}),
),
).toBeDefined();
});
});
Loading
Loading