Skip to content

Commit cbf60d3

Browse files
feat(perps): persist Pro positions sort and side filter prefs
Add positionsSideFilter and positionsSortConfig to proLayoutPreferences so mobile can persist Positions/Orders panel preferences across markets and restarts.
1 parent 785ff8c commit cbf60d3

7 files changed

Lines changed: 195 additions & 19 deletions

File tree

packages/perps-controller/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Add `positionsSideFilter` and `positionsSortConfig` to `ProLayoutPreferences` (defaults `'all'` and `{ field: 'positionValue', direction: 'desc' }`) so Pro Positions/Orders panel sort and side-filter preferences persist across markets and app restarts via the existing `getProLayoutPreferences()` / `setProLayoutPreferences(patch)` API; export `ProPositionsSideFilter`, `ProPositionsSortField`, `ProPositionsSortConfig`, `ProLayoutPreferencesPatch`, and `mergeProLayoutPreferences` (deep-merges nested `positionsSortConfig` over defaults for predated persisted state)
1213
- **BREAKING:** Add strategy placement order types to `OrderType`: `twap`, `scale`, and `chase`, placeable through `placeOrder` alongside the existing `market`, `limit`, and trigger types ([#9832](https://github.com/MetaMask/core/pull/9832))
1314
- `OrderType` is a wider union again, so — exactly as for the trigger types added in 11.0.0 — any consumer signature that narrows it back to a smaller set no longer accepts a value typed `OrderType`. Such signatures must widen to `OrderType` or narrow explicitly at the call site.
1415
- A strategy placement expands one request into an execution schedule rather than a single resting order, so `OrderResult.orderId` carries a _handle_ — a venue TWAP id, or a client-generated group/session id — rather than an exchange order id. Its documentation says so; the individual exchange ids are in `childOrderIds`.

packages/perps-controller/src/PerpsController.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { PerpsMeasurementName } from './constants/performanceMetrics.js';
2424
import type {
2525
SortOptionId,
2626
ProLayoutPreferences,
27+
ProLayoutPreferencesPatch,
2728
PerpsMode,
2829
} from './constants/perpsConfig.js';
2930
import {
@@ -35,6 +36,7 @@ import {
3536
MAX_SLIPPAGE_BOUNDS,
3637
DEFAULT_PERPS_MODE,
3738
DEFAULT_PRO_LAYOUT_PREFERENCES,
39+
mergeProLayoutPreferences,
3840
} from './constants/perpsConfig.js';
3941
import type { PerpsControllerMethodActions } from './PerpsController-method-action-types.js';
4042
import { PERPS_ERROR_CODES } from './perpsErrorCodes.js';
@@ -249,8 +251,15 @@ export {
249251
PerpsMode,
250252
DEFAULT_PERPS_MODE,
251253
DEFAULT_PRO_LAYOUT_PREFERENCES,
254+
mergeProLayoutPreferences,
255+
} from './constants/perpsConfig.js';
256+
export type {
257+
ProLayoutPreferences,
258+
ProLayoutPreferencesPatch,
259+
ProPositionsSideFilter,
260+
ProPositionsSortConfig,
261+
ProPositionsSortField,
252262
} from './constants/perpsConfig.js';
253-
export type { ProLayoutPreferences } from './constants/perpsConfig.js';
254263

255264
/**
256265
* State shape for PerpsController
@@ -5141,26 +5150,33 @@ export class PerpsController extends BaseController<
51415150
getProLayoutPreferences(): ProLayoutPreferences {
51425151
// Merge over defaults so callers always receive a fully-populated object,
51435152
// even if the persisted state predates one of the fields.
5144-
return {
5145-
...DEFAULT_PRO_LAYOUT_PREFERENCES,
5146-
...this.state.proLayoutPreferences,
5147-
};
5153+
return mergeProLayoutPreferences(this.state.proLayoutPreferences);
51485154
}
51495155

51505156
/**
51515157
* Update the user's pro-mode layout preferences.
51525158
*
51535159
* Patch-style setter: only the provided fields are updated, the rest are
5154-
* preserved. This keeps the signature stable as new layout fields are added.
5160+
* preserved. Nested `positionsSortConfig` is deep-merged so callers can
5161+
* patch a single sort field without clobbering direction (or vice versa).
5162+
* This keeps the signature stable as new layout fields are added.
51555163
*
51565164
* @param patch - Partial set of pro-mode layout preferences to update.
51575165
*/
5158-
setProLayoutPreferences(patch: Partial<ProLayoutPreferences>): void {
5166+
setProLayoutPreferences(patch: ProLayoutPreferencesPatch): void {
51595167
this.update((state) => {
5160-
state.proLayoutPreferences = {
5168+
state.proLayoutPreferences = mergeProLayoutPreferences({
51615169
...state.proLayoutPreferences,
51625170
...patch,
5163-
};
5171+
...(patch.positionsSortConfig
5172+
? {
5173+
positionsSortConfig: {
5174+
...state.proLayoutPreferences.positionsSortConfig,
5175+
...patch.positionsSortConfig,
5176+
},
5177+
}
5178+
: {}),
5179+
});
51645180
});
51655181
}
51665182

packages/perps-controller/src/constants/perpsConfig.ts

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -486,19 +486,45 @@ export enum PerpsMode {
486486
Pro = 'pro',
487487
}
488488

489+
/**
490+
* Side filter for the Pro Positions/Orders panel (long/short/all).
491+
*
492+
* Shared across markets via `proLayoutPreferences.positionsSideFilter`.
493+
*/
494+
export type ProPositionsSideFilter = 'all' | 'long' | 'short';
495+
496+
/**
497+
* Sort fields available on the Pro Positions list.
498+
*/
499+
export type ProPositionsSortField =
500+
| 'positionValue'
501+
| 'unrealizedPnl'
502+
| 'fundingRate';
503+
504+
/**
505+
* Sort configuration for the Pro Positions list.
506+
*/
507+
export type ProPositionsSortConfig = {
508+
field: ProPositionsSortField;
509+
direction: 'asc' | 'desc';
510+
};
511+
489512
/**
490513
* Pro-mode layout preferences (network-independent).
491514
*
492-
* Flat object that persists across markets (unlike the per-market
493-
* `tradeConfigurations`). `chartExpanded` and the `*Position` fields are
494-
* reserved for future container-position UI and are kept here now so no
495-
* state-shape migration is needed when that UI ships.
515+
* Persists across markets (unlike the per-market `tradeConfigurations`).
516+
* `chartExpanded` and the `*Position` fields are reserved for future
517+
* container-position UI. `positionsSideFilter` / `positionsSortConfig` back
518+
* the Positions/Orders panel sort and side filter so they survive market
519+
* navigation and app restarts.
496520
*/
497521
export type ProLayoutPreferences = {
498522
orderBookExpanded: boolean;
499523
chartExpanded: boolean;
500524
orderBookPosition: 'left' | 'right';
501525
orderFormPosition: 'left' | 'right';
526+
positionsSideFilter: ProPositionsSideFilter;
527+
positionsSortConfig: ProPositionsSortConfig;
502528
};
503529

504530
/**
@@ -513,8 +539,45 @@ export const DEFAULT_PRO_LAYOUT_PREFERENCES: ProLayoutPreferences = {
513539
chartExpanded: false,
514540
orderBookPosition: 'left',
515541
orderFormPosition: 'right',
542+
positionsSideFilter: 'all',
543+
positionsSortConfig: {
544+
field: 'positionValue',
545+
direction: 'desc',
546+
},
516547
};
517548

549+
/**
550+
* Patch shape for `setProLayoutPreferences`.
551+
*
552+
* Top-level fields are optional; nested `positionsSortConfig` may also be
553+
* partially specified so a caller can update only `field` or only `direction`.
554+
*/
555+
export type ProLayoutPreferencesPatch = Partial<
556+
Omit<ProLayoutPreferences, 'positionsSortConfig'>
557+
> & {
558+
positionsSortConfig?: Partial<ProPositionsSortConfig>;
559+
};
560+
561+
/**
562+
* Merge a partial/persisted pro-layout preference blob over defaults.
563+
*
564+
* Nested `positionsSortConfig` is deep-merged so a persisted object that
565+
* predates one of its fields still yields a fully-populated config.
566+
*
567+
* @param prefs - Partial preferences from persisted state or a setter patch.
568+
* @returns A fully-populated `ProLayoutPreferences` object.
569+
*/
570+
export const mergeProLayoutPreferences = (
571+
prefs?: ProLayoutPreferencesPatch | null,
572+
): ProLayoutPreferences => ({
573+
...DEFAULT_PRO_LAYOUT_PREFERENCES,
574+
...prefs,
575+
positionsSortConfig: {
576+
...DEFAULT_PRO_LAYOUT_PREFERENCES.positionsSortConfig,
577+
...prefs?.positionsSortConfig,
578+
},
579+
});
580+
518581
/**
519582
* Default Perps interface mode.
520583
*/

packages/perps-controller/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export {
3535
PerpsMode,
3636
DEFAULT_PERPS_MODE,
3737
DEFAULT_PRO_LAYOUT_PREFERENCES,
38+
mergeProLayoutPreferences,
3839
} from './PerpsController.js';
3940
export type {
4041
PerpsControllerState,
@@ -44,6 +45,10 @@ export type {
4445
PerpsControllerActions,
4546
PerpsControllerEvents,
4647
ProLayoutPreferences,
48+
ProLayoutPreferencesPatch,
49+
ProPositionsSideFilter,
50+
ProPositionsSortConfig,
51+
ProPositionsSortField,
4752
} from './PerpsController.js';
4853
export type {
4954
PerpsControllerCalculateFeesAction,

packages/perps-controller/src/selectors.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import {
44
MARKET_SORTING_CONFIG,
55
PERPS_CONSTANTS,
66
SortOptionId,
7-
DEFAULT_PRO_LAYOUT_PREFERENCES,
87
DEFAULT_PERPS_MODE,
8+
mergeProLayoutPreferences,
99
} from './constants/perpsConfig.js';
1010
import type {
1111
PerpsMode,
@@ -249,10 +249,8 @@ export const selectMarketFilterPreferences = (
249249
*/
250250
export const selectProLayoutPreferences = (
251251
state: PerpsControllerState,
252-
): ProLayoutPreferences => ({
253-
...DEFAULT_PRO_LAYOUT_PREFERENCES,
254-
...state?.proLayoutPreferences,
255-
});
252+
): ProLayoutPreferences =>
253+
mergeProLayoutPreferences(state?.proLayoutPreferences);
256254

257255
/**
258256
* Select the current Perps interface mode (lite/pro).

packages/perps-controller/tests/src/PerpsController.configuration.test.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -694,12 +694,17 @@ describe('PerpsController', () => {
694694
});
695695

696696
describe('pro layout preferences', () => {
697-
it('defaults to collapsed order book, collapsed chart, and reserved positions', () => {
697+
it('defaults to collapsed order book, collapsed chart, reserved positions, and positions sort/filter defaults', () => {
698698
expect(controller.getProLayoutPreferences()).toEqual({
699699
orderBookExpanded: false,
700700
chartExpanded: false,
701701
orderBookPosition: 'left',
702702
orderFormPosition: 'right',
703+
positionsSideFilter: 'all',
704+
positionsSortConfig: {
705+
field: 'positionValue',
706+
direction: 'desc',
707+
},
703708
});
704709
});
705710

@@ -711,19 +716,47 @@ describe('PerpsController', () => {
711716
chartExpanded: false,
712717
orderBookPosition: 'left',
713718
orderFormPosition: 'right',
719+
positionsSideFilter: 'all',
720+
positionsSortConfig: {
721+
field: 'positionValue',
722+
direction: 'desc',
723+
},
714724
});
715725
});
716726

717727
it('merges successive partial patches', () => {
718728
controller.setProLayoutPreferences({ orderBookExpanded: true });
719729
controller.setProLayoutPreferences({ orderBookPosition: 'right' });
720730
controller.setProLayoutPreferences({ orderFormPosition: 'left' });
731+
controller.setProLayoutPreferences({ positionsSideFilter: 'long' });
732+
controller.setProLayoutPreferences({
733+
positionsSortConfig: { field: 'unrealizedPnl', direction: 'asc' },
734+
});
721735

722736
expect(controller.getProLayoutPreferences()).toEqual({
723737
orderBookExpanded: true,
724738
chartExpanded: false,
725739
orderBookPosition: 'right',
726740
orderFormPosition: 'left',
741+
positionsSideFilter: 'long',
742+
positionsSortConfig: {
743+
field: 'unrealizedPnl',
744+
direction: 'asc',
745+
},
746+
});
747+
});
748+
749+
it('deep-merges positionsSortConfig so a partial sort patch preserves the other field', () => {
750+
controller.setProLayoutPreferences({
751+
positionsSortConfig: { field: 'fundingRate', direction: 'asc' },
752+
});
753+
controller.setProLayoutPreferences({
754+
positionsSortConfig: { field: 'unrealizedPnl' },
755+
});
756+
757+
expect(controller.getProLayoutPreferences().positionsSortConfig).toEqual({
758+
field: 'unrealizedPnl',
759+
direction: 'asc',
727760
});
728761
});
729762

@@ -746,6 +779,38 @@ describe('PerpsController', () => {
746779
chartExpanded: false,
747780
orderBookPosition: 'left',
748781
orderFormPosition: 'right',
782+
positionsSideFilter: 'all',
783+
positionsSortConfig: {
784+
field: 'positionValue',
785+
direction: 'desc',
786+
},
787+
});
788+
});
789+
790+
it('fills in nested positionsSortConfig defaults when only field is persisted', () => {
791+
controller.testUpdate((state) => {
792+
state.proLayoutPreferences = {
793+
orderBookExpanded: false,
794+
chartExpanded: false,
795+
orderBookPosition: 'left',
796+
orderFormPosition: 'right',
797+
positionsSideFilter: 'short',
798+
positionsSortConfig: {
799+
field: 'fundingRate',
800+
},
801+
} as PerpsControllerState['proLayoutPreferences'];
802+
});
803+
804+
expect(controller.getProLayoutPreferences()).toEqual({
805+
orderBookExpanded: false,
806+
chartExpanded: false,
807+
orderBookPosition: 'left',
808+
orderFormPosition: 'right',
809+
positionsSideFilter: 'short',
810+
positionsSortConfig: {
811+
field: 'fundingRate',
812+
direction: 'desc',
813+
},
749814
});
750815
});
751816
});

packages/perps-controller/tests/src/selectors.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,11 @@ describe('PerpsController selectors', () => {
636636
chartExpanded: false,
637637
orderBookPosition: 'left',
638638
orderFormPosition: 'right',
639+
positionsSideFilter: 'all',
640+
positionsSortConfig: {
641+
field: 'positionValue',
642+
direction: 'desc',
643+
},
639644
};
640645

641646
it('returns the pro-mode layout preferences', () => {
@@ -644,6 +649,11 @@ describe('PerpsController selectors', () => {
644649
chartExpanded: true,
645650
orderBookPosition: 'right' as const,
646651
orderFormPosition: 'left' as const,
652+
positionsSideFilter: 'long' as const,
653+
positionsSortConfig: {
654+
field: 'unrealizedPnl' as const,
655+
direction: 'asc' as const,
656+
},
647657
};
648658
const state = {
649659
proLayoutPreferences,
@@ -665,6 +675,24 @@ describe('PerpsController selectors', () => {
665675
});
666676
});
667677

678+
it('deep-merges nested positionsSortConfig defaults', () => {
679+
const state = {
680+
proLayoutPreferences: {
681+
positionsSideFilter: 'short',
682+
positionsSortConfig: { field: 'fundingRate' },
683+
},
684+
} as unknown as PerpsControllerState;
685+
686+
expect(selectProLayoutPreferences(state)).toStrictEqual({
687+
...defaults,
688+
positionsSideFilter: 'short',
689+
positionsSortConfig: {
690+
field: 'fundingRate',
691+
direction: 'desc',
692+
},
693+
});
694+
});
695+
668696
it('returns defaults when the state slice is missing', () => {
669697
const state = {} as unknown as PerpsControllerState;
670698

0 commit comments

Comments
 (0)