From 087d9e8e788dfbfa769a801e7b47406383ed67ef Mon Sep 17 00:00:00 2001 From: asalsys Date: Thu, 18 Dec 2025 02:26:25 -0500 Subject: [PATCH 1/4] chore: remove rule (#24120) ## **Description** This PR removes the `feature-flag-guidelines.mdc` cursor rule that enforced using the `useFeatureFlag` hook for feature flag access. **Reason for change:** The team has decided to go back to using Redux selectors for feature flag access instead of the `useFeatureFlag` hook pattern. The cursor rule is no longer aligned with the team's preferred approach and was causing confusion by enforcing a pattern we no longer want to follow. **Solution:** Remove the `.cursor/rules/feature-flag-guidelines.mdc` file to allow developers to use selectors for feature flags again. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: N/A ## **Manual testing steps** Feature: Cursor Rules Scenario: Developer no longer sees feature flag guidelines Given a developer is working on the MetaMask Mobile codebase When the developer checks the .cursor/rules directory Then the feature-flag-guidelines.mdc file should not exist And no cursor AI guidance enforces using useFeatureFlag hook ## **Screenshots/Recordings** ### **Before** N/A - This is a documentation/tooling change only. ### **After** N/A - This is a documentation/tooling change only. ## **Pre-merge author checklist** - [x] 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). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] 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/rules/feature-flag-guidelines.mdc | 91 ----------------------- 1 file changed, 91 deletions(-) delete mode 100644 .cursor/rules/feature-flag-guidelines.mdc diff --git a/.cursor/rules/feature-flag-guidelines.mdc b/.cursor/rules/feature-flag-guidelines.mdc deleted file mode 100644 index 424dae1d93e..00000000000 --- a/.cursor/rules/feature-flag-guidelines.mdc +++ /dev/null @@ -1,91 +0,0 @@ ---- -globs: "**/*" -alwaysApply: true ---- - -# Feature Flag Guidelines - -## Core Principle - -**ALWAYS** use the `useFeatureFlag` hook instead of creating new feature flag selectors. - -## Forbidden Patterns - -### ❌ NEVER Create New Feature Flag Selectors - -**DO NOT** create new selectors using `createSelector` for feature flags: - -```typescript -// ❌ FORBIDDEN - Do not create new feature flag selectors -export const selectMyFeatureEnabledFlag = createSelector( - selectRemoteFeatureFlags, - (remoteFeatureFlags) => { - // ... selector logic - }, -); -``` - -**DO NOT** add new feature flag selectors in: -- `app/selectors/featureFlagController/**/*.ts` -- `app/components/**/selectors/featureFlags/**/*.ts` -- Any other location that creates feature flag selectors - -## Required Pattern - -### ✅ ALWAYS Use the `useFeatureFlag` Hook - -**MUST** use the `useFeatureFlag` hook from `app/components/hooks/FeatureFlags/useFeatureFlag.ts`: - -```typescript -// ✅ REQUIRED - Use the hook instead -import { useFeatureFlag, FeatureFlagNames } from '../../../hooks/FeatureFlags/useFeatureFlag'; - -const MyComponent = () => { - const isFeatureEnabled = useFeatureFlag(FeatureFlagNames.rewardsEnabled); - - // Use the flag value - if (isFeatureEnabled) { - // ... feature logic - } -}; -``` - -## Steps to Use Feature Flags - -1. **Add the flag name** to the `FeatureFlagNames` enum in `app/components/hooks/FeatureFlags/useFeatureFlag.ts`: - ```typescript - export enum FeatureFlagNames { - rewardsEnabled = 'rewardsEnabled', - myNewFeature = 'myNewFeature', // Add your new flag here - } - ``` - -2. **Use the hook** in your component: - ```typescript - const isMyFeatureEnabled = useFeatureFlag(FeatureFlagNames.myNewFeature); - ``` - -3. **Do NOT** create a selector for the feature flag - -## Migration Pattern - -If you encounter existing feature flag selectors, prefer migrating to the hook: - -```typescript -// ❌ Old pattern (existing code - do not replicate) -const isFeatureEnabled = useSelector(selectMyFeatureEnabledFlag); - -// ✅ New pattern (use this instead) -const isFeatureEnabled = useFeatureFlag(FeatureFlagNames.myNewFeature); -``` - -## Enforcement - -- **REJECT** any code that creates new `createSelector` instances for feature flags -- **REJECT** any new files in `app/selectors/featureFlagController/` directories -- **REQUIRE** use of `useFeatureFlag` hook for all feature flag access -- **REQUIRE** adding flag names to `FeatureFlagNames` enum before use - -## Exception - -The only exception is the base selector `selectRemoteFeatureFlags` in `app/selectors/featureFlagController/index.ts`, which is used internally by the `useFeatureFlag` hook infrastructure. From 80b13d3ba8ebdd378e105b2a360da03fcd22ae69 Mon Sep 17 00:00:00 2001 From: abretonc7s <107169956+abretonc7s@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:07:53 +0800 Subject: [PATCH 2/4] feat(perps): refactor home to include positions pnl (#24104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR moves the unrealized P&L display from the balance header area to the Positions section header, improving the information hierarchy on the Perps home screen. **Changes:** 1. Extended `PerpsHomeSection` component with optional subtitle support (`subtitle`, `subtitleColor`, `subtitleSuffix`, `subtitleTestID` props) 2. Updated `PerpsHomeView` to calculate and display aggregate P&L below the "Positions" title 3. Removed inline P&L display from `PerpsMarketBalanceActions` component (was showing `"$X available · P&L +$Y (Z%)"`) 4. Cleaned up unused props, imports, and variables from `PerpsMarketBalanceActions` **Result:** - Balance header now shows only: `"$X available"` - Positions section header now shows: `"Positions"` with subtitle where the P&L value is color-coded (`-$18.47 (2.1%)`) and the label (`Unrealized PnL`) is in default color ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TAT-2178 ## **Manual testing steps** ```gherkin Feature: Display unrealized P&L below open positions Scenario: User views P&L in Positions section header Given user has open positions with unrealized P&L And user is on the Perps home screen When user views the Positions section Then the unrealized P&L is displayed below the "Positions" title And the P&L is color-coded (green for profit, red for loss) Scenario: User views balance area without P&L Given user has funded their Perps account And user is on the Perps home screen When user views the balance area at the top Then only the available balance is shown (no inline P&L) ``` ## **Screenshots/Recordings** ### **Before** image ### **After** image ## **Pre-merge author checklist** - [x] 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). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] 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. --- > [!NOTE] > Moves unrealized P&L from the balance header to a color-coded subtitle under the Positions section, adding subtitle support and tests while removing P&L from balance actions. > > - **UI** > - **`PerpsHomeSection`**: Adds optional subtitle support (`subtitle`, `subtitleColor`, `subtitleSuffix`, `subtitleTestID`) and refactors header layout (`headerContainer`/`titleRow`). > - **`PerpsHomeView`**: Computes aggregate unrealized PnL/ROE and sets as `Positions` subtitle with color and suffix; wires testID `PerpsHomeViewSelectorsIDs.POSITIONS_PNL_VALUE`. > - **`PerpsMarketBalanceActions`**: Removes inline P&L display and unused `positions` prop; balance area now shows only available balance; cleans up related imports/logic. > - **Tests** > - **`PerpsHomeSection.test.tsx`**: Adds cases for subtitle rendering, color, suffix, and pressability alongside actions. > - **E2E Selectors** > - Adds `POSITIONS_PNL_VALUE` to `PerpsHomeViewSelectorsIDs`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7fe59b4b2e59be1a038d3c915ec8de21a637e3ef. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../Views/PerpsHomeView/PerpsHomeView.tsx | 45 +++++- .../PerpsHomeSection.test.tsx | 135 ++++++++++++++++++ .../PerpsHomeSection/PerpsHomeSection.tsx | 71 +++++++-- .../PerpsMarketBalanceActions.tsx | 65 ++------- e2e/selectors/Perps/Perps.selectors.ts | 1 + 5 files changed, 254 insertions(+), 63 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx b/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx index 75bad8b1331..6e8668efef8 100644 --- a/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx +++ b/app/components/UI/Perps/Views/PerpsHomeView/PerpsHomeView.tsx @@ -22,7 +22,9 @@ import { ButtonSize, } from '@metamask/design-system-react-native'; import { useStyles } from '../../../../../component-library/hooks'; +import { TextColor } from '../../../../../component-library/components/Texts/Text'; import { strings } from '../../../../../../locales/i18n'; +import { formatPnl, formatPercentage } from '../../utils/formatUtils'; import Routes from '../../../../../constants/navigation/Routes'; import { usePerpsHomeData, @@ -93,6 +95,10 @@ const PerpsHomeView = () => { const totalBalance = perpsAccount?.totalBalance || '0'; const isBalanceEmpty = BigNumber(totalBalance).isZero(); + // Calculate P&L for positions subtitle + const unrealizedPnl = perpsAccount?.unrealizedPnl || '0'; + const roe = parseFloat(perpsAccount?.returnOnEquity || '0'); + // Fetch all home screen data const { positions, @@ -106,6 +112,40 @@ const PerpsHomeView = () => { isLoading, } = usePerpsHomeData({}); + // Calculate positions subtitle with P&L + const hasPositions = positions.length > 0; + const { positionsSubtitle, positionsSubtitleColor, positionsSubtitleSuffix } = + useMemo(() => { + const pnlNum = parseFloat(unrealizedPnl); + const isPnlZero = BigNumber(unrealizedPnl).isZero(); + + // Only show subtitle when there are positions and P&L is non-zero + if (!hasPositions || isPnlZero) { + return { + positionsSubtitle: undefined, + positionsSubtitleColor: undefined, + positionsSubtitleSuffix: undefined, + }; + } + + const color = + pnlNum > 0 + ? TextColor.Success + : pnlNum < 0 + ? TextColor.Error + : TextColor.Alternative; + + // Format: "-$18.47 (2.1%)" colored + "Unrealized PnL" in default color + const subtitle = `${formatPnl(pnlNum)} (${formatPercentage(roe, 1)})`; + const suffix = strings('perps.unrealized_pnl'); + + return { + positionsSubtitle: subtitle, + positionsSubtitleColor: color, + positionsSubtitleSuffix: suffix, + }; + }, [hasPositions, unrealizedPnl, roe]); + // Determine if any data is loading for initial load tracking // Orders and activity load via WebSocket instantly, only track positions and markets const isAnyLoading = isLoading.positions || isLoading.markets; @@ -248,13 +288,16 @@ const PerpsHomeView = () => { > {/* Balance Actions Component */} {/* Positions Section */} { const mockSkeleton = () => ; const mockChildren = Content; @@ -403,4 +405,137 @@ describe('PerpsHomeSection', () => { expect(mockOnActionPress).not.toHaveBeenCalled(); }); }); + + describe('subtitle rendering', () => { + it('renders subtitle when provided', () => { + const { getByText } = render( + + {mockChildren} + , + ); + + expect(getByText('-$18.47 (2.1%) Unrealized P&L')).toBeTruthy(); + }); + + it('does not render subtitle when not provided', () => { + const { queryByText } = render( + + {mockChildren} + , + ); + + // Should only have the title, no subtitle + expect(queryByText('Test Section')).toBeTruthy(); + }); + + it('renders subtitle with custom color', () => { + const { getByText } = render( + + {mockChildren} + , + ); + + expect(getByText('+$50.00 (5.0%) Unrealized P&L')).toBeTruthy(); + }); + + it('applies subtitleTestID when provided', () => { + const { getByTestId } = render( + + {mockChildren} + , + ); + + expect(getByTestId('custom-subtitle-testid')).toBeTruthy(); + }); + + it('renders subtitle alongside title and action button', () => { + const mockOnActionPress = jest.fn(); + + const { getByText } = render( + + {mockChildren} + , + ); + + expect(getByText('Positions')).toBeTruthy(); + expect(getByText('-$18.47 (2.1%)')).toBeTruthy(); + + // Action should still work + fireEvent.press(getByText('Positions')); + expect(mockOnActionPress).toHaveBeenCalledTimes(1); + }); + + it('renders subtitle with suffix', () => { + const { getByTestId } = render( + + {mockChildren} + , + ); + + // Verify both subtitle and suffix are rendered via testIDs + expect(getByTestId('test-subtitle')).toBeTruthy(); + expect(getByTestId('test-subtitle-suffix')).toBeTruthy(); + }); + + it('does not render suffix when subtitle is not provided', () => { + const { queryByTestId } = render( + + {mockChildren} + , + ); + + // Suffix should not render without a subtitle + expect(queryByTestId('test-subtitle')).toBeNull(); + expect(queryByTestId('test-subtitle-suffix')).toBeNull(); + }); + }); }); diff --git a/app/components/UI/Perps/components/PerpsHomeSection/PerpsHomeSection.tsx b/app/components/UI/Perps/components/PerpsHomeSection/PerpsHomeSection.tsx index fde4687a1e4..0e45614a26e 100644 --- a/app/components/UI/Perps/components/PerpsHomeSection/PerpsHomeSection.tsx +++ b/app/components/UI/Perps/components/PerpsHomeSection/PerpsHomeSection.tsx @@ -15,6 +15,22 @@ export interface PerpsHomeSectionProps { * Section title */ title: string; + /** + * Optional subtitle text (e.g., P&L value and percentage) + */ + subtitle?: string; + /** + * Color for subtitle text (e.g., Success for profit, Error for loss) + */ + subtitleColor?: TextColor; + /** + * Optional suffix for subtitle (rendered in default color, e.g., "Unrealized PnL") + */ + subtitleSuffix?: string; + /** + * Test ID for subtitle element + */ + subtitleTestID?: string; /** * Whether the section is loading */ @@ -51,14 +67,16 @@ const styles = StyleSheet.create({ section: { marginBottom: 24, }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', + headerContainer: { paddingHorizontal: 16, marginBottom: 8, marginTop: 12, }, + titleRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, content: { // Content styling handled by children }, @@ -89,6 +107,10 @@ const styles = StyleSheet.create({ */ const PerpsHomeSection: React.FC = ({ title, + subtitle, + subtitleColor = TextColor.Alternative, + subtitleSuffix, + subtitleTestID, isLoading, isEmpty, showWhenEmpty = false, @@ -104,7 +126,8 @@ const PerpsHomeSection: React.FC = ({ const showAction = onActionPress && !isLoading && !isEmpty; - const headerContent = ( + // Title row content (pressable when action is available) + const titleRowContent = ( <> {title} @@ -122,13 +145,37 @@ const PerpsHomeSection: React.FC = ({ return ( {/* Section Header */} - {showAction ? ( - - {headerContent} - - ) : ( - {headerContent} - )} + + {/* Title row - only this is pressable */} + {showAction ? ( + + {titleRowContent} + + ) : ( + {titleRowContent} + )} + + {/* Subtitle - NOT pressable */} + {subtitle && ( + + {subtitle} + {subtitleSuffix && ( + + {' '} + {subtitleSuffix} + + )} + + )} + {/* Section Content */} diff --git a/app/components/UI/Perps/components/PerpsMarketBalanceActions/PerpsMarketBalanceActions.tsx b/app/components/UI/Perps/components/PerpsMarketBalanceActions/PerpsMarketBalanceActions.tsx index bf25e778eb6..fc0a8ddbdd3 100644 --- a/app/components/UI/Perps/components/PerpsMarketBalanceActions/PerpsMarketBalanceActions.tsx +++ b/app/components/UI/Perps/components/PerpsMarketBalanceActions/PerpsMarketBalanceActions.tsx @@ -29,14 +29,9 @@ import PerpsBottomSheetTooltip from '../PerpsBottomSheetTooltip'; import { usePerpsLiveAccount } from '../../hooks/stream'; import { formatPerpsFiat, - formatPnl, - formatPercentage, PRICE_RANGES_MINIMAL_VIEW, } from '../../utils/formatUtils'; -import type { - PerpsNavigationParamList, - Position, -} from '../../controllers/types'; +import type { PerpsNavigationParamList } from '../../controllers/types'; import { PerpsMarketBalanceActionsSelectorsIDs } from '../../../../../../e2e/selectors/Perps/Perps.selectors'; import { BigNumber } from 'bignumber.js'; import { INITIAL_AMOUNT_UI_PROGRESS } from '../../constants/hyperLiquidConfig'; @@ -51,7 +46,6 @@ import { RootState } from '../../../../../reducers'; import { selectSelectedInternalAccountByScope } from '../../../../../selectors/multichainAccounts/accounts'; interface PerpsMarketBalanceActionsProps { - positions?: Position[]; showActionButtons?: boolean; } @@ -76,7 +70,6 @@ const PerpsMarketBalanceActionsSkeleton: React.FC = () => { }; const PerpsMarketBalanceActions: React.FC = ({ - positions = [], showActionButtons = true, }) => { const tw = useTailwind(); @@ -218,17 +211,7 @@ const PerpsMarketBalanceActions: React.FC = ({ const totalBalance = perpsAccount?.totalBalance || '0'; const availableBalance = perpsAccount?.availableBalance || '0'; - const unrealizedPnl = perpsAccount?.unrealizedPnl || '0'; - const roe = parseFloat(perpsAccount?.returnOnEquity || '0'); const isBalanceEmpty = BigNumber(totalBalance).isZero(); - const hasPositions = positions.length > 0; - - const pnlNum = useMemo(() => parseFloat(unrealizedPnl), [unrealizedPnl]); - const pnlColor = useMemo(() => { - if (pnlNum > 0) return TextColor.Success; - if (pnlNum < 0) return TextColor.Error; - return TextColor.Alternative; - }, [pnlNum]); const handleLearnMore = useCallback(() => { navigation.navigate(Routes.PERPS.TUTORIAL, { @@ -346,38 +329,20 @@ const PerpsMarketBalanceActions: React.FC = ({ {formatPerpsFiat(totalBalance)} - - - {formatPerpsFiat(availableBalance, { - ranges: PRICE_RANGES_MINIMAL_VIEW, - stripTrailingZeros: false, - })}{' '} - {strings('perps.available')} - - {hasPositions && !BigNumber(unrealizedPnl).isZero() && ( - <> - - {' · P&L '} - - - {formatPnl(pnlNum)} ({formatPercentage(roe, 1)}) - - - )} - + + {formatPerpsFiat(availableBalance, { + ranges: PRICE_RANGES_MINIMAL_VIEW, + stripTrailingZeros: false, + })}{' '} + {strings('perps.available')} + {/* Action Buttons */} {showActionButtons && ( Date: Thu, 18 Dec 2025 09:49:54 +0100 Subject: [PATCH 3/4] fix(perps): sync chart current price line with live candle close price (#24100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Use the last candle's close price from the candle stream instead of the separate allMids price stream for the chart's current price line. This eliminates the 0.5-2s delay between the live candlestick close and the current price line by ensuring both use the same data source. ## **Changelog** CHANGELOG entry: Fixed delay between the live candlestick close price and current price line on the Perps TradingView chart ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TAT-2209 ## **Manual testing steps** ```gherkin Feature: Perps TradingView Chart Price Synchronization Scenario: user observes current price line syncs with live candle Given user is on the Perps Market Details view with a TradingView chart When user observes the live candlestick updating in real-time Then the current price line (horizontal line on y-axis) should update simultaneously with the candlestick close price And there should be no visible delay between the candlestick close and the price line ``` ## **Screenshots/Recordings** ### **Before** The prices were not synced https://consensyssoftware.atlassian.net/browse/TAT-2209?atlOrigin=eyJpIjoiMWYwMzJkNzBiNDQ2NDNkN2FlNGQzYTRmYzZjNDE0ZTQiLCJwIjoiaiJ9 ### **After** The prices are synced https://github.com/user-attachments/assets/755e21de-3d98-4837-a569-ab2c0d1fccac ## **Pre-merge author checklist** - [x] 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). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] 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. --- > [!NOTE] > Synchronizes current price with the latest candle close across chart and headers, refactoring LivePriceHeader to accept a `currentPrice` prop and updating usages/tests. > > - **Perps Market Details**: > - Compute `chartCurrentPrice` from last candle close (`usePerpsLiveCandles`) and use it for chart TP/SL lines (`tpslLines`). > - Pass `currentPrice={chartCurrentPrice}` to `PerpsMarketHeader` for header price sync. > - **Perps Order Book**: > - Pass `currentPrice={marketPrice ?? 0}` to `PerpsMarketHeader` in both normal and error states. > - **PerpsMarketHeader**: > - Accepts required `currentPrice` prop and forwards it to `LivePriceHeader`. > - **LivePriceHeader**: > - API change: remove `fallbackPrice`; add required `currentPrice` prop. > - Use `currentPrice` for price display; subscribe only for 24h percent change. > - Handle loading/invalid price cases consistently. > - **Tests**: > - Update tests to provide `currentPrice` and remove `fallbackPrice` paths; adjust assertions accordingly. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f95a3a199be69787accf7625aabd266ba0e3f0b9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../PerpsMarketDetailsView.tsx | 21 ++- .../PerpsOrderBookView/PerpsOrderBookView.tsx | 14 +- .../LivePriceDisplay/LivePriceHeader.test.tsx | 140 ++++++------------ .../LivePriceDisplay/LivePriceHeader.tsx | 33 ++--- .../PerpsMarketHeader.test.tsx | 3 + .../PerpsMarketHeader/PerpsMarketHeader.tsx | 5 +- 6 files changed, 94 insertions(+), 122 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx index 1e9067b30c0..b207cc08f9b 100644 --- a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx @@ -344,6 +344,14 @@ const PerpsMarketDetailsView: React.FC = () => { throttleMs: 1000, }); + // Get current price from the last candle's close price for chart synchronization + // This ensures the current price line matches the live candle close price exactly + const chartCurrentPrice = useMemo(() => { + if (!candleData?.candles?.length) return 0; + const lastCandle = candleData.candles.at(-1); + return lastCandle?.close ? Number.parseFloat(lastCandle.close) : 0; + }, [candleData]); + // Auto-zoom to latest candle when interval changes and new data arrives // This ensures the chart shows the most recent data after interval change useEffect(() => { @@ -394,10 +402,10 @@ const PerpsMarketDetailsView: React.FC = () => { }, [existingPosition, orderFills]); // Compute TP/SL lines for the chart based on existing position - // Always include currentPrice to ensure chart price line matches header (TAT-2112) + // Use chartCurrentPrice (from candle close) to ensure price line syncs with live candle const tpslLines = useMemo(() => { - const currentPriceStr = - currentPrice > 0 ? currentPrice.toString() : undefined; + const chartPriceStr = + chartCurrentPrice > 0 ? chartCurrentPrice.toString() : undefined; if (existingPosition) { return { @@ -405,13 +413,13 @@ const PerpsMarketDetailsView: React.FC = () => { takeProfitPrice: existingPosition.takeProfitPrice, stopLossPrice: existingPosition.stopLossPrice, liquidationPrice: existingPosition.liquidationPrice || undefined, - currentPrice: currentPriceStr, + currentPrice: chartPriceStr, }; } // Even without position, show current price line on chart - return currentPriceStr ? { currentPrice: currentPriceStr } : undefined; - }, [existingPosition, currentPrice]); + return chartPriceStr ? { currentPrice: chartPriceStr } : undefined; + }, [existingPosition, chartCurrentPrice]); // Stop loss prompt banner logic // Hook handles visibility orchestration including fade-out animation @@ -906,6 +914,7 @@ const PerpsMarketDetailsView: React.FC = () => { onFullscreenPress={handleFullscreenChartOpen} isFavorite={isWatchlist} testID={PerpsMarketDetailsViewSelectorsIDs.HEADER} + currentPrice={chartCurrentPrice} /> diff --git a/app/components/UI/Perps/Views/PerpsOrderBookView/PerpsOrderBookView.tsx b/app/components/UI/Perps/Views/PerpsOrderBookView/PerpsOrderBookView.tsx index 15767bb1600..8cbe5d202d3 100644 --- a/app/components/UI/Perps/Views/PerpsOrderBookView/PerpsOrderBookView.tsx +++ b/app/components/UI/Perps/Views/PerpsOrderBookView/PerpsOrderBookView.tsx @@ -341,7 +341,11 @@ const PerpsOrderBookView: React.FC = ({ return ( {market ? ( - + ) : ( = ({ return ( {/* Market Header */} - {market && } + {market && ( + + )} {/* Controls Row - Unit Toggle and Grouping */} diff --git a/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.test.tsx b/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.test.tsx index b5ffc657404..b42af592c17 100644 --- a/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.test.tsx +++ b/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.test.tsx @@ -30,26 +30,10 @@ describe('LivePriceHeader', () => { it('should render without crashing', () => { mockUsePerpsLivePrices.mockReturnValue({}); - const component = render(); + const component = render(); expect(component).toBeDefined(); }); - it('should show placeholders when no price data available', () => { - mockUsePerpsLivePrices.mockReturnValue({}); - const { getByText } = render(); - expect(getByText('$---')).toBeTruthy(); - expect(getByText('--%')).toBeTruthy(); - }); - - it('should show placeholders when price data is undefined', () => { - mockUsePerpsLivePrices.mockReturnValue({ - ETH: undefined as unknown as PriceUpdate, - }); - const { getByText } = render(); - expect(getByText('$---')).toBeTruthy(); - expect(getByText('--%')).toBeTruthy(); - }); - it('should show placeholders when price is invalid (zero)', () => { mockUsePerpsLivePrices.mockReturnValue({ ETH: { @@ -59,35 +43,25 @@ describe('LivePriceHeader', () => { timestamp: Date.now(), }, }); - const { getByText } = render(); + const { getByText } = render( + , + ); expect(getByText('$---')).toBeTruthy(); expect(getByText('--%')).toBeTruthy(); }); it('should show placeholders when price is invalid (negative)', () => { - mockUsePerpsLivePrices.mockReturnValue({ - ETH: { - coin: 'ETH', - price: '-100', - percentChange24h: '5', - timestamp: Date.now(), - }, - }); - const { getByText } = render(); + const { getByText } = render( + , + ); expect(getByText('$---')).toBeTruthy(); expect(getByText('--%')).toBeTruthy(); }); it('should show placeholders when price is invalid (NaN)', () => { - mockUsePerpsLivePrices.mockReturnValue({ - ETH: { - coin: 'ETH', - price: 'invalid', - percentChange24h: '5', - timestamp: Date.now(), - }, - }); - const { getByText } = render(); + const { getByText } = render( + , + ); expect(getByText('$---')).toBeTruthy(); expect(getByText('--%')).toBeTruthy(); }); @@ -101,7 +75,9 @@ describe('LivePriceHeader', () => { timestamp: Date.now(), }, }); - const { getByText } = render(); + const { getByText } = render( + , + ); expect(getByText('$3,000')).toBeTruthy(); // 4 sig figs, no trailing zeros expect(getByText('+5.50%')).toBeTruthy(); }); @@ -115,7 +91,9 @@ describe('LivePriceHeader', () => { timestamp: Date.now(), }, }); - const { getByText } = render(); + const { getByText } = render( + , + ); expect(getByText('$2,500')).toBeTruthy(); // 4 sig figs, no trailing zeros expect(getByText('-3.20%')).toBeTruthy(); }); @@ -129,7 +107,9 @@ describe('LivePriceHeader', () => { timestamp: Date.now(), }, }); - const { getByText } = render(); + const { getByText } = render( + , + ); expect(getByText('$2,000')).toBeTruthy(); // 4 sig figs, no trailing zeros expect(getByText('+0.00%')).toBeTruthy(); }); @@ -143,45 +123,22 @@ describe('LivePriceHeader', () => { timestamp: Date.now(), }, }); - const { getByText } = render(); - expect(getByText('$100')).toBeTruthy(); // 4 sig figs, no trailing zeros - expect(getByText('+2.10%')).toBeTruthy(); - }); - - it('uses fallback price but shows loading state for change when no live data', () => { - mockUsePerpsLivePrices.mockReturnValue({}); const { getByText } = render( - , + , ); - expect(getByText('$1,500')).toBeTruthy(); // 4 sig figs, no trailing zeros - expect(getByText('--%')).toBeTruthy(); + expect(getByText('$100')).toBeTruthy(); // 4 sig figs, no trailing zeros + expect(getByText('+2.10%')).toBeTruthy(); }); it('should show placeholders when fallback price is invalid', () => { mockUsePerpsLivePrices.mockReturnValue({}); const { getByText } = render( - , + , ); expect(getByText('$---')).toBeTruthy(); expect(getByText('--%')).toBeTruthy(); }); - it('should prefer live data over fallback', () => { - mockUsePerpsLivePrices.mockReturnValue({ - BTC: { - coin: 'BTC', - price: '50000', - percentChange24h: '3.0', - timestamp: Date.now(), - }, - }); - const { getByText } = render( - , - ); - expect(getByText('$50,000')).toBeTruthy(); // 4 sig figs, no trailing zeros - expect(getByText('+3.00%')).toBeTruthy(); - }); - describe('Color behavior for percentage change', () => { it('uses neutral color for loading state when percentChange is undefined', () => { mockUsePerpsLivePrices.mockReturnValue({ @@ -193,7 +150,9 @@ describe('LivePriceHeader', () => { }, }); - const { UNSAFE_getAllByType } = render(); + const { UNSAFE_getAllByType } = render( + , + ); const textElements = UNSAFE_getAllByType(Text); const changeText = textElements.find((el) => el.props.children === '--%'); @@ -205,7 +164,7 @@ describe('LivePriceHeader', () => { mockUsePerpsLivePrices.mockReturnValue({}); const { UNSAFE_getAllByType } = render( - , + , ); const textElements = UNSAFE_getAllByType(Text); @@ -224,7 +183,9 @@ describe('LivePriceHeader', () => { }, }); - const { UNSAFE_getAllByType } = render(); + const { UNSAFE_getAllByType } = render( + , + ); const textElements = UNSAFE_getAllByType(Text); const changeText = textElements.find( @@ -244,7 +205,9 @@ describe('LivePriceHeader', () => { }, }); - const { UNSAFE_getAllByType } = render(); + const { UNSAFE_getAllByType } = render( + , + ); const textElements = UNSAFE_getAllByType(Text); const changeText = textElements.find( @@ -264,7 +227,9 @@ describe('LivePriceHeader', () => { }, }); - const { UNSAFE_getAllByType } = render(); + const { UNSAFE_getAllByType } = render( + , + ); const textElements = UNSAFE_getAllByType(Text); const changeText = textElements.find( @@ -286,7 +251,9 @@ describe('LivePriceHeader', () => { }, }); - const { getByText } = render(); + const { getByText } = render( + , + ); expect(getByText('$3,000')).toBeTruthy(); expect(getByText('--%')).toBeTruthy(); @@ -301,7 +268,9 @@ describe('LivePriceHeader', () => { } as PriceUpdate, }); - const { getByText } = render(); + const { getByText } = render( + , + ); expect(getByText('$2,500')).toBeTruthy(); expect(getByText('--%')).toBeTruthy(); @@ -318,7 +287,7 @@ describe('LivePriceHeader', () => { }); const { getByText, queryByText } = render( - , + , ); expect(getByText('$2,000')).toBeTruthy(); @@ -330,32 +299,13 @@ describe('LivePriceHeader', () => { mockUsePerpsLivePrices.mockReturnValue({}); const { getByText } = render( - , + , ); expect(getByText('$1,800')).toBeTruthy(); expect(getByText('--%')).toBeTruthy(); }); - it('uses live percentChange when available', () => { - mockUsePerpsLivePrices.mockReturnValue({ - BTC: { - coin: 'BTC', - price: '55000', - percentChange24h: '4.2', - timestamp: Date.now(), - }, - }); - - const { getByText, queryByText } = render( - , - ); - - expect(getByText('$55,000')).toBeTruthy(); - expect(getByText('+4.20%')).toBeTruthy(); - expect(queryByText('--%')).toBeNull(); - }); - it('displays loading state when live price exists but percentChange is undefined', () => { mockUsePerpsLivePrices.mockReturnValue({ ETH: { @@ -367,7 +317,7 @@ describe('LivePriceHeader', () => { }); const { getByText } = render( - , + , ); expect(getByText('$3,100')).toBeTruthy(); diff --git a/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.tsx b/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.tsx index acf17b8fb1e..15faa66a86e 100644 --- a/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.tsx +++ b/app/components/UI/Perps/components/LivePriceDisplay/LivePriceHeader.tsx @@ -15,10 +15,11 @@ import { PERPS_CONSTANTS } from '../../constants/perpsConfig'; interface LivePriceHeaderProps { symbol: string; - fallbackPrice?: string; testIDPrice?: string; testIDChange?: string; throttleMs?: number; + /** Current price from candle stream - syncs header with chart */ + currentPrice: number; } const styleSheet = () => @@ -32,16 +33,17 @@ const styleSheet = () => /** * Component that displays live price and change for header - * Subscribes to price stream independently to avoid parent re-renders + * Uses currentPrice prop from candle stream, subscribes to price stream for 24h change only */ const LivePriceHeader: React.FC = ({ symbol, - fallbackPrice = '0', testIDPrice, testIDChange, throttleMs = 1000, // Balanced updates for header (1 update per second) + currentPrice, }) => { const { styles } = useStyles(styleSheet, {}); + // Subscribe to price stream only for 24h change percentage const prices = usePerpsLivePrices({ symbols: [symbol], throttleMs, @@ -49,18 +51,13 @@ const LivePriceHeader: React.FC = ({ const priceData = prices[symbol]; - // Use fallback data if no live data yet - const displayPrice = priceData - ? parseFloat(priceData.price) - : parseFloat(fallbackPrice); - // Use null to indicate loading state - only use actual values (including 0) when available // When we have live price data, only use percentChange from that data - don't fall back - const displayChange = priceData - ? priceData.percentChange24h !== undefined - ? parseFloat(priceData.percentChange24h) - : null - : null; + const displayChange = useMemo(() => { + if (!priceData) return null; + if (priceData.percentChange24h === undefined) return null; + return Number.parseFloat(priceData.percentChange24h); + }, [priceData]); // Only determine change color when we have actual data (not loading) const isPositiveChange = displayChange !== null && displayChange >= 0; @@ -74,19 +71,19 @@ const LivePriceHeader: React.FC = ({ // Format price display with edge case handling const formattedPrice = useMemo(() => { // Handle invalid or edge case values - if (!displayPrice || displayPrice <= 0 || !Number.isFinite(displayPrice)) { + if (!currentPrice || currentPrice <= 0 || !Number.isFinite(currentPrice)) { return PERPS_CONSTANTS.FALLBACK_PRICE_DISPLAY; } try { - return formatPerpsFiat(displayPrice, { + return formatPerpsFiat(currentPrice, { ranges: PRICE_RANGES_UNIVERSAL, }); } catch { // Fallback if formatPrice throws return PERPS_CONSTANTS.FALLBACK_PRICE_DISPLAY; } - }, [displayPrice]); + }, [currentPrice]); const formattedChange = useMemo(() => { // If displayChange is null, we're still loading - show loading indicator @@ -94,7 +91,7 @@ const LivePriceHeader: React.FC = ({ return PERPS_CONSTANTS.FALLBACK_PERCENTAGE_DISPLAY; } - if (!displayPrice || displayPrice <= 0 || !Number.isFinite(displayPrice)) { + if (!currentPrice || currentPrice <= 0 || !Number.isFinite(currentPrice)) { return PERPS_CONSTANTS.FALLBACK_PERCENTAGE_DISPLAY; } @@ -103,7 +100,7 @@ const LivePriceHeader: React.FC = ({ } catch { return PERPS_CONSTANTS.FALLBACK_PERCENTAGE_DISPLAY; } - }, [displayPrice, displayChange]); + }, [currentPrice, displayChange]); return ( diff --git a/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.test.tsx b/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.test.tsx index 1be725d252f..57c910109d0 100644 --- a/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.test.tsx +++ b/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.test.tsx @@ -33,6 +33,7 @@ describe('PerpsMarketHeader', () => { , { state: initialState }, ); @@ -47,6 +48,7 @@ describe('PerpsMarketHeader', () => { market={mockMarket} onBackPress={onBackPress} testID={PerpsMarketHeaderSelectorsIDs.CONTAINER} + currentPrice={45000} />, { state: initialState }, ); @@ -64,6 +66,7 @@ describe('PerpsMarketHeader', () => { market={mockMarket} onMorePress={onMorePress} testID={PerpsMarketHeaderSelectorsIDs.CONTAINER} + currentPrice={45000} />, { state: initialState }, ); diff --git a/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.tsx b/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.tsx index d64cf2a074e..839887f9b94 100644 --- a/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.tsx +++ b/app/components/UI/Perps/components/PerpsMarketHeader/PerpsMarketHeader.tsx @@ -29,6 +29,8 @@ interface PerpsMarketHeaderProps { onFullscreenPress?: () => void; isFavorite?: boolean; testID?: string; + /** Current price from candle stream - syncs header with chart */ + currentPrice: number; } const PerpsMarketHeader: React.FC = ({ @@ -39,6 +41,7 @@ const PerpsMarketHeader: React.FC = ({ onFullscreenPress, isFavorite = false, testID, + currentPrice, }) => { const { styles } = useStyles(styleSheet, {}); @@ -80,10 +83,10 @@ const PerpsMarketHeader: React.FC = ({ From 083cd4fe351d618273acd103360573f4a251a246 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 18 Dec 2025 10:30:23 +0000 Subject: [PATCH 4/4] fix: disable automatic gas fee updates (#24121) ## **Description** Disable automatic gas fee updates for source transactions generated by Perps and Predict deposits. ## **Changelog** CHANGELOG entry: null ## **Related issues** ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] 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). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] 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. --- > [!NOTE] > Refines automatic gas fee update logic, disabling it for transactions with nested relayDeposit and MetaMask-origin token approvals, and updates tests accordingly. > > - **Transaction Controller**: > - **Automatic Gas Fee Updates**: > - Implement `isAutomaticGasFeeUpdateEnabled(transaction)` and wire into `TransactionController` options. > - Disable when transaction has nested `relayDeposit`. > - Disable for `tokenMethodApprove` when `origin === ORIGIN_METAMASK`. > - Maintain behavior: enabled for `REDESIGNED_TRANSACTION_TYPES`, disabled for non-redesigned types. > - Add `hasTransactionType` and `ORIGIN_METAMASK` usage. > - **Tests**: > - Expand `isAutomaticGasFeeUpdateEnabled` test coverage for redesigned/non-redesigned, nested `relayDeposit`, and MetaMask vs external origins. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f664ff642dd5742d66620c7fa0a29021dfceef16. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../transaction-controller-init.test.ts | 93 ++++++++++++++++--- .../transaction-controller-init.ts | 23 ++++- 2 files changed, 100 insertions(+), 16 deletions(-) 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 2bfe7e8d46f..bfcec001b34 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 @@ -10,7 +10,7 @@ import { type PublishBatchHookTransaction, } from '@metamask/transaction-controller'; -import { toHex } from '@metamask/controller-utils'; +import { ORIGIN_METAMASK, toHex } from '@metamask/controller-utils'; import { MOCK_ANY_NAMESPACE, MockAnyNamespace } from '@metamask/messenger'; import { Hex } from '@metamask/utils'; import { selectSwapsChainFeatureFlags } from '../../../../reducers/swaps'; @@ -487,20 +487,87 @@ describe('Transaction Controller Init', () => { expect(updateTransactionsProp).toBe(true); }); - it('determines if automatic gas fee update is enabled based on transaction type', () => { - const option = testConstructorOption('isAutomaticGasFeeUpdateEnabled'); - const isEnabledFn = option as ({ type }: { type: string }) => boolean; + describe('isAutomaticGasFeeUpdateEnabled', () => { + it('returns true for redesigned transaction types', () => { + const option = testConstructorOption('isAutomaticGasFeeUpdateEnabled'); + const isEnabledFn = option as ({ + type, + }: { + type: string; + origin?: string; + }) => boolean; + + expect(isEnabledFn({ type: TransactionType.stakingDeposit })).toBe(true); + expect(isEnabledFn({ type: TransactionType.stakingUnstake })).toBe(true); + expect(isEnabledFn({ type: TransactionType.stakingClaim })).toBe(true); + expect(isEnabledFn({ type: TransactionType.contractInteraction })).toBe( + true, + ); + }); - // Redesigned transaction types - expect(isEnabledFn({ type: TransactionType.stakingDeposit })).toBe(true); - expect(isEnabledFn({ type: TransactionType.stakingUnstake })).toBe(true); - expect(isEnabledFn({ type: TransactionType.stakingClaim })).toBe(true); - expect(isEnabledFn({ type: TransactionType.contractInteraction })).toBe( - true, - ); + it('returns false for non-redesigned transaction types', () => { + const option = testConstructorOption('isAutomaticGasFeeUpdateEnabled'); + const isEnabledFn = option as ({ + type, + }: { + type: string; + origin?: string; + }) => boolean; + + expect(isEnabledFn({ type: TransactionType.bridge })).toBe(false); + }); + + it('returns false for transaction with nested relayDeposit type', () => { + const option = testConstructorOption('isAutomaticGasFeeUpdateEnabled'); + const isEnabledFn = option as (transaction: { + type: string; + origin?: string; + nestedTransactions?: { type: string }[]; + }) => boolean; + + const result = isEnabledFn({ + type: TransactionType.contractInteraction, + nestedTransactions: [{ type: TransactionType.relayDeposit }], + }); + + expect(result).toBe(false); + }); + + it('returns false for tokenMethodApprove with ORIGIN_METAMASK', () => { + const option = testConstructorOption('isAutomaticGasFeeUpdateEnabled'); + const isEnabledFn = option as ({ + type, + origin, + }: { + type: string; + origin?: string; + }) => boolean; + + const result = isEnabledFn({ + type: TransactionType.tokenMethodApprove, + origin: ORIGIN_METAMASK, + }); + + expect(result).toBe(false); + }); + + it('returns true for tokenMethodApprove with non-MetaMask origin', () => { + const option = testConstructorOption('isAutomaticGasFeeUpdateEnabled'); + const isEnabledFn = option as ({ + type, + origin, + }: { + type: string; + origin?: string; + }) => boolean; + + const result = isEnabledFn({ + type: TransactionType.tokenMethodApprove, + origin: 'https://external-dapp.com', + }); - // Non-redesigned transaction types - expect(isEnabledFn({ type: TransactionType.bridge })).toBe(false); + expect(result).toBe(true); + }); }); it('gets network state from network controller on option getNetworkState', () => { diff --git a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts index 057ebe79764..b80b7371779 100644 --- a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts +++ b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts @@ -50,7 +50,8 @@ import { trace } from '../../../../util/trace'; import { Delegation7702PublishHook } from '../../../../util/transactions/hooks/delegation-7702-publish'; import { isSendBundleSupported } from '../../../../util/transactions/sentinel-api'; import { NetworkClientId } from '@metamask/network-controller'; -import { toHex } from '@metamask/controller-utils'; +import { ORIGIN_METAMASK, toHex } from '@metamask/controller-utils'; +import { hasTransactionType } from '../../../../components/Views/confirmations/utils/transaction'; export const TransactionControllerInit: ControllerInitFunction< TransactionController, @@ -78,8 +79,7 @@ export const TransactionControllerInit: ControllerInitFunction< try { const transactionController: TransactionController = new TransactionController({ - isAutomaticGasFeeUpdateEnabled: ({ type }) => - REDESIGNED_TRANSACTION_TYPES.includes(type as TransactionType), + isAutomaticGasFeeUpdateEnabled, disableHistory: true, disableSendFlowHistory: true, disableSwaps: true, @@ -359,6 +359,23 @@ function beforeSign( return predictController.beforeSign(hookRequest); } +function isAutomaticGasFeeUpdateEnabled(transaction: TransactionMeta) { + if (hasTransactionType(transaction, [TransactionType.relayDeposit])) { + return false; + } + + if ( + transaction.origin === ORIGIN_METAMASK && + transaction.type === TransactionType.tokenMethodApprove + ) { + return false; + } + + return REDESIGNED_TRANSACTION_TYPES.includes( + transaction.type as TransactionType, + ); +} + function addTransactionControllerListeners( transactionEventHandlerRequest: TransactionEventHandlerRequest, ) {