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
3 changes: 3 additions & 0 deletions packages/perps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add optional `PerpsAccountSigner.getChainId`. When set, HyperLiquid user-signed actions (builder fee approval, withdrawals, transfers) are signed for the chain it returns instead of chain 1, so a wallet that only signs for its connected chain does not switch chains ([#10643](https://github.com/MetaMask/core/pull/10643))
- 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

Expand Down
15 changes: 15 additions & 0 deletions packages/perps-controller/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1807,6 +1807,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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -430,6 +431,7 @@ export function applyFeeResolution(params: {
...fees,
metamaskFeeRate,
feeRate,
feeSource: resolution.source,
...(notional !== undefined && {
metamaskFeeAmount: notional * metamaskFeeRate,
feeAmount: notional * feeRate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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'])(
Expand Down Expand Up @@ -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);
});
Expand Down
Loading