Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
36b3e2b
feat(ramps-controller): add V2 ramps order syncing via User Storage (…
cursoragent Jul 27, 2026
f58d4bc
Merge remote-tracking branch 'origin/main' into feat/ramps-order-syncing
georgeweiler Jul 27, 2026
82adc61
fix(ramps-controller): restore controller wiring dropped in squash
georgeweiler Jul 27, 2026
7017f31
style(ramps-controller): oxfmt RampsController sync wiring
georgeweiler Jul 27, 2026
0cd9222
fix(ramps-controller): ignore local-only paymentDetails in areOrdersE…
georgeweiler Jul 27, 2026
1c0291a
refactor(ramps-controller): drop cross-SRP order aggregation
georgeweiler Jul 27, 2026
7e8b986
refactor(ramps-controller): use fast-deep-equal for order equality
georgeweiler Jul 27, 2026
fd1c7ee
docs(ramps-controller): consolidate order sync changelog
georgeweiler Jul 27, 2026
bcbcdc1
chore(ramps-controller): update yarn.lock for fast-deep-equal
georgeweiler Jul 27, 2026
d3f718f
Merge remote-tracking branch 'origin/main' into feat/ramps-order-syncing
Copilot Jul 28, 2026
29505d9
fix(ramps-controller): bump lastUpdatedAt and trigger sync on polling…
cursoragent Jul 28, 2026
7630fa0
Merge remote-tracking branch 'origin/main' into feat/ramps-order-syncing
georgeweiler Aug 11, 2026
8c48207
fix(ramps-controller): harden order sync mid-upload and incremental e…
georgeweiler Aug 11, 2026
7dc5304
fix(ramps-controller): preserve sync delete intent
georgeweiler Aug 11, 2026
a036390
fix(ramps-controller): bump timestamp for external mid-sync edits
georgeweiler Aug 11, 2026
9b95933
feat(ramps-controller): add V2 ramps order syncing via User Storage (…
cursoragent Jul 27, 2026
f67d9c8
fix(ramps-controller): restore controller wiring dropped in squash
georgeweiler Jul 27, 2026
6f1553a
style(ramps-controller): oxfmt RampsController sync wiring
georgeweiler Jul 27, 2026
67a2aa0
fix(ramps-controller): ignore local-only paymentDetails in areOrdersE…
georgeweiler Jul 27, 2026
59587e0
refactor(ramps-controller): drop cross-SRP order aggregation
georgeweiler Jul 27, 2026
832127a
refactor(ramps-controller): use fast-deep-equal for order equality
georgeweiler Jul 27, 2026
4374080
docs(ramps-controller): consolidate order sync changelog
georgeweiler Jul 27, 2026
7b2814d
chore(ramps-controller): update yarn.lock for fast-deep-equal
georgeweiler Jul 27, 2026
46387f8
fix(ramps-controller): bump lastUpdatedAt and trigger sync on polling…
cursoragent Jul 28, 2026
3f9138d
fix(ramps-controller): harden order sync mid-upload and incremental e…
georgeweiler Aug 11, 2026
90c589d
fix(ramps-controller): preserve sync delete intent
georgeweiler Aug 11, 2026
1be137d
fix(ramps-controller): bump timestamp for external mid-sync edits
georgeweiler Aug 11, 2026
66a51f7
fix(ramps-controller): normalize persisted order timestamps
georgeweiler Sep 3, 2026
a72a287
Merge remote-tracking branch 'origin/feat/ramps-order-syncing' into f…
georgeweiler Sep 3, 2026
4458638
Merge remote-tracking branch 'origin/main' into feat/ramps-order-syncing
georgeweiler Sep 3, 2026
2d576f0
fix(ramps-controller): drain queued sync after failure
georgeweiler Sep 4, 2026
c4d2f6b
Merge remote-tracking branch 'origin/main' into feat/ramps-order-syncing
georgeweiler Sep 9, 2026
b16c13b
fix(ramps-controller): harden order sync races
georgeweiler Sep 9, 2026
4dfc75d
fix(ramps-controller): skip User Storage writes on unchanged polls
georgeweiler Sep 9, 2026
c58a14f
Merge origin/main into feat/ramps-order-syncing
georgeweiler Sep 9, 2026
bd505ff
fix(ramps-controller): resolve no-shadow and regenerate action types
georgeweiler Sep 9, 2026
a200d92
Merge origin/main into feat/ramps-order-syncing
georgeweiler Sep 10, 2026
6b4819f
fix(ramps): merge paymentDetails when only local-only fields change
cursoragent Sep 10, 2026
1bcfd7d
fix: include isRampsSyncingEnabled in full-state test literals
georgeweiler Sep 10, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -5680,6 +5680,7 @@ describe('AccountTreeController', () => {
isBackupAndSyncUpdateLoading: false,
isContactSyncingEnabled: false,
isContactSyncingInProgress: false,
isRampsSyncingEnabled: true,
},
[],
);
Expand Down
10 changes: 10 additions & 0 deletions packages/profile-sync-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `BACKUPANDSYNC_FEATURES.rampsSyncing` and `UserStorageController.isRampsSyncingEnabled` ([#9474](https://github.com/MetaMask/core/pull/9474))
- Defaults to enabled (`true`); hosts can toggle Buy & sell order sync independently of account/contact sync

### Changed

- **BREAKING:** `UserStorageControllerState` now includes `isRampsSyncingEnabled`. Consumers that construct full state objects must include this field. ([#9474](https://github.com/MetaMask/core/pull/9474))
- Prefer `isRampsSyncingEnabled ?? true` in selectors so wallets upgraded before this field existed keep ramps syncing on by default

## [31.0.0]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -604,11 +604,10 @@ describe('UserStorageController', () => {
const controller = new UserStorageController({
messenger: messengerMocks.messenger,
state: {
...defaultState,
isBackupAndSyncEnabled: false,
isBackupAndSyncUpdateLoading: false,
isAccountSyncingEnabled: false,
isContactSyncingEnabled: false,
isContactSyncingInProgress: false,
},
});

Expand All @@ -629,11 +628,10 @@ describe('UserStorageController', () => {
const controller = new UserStorageController({
messenger: messengerMocks.messenger,
state: {
...defaultState,
isBackupAndSyncEnabled: false,
isBackupAndSyncUpdateLoading: false,
isAccountSyncingEnabled: false,
isContactSyncingEnabled: false,
isContactSyncingInProgress: false,
},
});

Expand Down Expand Up @@ -663,6 +661,7 @@ describe('UserStorageController', () => {
isAccountSyncingEnabled: true,
isContactSyncingEnabled: true,
isContactSyncingInProgress: false,
isRampsSyncingEnabled: true,
},
});

Expand All @@ -674,6 +673,31 @@ describe('UserStorageController', () => {
expect(controller.state.isAccountSyncingEnabled).toBe(false);
expect(controller.state.isBackupAndSyncEnabled).toBe(true);
});

it('should not disable backup and sync when disabling ramps syncing', async () => {
const { messengerMocks } = await arrangeMocks();
messengerMocks.mockAuthIsSignedIn.mockReturnValue(false);

const controller = new UserStorageController({
messenger: messengerMocks.messenger,
state: {
isBackupAndSyncEnabled: true,
isBackupAndSyncUpdateLoading: false,
isAccountSyncingEnabled: true,
isContactSyncingEnabled: true,
isContactSyncingInProgress: false,
isRampsSyncingEnabled: true,
},
});

await controller.setIsBackupAndSyncFeatureEnabled(
BACKUPANDSYNC_FEATURES.rampsSyncing,
false,
);

expect(controller.state.isRampsSyncingEnabled).toBe(false);
expect(controller.state.isBackupAndSyncEnabled).toBe(true);
});
});

describe('error handling edge cases', () => {
Expand Down Expand Up @@ -917,6 +941,7 @@ describe('metadata', () => {
"isAccountSyncingEnabled": true,
"isBackupAndSyncEnabled": true,
"isContactSyncingEnabled": true,
"isRampsSyncingEnabled": true,
}
`);
});
Expand All @@ -937,6 +962,7 @@ describe('metadata', () => {
"isAccountSyncingEnabled": true,
"isBackupAndSyncEnabled": true,
"isContactSyncingEnabled": true,
"isRampsSyncingEnabled": true,
}
`);
});
Expand All @@ -953,6 +979,7 @@ describe('metadata', () => {
"isAccountSyncingEnabled": true,
"isBackupAndSyncEnabled": true,
"isContactSyncingEnabled": true,
"isRampsSyncingEnabled": true,
}
`);
});
Expand All @@ -975,6 +1002,7 @@ describe('metadata', () => {
"isBackupAndSyncUpdateLoading": false,
"isContactSyncingEnabled": true,
"isContactSyncingInProgress": false,
"isRampsSyncingEnabled": true,
}
`);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export type UserStorageControllerState = {
* Condition used by UI to determine if contact syncing is in progress.
*/
isContactSyncingInProgress: boolean;
/**
* Condition used by UI to determine if ramps order syncing is enabled.
*/
isRampsSyncingEnabled: boolean;
Comment thread
georgeweiler marked this conversation as resolved.
};

export const defaultState: UserStorageControllerState = {
Expand All @@ -82,6 +86,7 @@ export const defaultState: UserStorageControllerState = {
isAccountSyncingEnabled: true,
isContactSyncingEnabled: true,
isContactSyncingInProgress: false,
isRampsSyncingEnabled: true,
};

const metadata: StateMetadata<UserStorageControllerState> = {
Expand Down Expand Up @@ -115,6 +120,12 @@ const metadata: StateMetadata<UserStorageControllerState> = {
includeInDebugSnapshot: false,
usedInUi: true,
},
isRampsSyncingEnabled: {
includeInStateLogs: true,
persist: true,
includeInDebugSnapshot: true,
usedInUi: true,
},
};

type ControllerConfig = {
Expand Down Expand Up @@ -640,6 +651,10 @@ export class UserStorageController extends BaseController<
if (feature === BACKUPANDSYNC_FEATURES.contactSyncing) {
state.isContactSyncingEnabled = enabled;
}

if (feature === BACKUPANDSYNC_FEATURES.rampsSyncing) {
state.isRampsSyncingEnabled = enabled;
}
});
} catch (e) {
// istanbul ignore next
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const BACKUPANDSYNC_FEATURES = {
main: 'main',
accountSyncing: 'accountSyncing',
contactSyncing: 'contactSyncing',
rampsSyncing: 'rampsSyncing',
} as const;

/**
Expand Down
21 changes: 21 additions & 0 deletions packages/ramps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add V2 ramps order syncing with User Storage ([#9474](https://github.com/MetaMask/core/pull/9474))
- Synchronize orders across clients for the same SRP using timestamp-based last-write-wins conflict resolution, soft-delete tombstones, and incremental add/update/delete pushes
- Feature key: `rampsOrders`; hosts call `RampsController:syncOrdersWithUserStorage` on unlock when Backup & Sync + ramps syncing are enabled
- Persist optional `lastUpdatedAt` on local `RampsOrder` entries for LWW (not returned by the V2 API)
- Strip `paymentDetails` from remote payloads (PII stays local-only)
- Soft deletes use remote tombstones; retention matches contact sync (no remote purge/compaction)
- Mid-sync local mutations coalesce into a follow-up full sync pass so uploads are not dropped during `performBatchSetStorage`
- Polling via `getOrder` → `addOrder` stamps `lastUpdatedAt` and writes to User Storage only when the syncable payload changed
- Normalize ISO and numeric-string `createdAt` values from Portfolio and older clients to epoch milliseconds
- Optional `onOrderSyncErroneousSituation` (full sync and incremental push/delete) and `trace` callbacks

### Changed

- **BREAKING:** `RampsControllerMessenger` now requires these actions to be delegated for order syncing ([#9474](https://github.com/MetaMask/core/pull/9474)):
- `UserStorageController:getState`
- `UserStorageController:performGetStorageAllFeatureEntries`
- `UserStorageController:performBatchSetStorage`
- `AuthenticationController:isSignedIn`

## [21.0.0]

### Changed
Expand Down
3 changes: 2 additions & 1 deletion packages/ramps-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"@metamask/controller-utils": "^13.0.0",
"@metamask/messenger": "^3.0.0",
"@metamask/profile-sync-controller": "^31.0.0",
"@metamask/remote-feature-flag-controller": "^7.0.0"
"@metamask/remote-feature-flag-controller": "^7.0.0",
"fast-deep-equal": "^3.1.3"
},
"devDependencies": {
"@metamask/auto-changelog": "^6.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ export type RampsControllerGetQuotesAction = {
* If an order with the same internal order code already exists, the incoming
* fields are merged on top of the existing order so that fields not present
* in the update (e.g. paymentDetails from the Transak API) are preserved.
* Unchanged syncable payloads (including unchanged poll results) are ignored
* so `lastUpdatedAt` is not bumped and User Storage is not rewritten.
*
* @param order - The RampsOrder to add or update.
*/
Expand All @@ -324,6 +326,20 @@ export type RampsControllerRemoveOrderAction = {
handler: RampsController['removeOrder'];
};

/**
* Bidirectionally syncs V2 ramps orders with User Storage.
* Hosts should call this on unlock / when ramps syncing is enabled.
*
* Overlapping calls are coalesced into the in-flight worker. After the worker
* settles, this method loops when `#orderSyncQueued` is still set so a
* request that arrived between the worker's last loop check and promise
* resolution is not dropped.
*/
export type RampsControllerSyncOrdersWithUserStorageAction = {
type: `RampsController:syncOrdersWithUserStorage`;
handler: RampsController['syncOrdersWithUserStorage'];
};

/**
* Adds or updates a local autoramp last-seen cursor (e.g. after create).
*
Expand Down Expand Up @@ -832,6 +848,7 @@ export type RampsControllerMethodActions =
| RampsControllerGetQuotesAction
| RampsControllerAddOrderAction
| RampsControllerRemoveOrderAction
| RampsControllerSyncOrdersWithUserStorageAction
| RampsControllerAddAutorampAction
| RampsControllerCreateAutorampAction
| RampsControllerRegisterMoneyAccountWalletAction
Expand Down
Loading