Skip to content

Commit bcf7c34

Browse files
feat: Enable conditional gasless deposits for Perps and Predict (MetaMask#24290)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Gate gas fee token usage for Perps (Arbitrum USDC) and Predict (Polygon USDC.e). Add shared helper for gas-fee-token eligibility and update controller tests. ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: ## **Related issues** Fixes: MetaMask/MetaMask-planning#6352 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Implements chain/token-gated gasless-style deposits by wiring `gasFeeToken` into submission paths where eligible. > > - Perps: In `PerpsController.depositWithConfirmation`, set `gasFeeToken` when `assetChainId` is `ARBITRUM_MAINNET_CHAIN_ID_HEX` and `transaction.to` equals `USDC_ARBITRUM_MAINNET_ADDRESS`; pass to `TransactionController.addTransaction` (skips initial gas estimate). Added `Hex` and chain/token constants. > - Predict: `PolymarketProvider.prepareDeposit` now returns `gasFeeToken` (Polygon only, set to collateral) and `PredictController.depositWithConfirmation` forwards it to `addTransactionBatch`. > - Types: Extend `PrepareDepositResponse` to include optional `gasFeeToken`. > - Tests: Update Perps/Predict controller tests to mock `estimateGas/estimateGasFee`, account balances, and assert `gasFeeToken` is set when conditions match (and `undefined` otherwise). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 894892b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 75f64d7 commit bcf7c34

6 files changed

Lines changed: 165 additions & 4 deletions

File tree

app/components/UI/Perps/controllers/PerpsController.test.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import {
1313
type PerpsControllerMessenger,
1414
} from './PerpsController';
1515
import { PERPS_ERROR_CODES } from './perpsErrorCodes';
16+
import {
17+
GasFeeEstimateLevel,
18+
GasFeeEstimateType,
19+
} from '@metamask/transaction-controller';
1620
import type { IPerpsProvider } from './types';
1721
import { HyperLiquidProvider } from './providers/HyperLiquidProvider';
1822
import { createMockHyperLiquidProvider } from '../__mocks__/providerMocks';
@@ -23,6 +27,10 @@ import { MarketDataService } from './services/MarketDataService';
2327
import { TradingService } from './services/TradingService';
2428
import { AccountService } from './services/AccountService';
2529
import { DataLakeService } from './services/DataLakeService';
30+
import {
31+
ARBITRUM_MAINNET_CHAIN_ID_HEX,
32+
USDC_ARBITRUM_MAINNET_ADDRESS,
33+
} from '../constants/hyperLiquidConfig';
2634
import Engine from '../../../../core/Engine';
2735

2836
jest.mock('./providers/HyperLiquidProvider');
@@ -104,11 +112,23 @@ jest.mock('../../../../core/Engine', () => {
104112
]),
105113
};
106114

115+
const mockTransactionController = {
116+
estimateGasFee: jest.fn(),
117+
estimateGas: jest.fn(),
118+
};
119+
120+
const mockAccountTrackerController = {
121+
state: {
122+
accountsByChainId: {},
123+
},
124+
};
125+
107126
const mockEngineContext = {
108127
RewardsController: mockRewardsController,
109128
NetworkController: mockNetworkController,
110129
AccountTreeController: mockAccountTreeController,
111-
TransactionController: {},
130+
TransactionController: mockTransactionController,
131+
AccountTrackerController: mockAccountTrackerController,
112132
};
113133

