From 5dedd03190e7737fd11b95780522ceac76443712 Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT Date: Thu, 1 Oct 2026 16:36:26 +0700 Subject: [PATCH 1/3] feat(perps-controller): report the winning fee source on fee quotes --- packages/perps-controller/CHANGELOG.md | 3 +++ packages/perps-controller/src/types/index.ts | 15 ++++++++++++++ .../src/utils/subscriptionFeeWaiver.ts | 4 +++- .../src/utils/subscriptionFeeWaiver.test.ts | 20 +++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/perps-controller/CHANGELOG.md b/packages/perps-controller/CHANGELOG.md index 0a966eca0aa..e38f6931960 100644 --- a/packages/perps-controller/CHANGELOG.md +++ b/packages/perps-controller/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add optional `feeSource` to `FeeCalculationResult`, reporting which fee source (`default`, `rewards` or `subscription`) won the fee resolution the quoted MetaMask fee was priced from, so clients can attribute the discount shown in a fee preview without re-deriving it from the rates ([#PR_NUMBER](https://github.com/MetaMask/core/pull/PR_NUMBER)) + - Absent when the quote was not re-priced from a fee resolution, for example a placement that carries no MetaMask builder fee + - `rewards` can carry a 0% discount, so check `metamaskFeeRate` before presenting it as a reduction - Add `TWAP_SLICE`, `VAULT_CLOSE` and `SPOT_DUST_CONVERSION` to `DETAILED_ORDER_TYPES` for the HyperLiquid order types added in `@nktkas/hyperliquid` 0.33.3 ([#10591](https://github.com/MetaMask/core/pull/10591)) - Add `PerpsController.getMarginModeLock` (and the `PerpsController:getMarginModeLock` messenger action) plus the optional `PerpsProvider.getMarginModeLock`, reporting the margin mode an asset is locked to by an open position or resting order/TWAP so clients can keep their margin-mode picker in sync with what order placement will accept. HyperLiquid implements it; other providers report `not_implemented`. ([#10414](https://github.com/MetaMask/core/pull/10414)) - Implement `getMarginModeLock` for Lighter, reporting the mode an open position binds to its market. ([#10414](https://github.com/MetaMask/core/pull/10414)) diff --git a/packages/perps-controller/src/types/index.ts b/packages/perps-controller/src/types/index.ts index c36124e89e7..4be69b3ef25 100644 --- a/packages/perps-controller/src/types/index.ts +++ b/packages/perps-controller/src/types/index.ts @@ -1801,6 +1801,21 @@ export type FeeCalculationResult = { */ chargesMetamaskBuilderFee?: boolean; + /** + * Fee source whose rate the MetaMask fee above was priced from: the winner + * of the unified fee resolution this quote was computed with. + * + * Absent when the quote was not re-priced from a resolution, e.g. a + * placement that carries no MetaMask builder fee. `subscription` always + * implies a reduced fee. `rewards` can win a tie with the default, so it may + * carry a 0% discount; check `metamaskFeeRate` before presenting it as a + * reduction. + * + * Describes this preview only. The submit path resolves the fee again, so + * the order is attributed to whichever source wins at submission. + */ + feeSource?: PerpsFeeSource; + // Optional detailed breakdown for transparency breakdown?: { baseFeeRate: number; diff --git a/packages/perps-controller/src/utils/subscriptionFeeWaiver.ts b/packages/perps-controller/src/utils/subscriptionFeeWaiver.ts index a48b4fcd2bb..41a2fb06cf3 100644 --- a/packages/perps-controller/src/utils/subscriptionFeeWaiver.ts +++ b/packages/perps-controller/src/utils/subscriptionFeeWaiver.ts @@ -373,7 +373,8 @@ export function quantizeBuilderFeeTenthsBps(discountBips: number): number { * placement carries a MetaMask builder fee at all, or undefined when it does not * report one. Distinguishes a genuine zero (a TWAP, for instance) from the zero * a concurrent fully-waived submit leaves in provider state. - * @returns The quote with its MetaMask component and totals re-priced. + * @returns The quote with its MetaMask component and totals re-priced, tagged + * with the fee source they were priced from. */ export function applyFeeResolution(params: { fees: FeeCalculationResult; @@ -430,6 +431,7 @@ export function applyFeeResolution(params: { ...fees, metamaskFeeRate, feeRate, + feeSource: resolution.source, ...(notional !== undefined && { metamaskFeeAmount: notional * metamaskFeeRate, feeAmount: notional * feeRate, diff --git a/packages/perps-controller/tests/src/utils/subscriptionFeeWaiver.test.ts b/packages/perps-controller/tests/src/utils/subscriptionFeeWaiver.test.ts index 7aaf3df0a64..51e219cbdf6 100644 --- a/packages/perps-controller/tests/src/utils/subscriptionFeeWaiver.test.ts +++ b/packages/perps-controller/tests/src/utils/subscriptionFeeWaiver.test.ts @@ -404,6 +404,24 @@ describe('applyFeeResolution', () => { expect(priced.feeRate).toBeCloseTo(0.0012, 10); expect(priced.metamaskFeeAmount).toBeCloseTo(0.75, 10); expect(priced.feeAmount).toBeCloseTo(1.2, 10); + expect(priced.feeSource).toBe('subscription'); + }); + + it('reports the rewards source when a rewards discount won', () => { + const priced = applyFeeResolution({ + fees, + resolution: { + // A 65% rewards discount off the 10-bip default. + feeBips: 3.5, + discountBips: 6500, + source: 'rewards', + subscription: createStatus({ eligible: false, reason: 'no-source' }), + }, + amount: '1000', + }); + + expect(priced.metamaskFeeRate).toBeCloseTo(0.00035, 10); + expect(priced.feeSource).toBe('rewards'); }); it('zeroes the MetaMask component on a full waiver', () => { @@ -467,6 +485,7 @@ describe('applyFeeResolution', () => { // The full 10-bip builder fee, not the 5-bip rate the provider carried. expect(priced.metamaskFeeRate).toBeCloseTo(0.001, 10); expect(priced.feeRate).toBeCloseTo(0.00145, 10); + expect(priced.feeSource).toBe('default'); }); it.each(['-1000', '0'])( @@ -574,6 +593,7 @@ describe('applyFeeResolution', () => { }); expect(priced.metamaskFeeRate).toBe(0); + expect(priced.feeSource).toBe('subscription'); // The previously quoted amounts are left as the provider reported them. expect(priced.metamaskFeeAmount).toBe(fees.metamaskFeeAmount); }); From 378f5fcf75131b2bf126aadba27914ec7a136b29 Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT Date: Thu, 1 Oct 2026 16:37:11 +0700 Subject: [PATCH 2/3] chore(perps-controller): link changelog entry to #10650 --- packages/perps-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perps-controller/CHANGELOG.md b/packages/perps-controller/CHANGELOG.md index e38f6931960..c6fca7286f8 100644 --- a/packages/perps-controller/CHANGELOG.md +++ b/packages/perps-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add optional `feeSource` to `FeeCalculationResult`, reporting which fee source (`default`, `rewards` or `subscription`) won the fee resolution the quoted MetaMask fee was priced from, so clients can attribute the discount shown in a fee preview without re-deriving it from the rates ([#PR_NUMBER](https://github.com/MetaMask/core/pull/PR_NUMBER)) +- Add optional `feeSource` to `FeeCalculationResult`, reporting which fee source (`default`, `rewards` or `subscription`) won the fee resolution the quoted MetaMask fee was priced from, so clients can attribute the discount shown in a fee preview without re-deriving it from the rates ([#10650](https://github.com/MetaMask/core/pull/10650)) - Absent when the quote was not re-priced from a fee resolution, for example a placement that carries no MetaMask builder fee - `rewards` can carry a 0% discount, so check `metamaskFeeRate` before presenting it as a reduction - Add `TWAP_SLICE`, `VAULT_CLOSE` and `SPOT_DUST_CONVERSION` to `DETAILED_ORDER_TYPES` for the HyperLiquid order types added in `@nktkas/hyperliquid` 0.33.3 ([#10591](https://github.com/MetaMask/core/pull/10591)) From b9e83ce67a332def3caeab40434ec1b403367902 Mon Sep 17 00:00:00 2001 From: Tai Nguyen TT Date: Thu, 1 Oct 2026 16:50:57 +0700 Subject: [PATCH 3/3] fix(perps-controller): keep the feeSource changelog entry unreleased --- packages/perps-controller/CHANGELOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/perps-controller/CHANGELOG.md b/packages/perps-controller/CHANGELOG.md index 8642589a1d8..49b0d553259 100644 --- a/packages/perps-controller/CHANGELOG.md +++ b/packages/perps-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add optional `feeSource` to `FeeCalculationResult`, reporting which fee source (`default`, `rewards` or `subscription`) won the fee resolution the quoted MetaMask fee was priced from, so clients can attribute the discount shown in a fee preview without re-deriving it from the rates ([#10650](https://github.com/MetaMask/core/pull/10650)) + - Absent when the quote was not re-priced from a fee resolution, for example a placement that carries no MetaMask builder fee + - `rewards` can carry a 0% discount, so check `metamaskFeeRate` before presenting it as a reduction + ### Changed - **BREAKING:** `OrderFill.pnl` is optional when the venue omits realized PnL. Consumers must preserve missing amounts as unknown when aggregating or displaying fills; only a reported `'0'` is zero ([#10605](https://github.com/MetaMask/core/pull/10605)) @@ -19,9 +25,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add optional `feeSource` to `FeeCalculationResult`, reporting which fee source (`default`, `rewards` or `subscription`) won the fee resolution the quoted MetaMask fee was priced from, so clients can attribute the discount shown in a fee preview without re-deriving it from the rates ([#10650](https://github.com/MetaMask/core/pull/10650)) - - Absent when the quote was not re-priced from a fee resolution, for example a placement that carries no MetaMask builder fee - - `rewards` can carry a 0% discount, so check `metamaskFeeRate` before presenting it as a reduction - Add `TWAP_SLICE`, `VAULT_CLOSE` and `SPOT_DUST_CONVERSION` to `DETAILED_ORDER_TYPES` for the HyperLiquid order types added in `@nktkas/hyperliquid` 0.33.3 ([#10591](https://github.com/MetaMask/core/pull/10591)) - Add `PerpsController.getMarginModeLock` (and the `PerpsController:getMarginModeLock` messenger action) plus the optional `PerpsProvider.getMarginModeLock`, reporting the margin mode an asset is locked to by an open position or resting order/TWAP so clients can keep their margin-mode picker in sync with what order placement will accept. HyperLiquid implements it; other providers report `not_implemented`. ([#10414](https://github.com/MetaMask/core/pull/10414)) - Implement `getMarginModeLock` for Lighter, reporting the mode an open position binds to its market. ([#10414](https://github.com/MetaMask/core/pull/10414))