Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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