114134
// Return as default export to match the actual Engine import
@@ -2213,6 +2233,7 @@ describe('PerpsController', () => {
22132233
to: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
22142234
value: '0x0',
22152235
data: '0x',
2236+
gas: '0x186a0',
22162237
};
22172238

22182239
const mockDepositId = 'deposit-123';
@@ -2234,6 +2255,34 @@ describe('PerpsController', () => {
22342255
.fn()
22352256
.mockReturnValue(mockNetworkClientId);
22362257

2258+
Engine.context.TransactionController.estimateGasFee = jest
2259+
.fn()
2260+
.mockResolvedValue({
2261+
estimates: {
2262+
type: GasFeeEstimateType.FeeMarket,
2263+
[GasFeeEstimateLevel.Low]: {
2264+
maxFeePerGas: '0x3b9aca00',
2265+
maxPriorityFeePerGas: '0x1',
2266+
},
2267+
[GasFeeEstimateLevel.Medium]: {
2268+
maxFeePerGas: '0x3b9aca00',
2269+
maxPriorityFeePerGas: '0x1',
2270+
},
2271+
[GasFeeEstimateLevel.High]: {
2272+
maxFeePerGas: '0x3b9aca00',
2273+
maxPriorityFeePerGas: '0x1',
2274+
},
2275+
},
2276+
});
2277+
2278+
Engine.context.AccountTrackerController.state.accountsByChainId = {
2279+
[mockAssetChainId]: {
2280+
[mockTransaction.from.toLowerCase()]: {
2281+
balance: '0xde0b6b3a7640000',
2282+
},
2283+
},
2284+
};
2285+
22372286
// Mock TransactionController with promise-based result
22382287
Engine.context.TransactionController.addTransaction = jest
22392288
.fn()
@@ -2248,6 +2297,7 @@ describe('PerpsController', () => {
22482297
delete (Engine.context.NetworkController as any)
22492298
.findNetworkClientIdByChainId;
22502299
delete (Engine.context.TransactionController as any).addTransaction;
2300+
delete (Engine.context.TransactionController as any).estimateGasFee;
22512301
jest.clearAllMocks();
22522302
});
22532303

@@ -2297,9 +2347,50 @@ describe('PerpsController', () => {
22972347
origin: 'metamask',
22982348
type: 'perpsDeposit',
22992349
skipInitialGasEstimate: true,
2350+
gasFeeToken: undefined,
23002351
});
23012352
});
23022353

2354+
it('adds gasFeeToken for Arbitrum USDC deposits', async () => {
2355+
markControllerAsInitialized();
2356+
controller.testSetProviders(new Map([['hyperliquid', mockProvider]]));
2357+
2358+
Engine.context.AccountTrackerController.state.accountsByChainId = {
2359+
[ARBITRUM_MAINNET_CHAIN_ID_HEX]: {
2360+
[mockTransaction.from.toLowerCase()]: {
2361+
balance: '0x0',
2362+
},
2363+
},
2364+
};
2365+
2366+
jest.spyOn(DepositService, 'prepareTransaction').mockResolvedValueOnce({
2367+
transaction: {
2368+
...mockTransaction,
2369+
to: USDC_ARBITRUM_MAINNET_ADDRESS,
2370+
},
2371+
assetChainId: ARBITRUM_MAINNET_CHAIN_ID_HEX,
2372+
currentDepositId: mockDepositId,
2373+
});
2374+
2375+
await controller.depositWithConfirmation('100');
2376+
2377+
expect(
2378+
Engine.context.TransactionController.addTransaction,
2379+
).toHaveBeenCalledWith(
2380+
{
2381+
...mockTransaction,
2382+
to: USDC_ARBITRUM_MAINNET_ADDRESS,
2383+
},
2384+
{
2385+
networkClientId: mockNetworkClientId,
2386+
origin: 'metamask',
2387+
type: 'perpsDeposit',
2388+
skipInitialGasEstimate: true,
2389+
gasFeeToken: USDC_ARBITRUM_MAINNET_ADDRESS,
2390+
},
2391+
);
2392+
});
2393+
23032394
it('throws error when controller not initialized', async () => {
23042395
controller.testSetInitialized(false);
23052396

app/components/UI/Perps/controllers/PerpsController.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ import {
1313
TransactionControllerTransactionSubmittedEvent,
1414
TransactionType,
1515
} from '@metamask/transaction-controller';
16+
import { Hex } from '@metamask/utils';
1617
import Engine from '../../../../core/Engine';
17-
import { USDC_SYMBOL } from '../constants/hyperLiquidConfig';
18+
import {
19+
ARBITRUM_MAINNET_CHAIN_ID_HEX,
20+
USDC_ARBITRUM_MAINNET_ADDRESS,
21+
USDC_SYMBOL,
22+
} from '../constants/hyperLiquidConfig';
1823
import {
1924
LastTransactionResult,
2025
TransactionStatus,
@@ -1380,6 +1385,14 @@ export class PerpsController extends BaseController<
13801385
const networkClientId =
13811386
NetworkController.findNetworkClientIdByChainId(assetChainId);
13821387

1388+
const gasFeeToken =
1389+
transaction.to &&
1390+
assetChainId.toLowerCase() === ARBITRUM_MAINNET_CHAIN_ID_HEX &&
1391+
transaction.to.toLowerCase() ===
1392+
USDC_ARBITRUM_MAINNET_ADDRESS.toLowerCase()
1393+
? (transaction.to as Hex)
1394+
: undefined;
1395+
13831396
// addTransaction shows the confirmation screen and returns a promise
13841397
// The promise will resolve when transaction completes or reject if cancelled/failed
13851398
const { result, transactionMeta } =
@@ -1388,6 +1401,7 @@ export class PerpsController extends BaseController<
13881401
origin: 'metamask',
13891402
type: TransactionType.perpsDeposit,
13901403
skipInitialGasEstimate: true,
1404+
gasFeeToken,
13911405
});
13921406

13931407
// Store the transaction ID and try to get amount from transaction

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ import {
77
type MessengerEvents,
88
type MockAnyNamespace,
99
} from '@metamask/messenger';
10+
import {
11+
GasFeeEstimateLevel,
12+
GasFeeEstimateType,
13+
} from '@metamask/transaction-controller';
1014
import type { NetworkState } from '@metamask/network-controller';
1115
import type { InternalAccount } from '@metamask/keyring-internal-api';
16+
import type { Hex } from '@metamask/utils';
1217

1318
import Engine from '../../../../core/Engine';
1419
import DevLogger from '../../../../core/SDKConnect/utils/DevLogger';
@@ -17,6 +22,7 @@ import {
1722
addTransactionBatch,
1823
} from '../../../../util/transaction-controller';
1924
import { PolymarketProvider } from '../providers/polymarket/PolymarketProvider';
25+
import { MATIC_CONTRACTS } from '../providers/polymarket/constants';
2026
import type { OrderPreview } from '../providers/types';
2127
import {
2228
PredictBalance,
@@ -79,6 +85,15 @@ jest.mock('../../../../core/Engine', () => ({
7985
},
8086
}),
8187
},
88+
TransactionController: {
89+
estimateGas: jest.fn(),
90+
estimateGasFee: jest.fn(),
91+
},
92+
AccountTrackerController: {
93+
state: {
94+
accountsByChainId: {},
95+
},
96+
},
8297
RemoteFeatureFlagController: {
8398
state: {
8499
remoteFeatureFlags: {
@@ -2585,12 +2600,44 @@ describe('PredictController', () => {
25852600
mockPolymarketProvider.prepareDeposit.mockResolvedValue({
25862601
transactions: mockTransactions,
25872602
chainId: mockChainId,
2603+
gasFeeToken: MATIC_CONTRACTS.collateral as Hex,
25882604
});
25892605

25902606
(addTransactionBatch as jest.Mock).mockResolvedValue({
25912607
batchId: mockBatchId,
25922608
});
25932609

2610+
Engine.context.AccountTrackerController.state.accountsByChainId = {
2611+
[mockChainId]: {
2612+
'0x1234567890123456789012345678901234567890': {
2613+
balance: '0x0',
2614+
},
2615+
},
2616+
};
2617+
2618+
Engine.context.TransactionController.estimateGas = jest
2619+
.fn()
2620+
.mockResolvedValue({ gas: '0x5208' });
2621+
Engine.context.TransactionController.estimateGasFee = jest
2622+
.fn()
2623+
.mockResolvedValue({
2624+
estimates: {
2625+
type: GasFeeEstimateType.FeeMarket,
2626+
[GasFeeEstimateLevel.Low]: {
2627+
maxFeePerGas: '0x3b9aca00',
2628+
maxPriorityFeePerGas: '0x1',
2629+
},
2630+
[GasFeeEstimateLevel.Medium]: {
2631+
maxFeePerGas: '0x3b9aca00',
2632+
maxPriorityFeePerGas: '0x1',
2633+
},
2634+
[GasFeeEstimateLevel.High]: {
2635+
maxFeePerGas: '0x3b9aca00',
2636+
maxPriorityFeePerGas: '0x1',
2637+
},
2638+
},
2639+
});
2640+
25942641
await withController(async ({ controller }) => {
25952642
// When calling depositWithConfirmation
25962643
const result = await controller.depositWithConfirmation({
@@ -2621,6 +2668,7 @@ describe('PredictController', () => {
26212668
disableUpgrade: true,
26222669
skipInitialGasEstimate: true,
26232670
transactions: mockTransactions,
2671+
gasFeeToken: MATIC_CONTRACTS.collateral,
26242672
});
26252673
});
26262674
});

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1743,7 +1743,7 @@ export class PredictController extends BaseController<
17431743
throw new Error('Deposit preparation returned undefined');
17441744
}
17451745

1746-
const { transactions, chainId } = depositPreparation;
1746+
const { transactions, chainId, gasFeeToken } = depositPreparation;
17471747

17481748
if (!transactions || transactions.length === 0) {
17491749
throw new Error('No transactions returned from deposit preparation');
@@ -1776,6 +1776,7 @@ export class PredictController extends BaseController<
17761776
disableUpgrade: true,
17771777
skipInitialGasEstimate: true,
17781778
transactions,
1779+
gasFeeToken,
17791780
});
17801781

17811782
if (!batchResult?.batchId) {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1419,9 +1419,15 @@ export class PolymarketProvider implements PredictProvider {
14191419
type: TransactionType.predictDeposit,
14201420
});
14211421

1422+
const chainId = CHAIN_IDS.POLYGON;
1423+
const isPolygonChain =
1424+
chainId.toLowerCase() ===
1425+
numberToHex(POLYGON_MAINNET_CHAIN_ID).toLowerCase();
1426+
14221427
return {
1423-
chainId: CHAIN_IDS.POLYGON,
1428+
chainId,
14241429
transactions,
1430+
gasFeeToken: isPolygonChain ? (collateral as Hex) : undefined,
14251431
};
14261432
}
14271433

app/components/UI/Predict/providers/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ export interface PrepareDepositResponse {
167167
};
168168
type?: TransactionType;
169169
}[];
170+
gasFeeToken?: Hex;
170171
}
171172

172173
export interface GetPredictWalletParams {

0 commit comments

Comments
 (0)