Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
8278450
feat: add multichain account syncing skeleton architecture WIP
mathieuartu Aug 5, 2025
047d6be
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Aug 5, 2025
c3c5177
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Aug 5, 2025
92e7c25
WIP
mathieuartu Aug 5, 2025
1714365
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Aug 5, 2025
d0bdc57
feat: add multichain-account-service peerDep
mathieuartu Aug 5, 2025
afb4846
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Aug 5, 2025
5fd9808
still wip
mathieuartu Aug 5, 2025
34fb3e0
feat: move every piece of logic in extracted methods
mathieuartu Aug 6, 2025
92d9420
feat: better architecture
mathieuartu Aug 6, 2025
4c04cea
fix: lint errors & architecture
mathieuartu Aug 6, 2025
4a081e1
fix: rename file
mathieuartu Aug 6, 2025
01d3d53
feat: implement tracing and analytics
mathieuartu Aug 6, 2025
d81642d
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Aug 6, 2025
d984233
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Aug 6, 2025
b00431b
fix: UT
mathieuartu Aug 6, 2025
fa7a7ae
fix: add proper jsdoc everywhere
mathieuartu Aug 6, 2025
36432ec
feat: improve logs & error management
mathieuartu Aug 6, 2025
6445ff5
fix: improve snapshot system
mathieuartu Aug 6, 2025
8dc9eab
fix: improve comment
mathieuartu Aug 6, 2025
3f261c8
feat: add legacy syncing conflict resolution method
mathieuartu Aug 7, 2025
849e9f7
fix: better conflict resolution logic for legacy syncs
mathieuartu Aug 7, 2025
7d06813
update JSDOC
mathieuartu Aug 7, 2025
1e11b5f
feat: add better conflict resolution for legacy syncing
mathieuartu Aug 7, 2025
29c06e3
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Aug 7, 2025
07dff5d
feat: remove conflict resolution methods as those are not needed anymore
mathieuartu Aug 7, 2025
bd50593
feat: use superstruct to validate user storage data
mathieuartu Aug 19, 2025
18aa59b
feat: add atomic updates support
mathieuartu Aug 19, 2025
0c75fd2
fix: extract event queue
mathieuartu Aug 19, 2025
0c7e038
feat: move everything to BackupAndSyncService
mathieuartu Aug 19, 2025
978b784
feat: update global folder structure and names
mathieuartu Aug 19, 2025
53bcffa
fix: comment out init method - may be unnecessary
mathieuartu Aug 19, 2025
372d070
fix: existing UTs
mathieuartu Aug 19, 2025
271acd9
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Aug 20, 2025
1a46c75
fix: yarn.lock
mathieuartu Aug 20, 2025
ee36c63
fix: update for clearer names
mathieuartu Aug 20, 2025
4653830
fix: add CHANGELOG entry and fix import issue
mathieuartu Aug 20, 2025
6c17c33
fix: update dependency version + profile-sync-controller CHANGELOG
mathieuartu Aug 20, 2025
5978064
fix: tsconfig files
mathieuartu Aug 20, 2025
f6cf802
fix: update yarn.lock
mathieuartu Aug 20, 2025
279444b
fix: do not use subpath exports for profile-sync-controller
mathieuartu Aug 20, 2025
8a715e7
fix: rename state property + better types
mathieuartu Aug 20, 2025
c90e025
feat: add possible remote feature flag usage to user storage controller
mathieuartu Aug 20, 2025
fc19df5
feat: better flows, better types, fix logic errors
mathieuartu Aug 20, 2025
971632b
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Aug 21, 2025
51aa061
fix: yarn.lock
mathieuartu Aug 21, 2025
d098734
feat: add logger + better architecture
mathieuartu Aug 21, 2025
3615811
feat: add contextual logger
mathieuartu Aug 21, 2025
a7f1a1c
fix: update package.json
mathieuartu Aug 21, 2025
8658e68
feat: use RemoteFeatureFlagController to get multichain FF value
mathieuartu Aug 21, 2025
912722d
fix: import order
mathieuartu Aug 21, 2025
df14223
fix: small improvements
mathieuartu Aug 21, 2025
82f3bd8
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Aug 21, 2025
dc34acc
fix: merge artifacts
mathieuartu Aug 21, 2025
acd7058
fix: lint issues
mathieuartu Aug 21, 2025
541d905
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Aug 21, 2025
cea8a34
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 1, 2025
7935b45
chore: add method jsdoc
mathieuartu Sep 1, 2025
94c7382
chore: add unit tests with 100% coverage
mathieuartu Sep 1, 2025
d8294a8
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 1, 2025
54c80a9
chore: update yarn.lock
mathieuartu Sep 1, 2025
e5d5396
chore: fix lint issues
mathieuartu Sep 1, 2025
5fbc662
chore: fix more lint issues
mathieuartu Sep 1, 2025
66e5be2
fix: all lint problems
mathieuartu Sep 1, 2025
7ebff27
fix: import order
mathieuartu Sep 1, 2025
f722537
chore: update package.json
mathieuartu Sep 2, 2025
3400640
feat: push new wallets & groups even without metadata and prepare for…
mathieuartu Sep 2, 2025
dad5461
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 2, 2025
5f9d516
feat: add stripped down version of legacy account syncing to help mig…
mathieuartu Sep 2, 2025
2e63354
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 2, 2025
18ba5c4
fix: update CHANGELOG
mathieuartu Sep 2, 2025
0e02e61
fix: AccountsController ts comment
mathieuartu Sep 2, 2025
7622939
fix: ts-ignore
mathieuartu Sep 2, 2025
f0ec870
fix: ts-ignore
mathieuartu Sep 2, 2025
fb30289
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 2, 2025
93dcb54
fix: revert AccountsController changes
mathieuartu Sep 2, 2025
fd5158f
feat: disable multiple legacy syncs, add more debug logs
mathieuartu Sep 3, 2025
d01278e
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 3, 2025
612e915
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 3, 2025
e50e552
feat: remove UserStorageController account syncing code
mathieuartu Sep 3, 2025
bfd2a71
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 3, 2025
a7f224b
fix: update yarn.lock
mathieuartu Sep 3, 2025
b13da65
fix: lint issue
mathieuartu Sep 3, 2025
1fd8295
fix: remove isAccountSyncingInProgress state property
mathieuartu Sep 3, 2025
236bab4
fix: prevent atomic syncs if big sync has never ran yet + fix legacy …
mathieuartu Sep 3, 2025
d7c088a
fix: better comment to explain group creation no-op in case of error
mathieuartu Sep 4, 2025
f8caa65
fix: PR feedbacks (global logger enablement, ProfileId type, #context…
mathieuartu Sep 8, 2025
a00c446
fix: use imperative for test descriptions
mathieuartu Sep 8, 2025
d5895e7
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 8, 2025
bde2cd2
fix: yarn.lock
mathieuartu Sep 8, 2025
bdceb6d
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 8, 2025
3eef0ed
feat: rename analytics object & types
mathieuartu Sep 8, 2025
0465587
fix: address PR feedbacks
mathieuartu Sep 8, 2025
8a5b6ca
fix: cursor feedback
mathieuartu Sep 8, 2025
024ba1d
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 8, 2025
54bb87c
fix: update CHANGELOG
mathieuartu Sep 8, 2025
50346af
fix: address PR feedbacks
mathieuartu Sep 8, 2025
c667c49
fix: remove remote-feature-flag ref in user-storage-controller + pr f…
mathieuartu Sep 8, 2025
380bb5d
fix: PR feedbacks
mathieuartu Sep 8, 2025
03ab1b9
fix: pr feedbacks
mathieuartu Sep 8, 2025
2173883
fix: pr feedbacks
mathieuartu Sep 8, 2025
4a1458a
fix: pr feedback
mathieuartu Sep 8, 2025
ae627d8
fix: perf improvements
mathieuartu Sep 9, 2025
9b7e519
fix: perf improvements & pr feedbacks
mathieuartu Sep 9, 2025
13fc61c
fix: use metamask/utils logger instead of a custom one
mathieuartu Sep 9, 2025
82849f8
fix: update yarn.lock
mathieuartu Sep 9, 2025
609700f
fix: lint issue
mathieuartu Sep 9, 2025
5bec9d5
fix: update yarn.lock
mathieuartu Sep 9, 2025
3e5cd7e
fix: multiple logic improvements
mathieuartu Sep 9, 2025
5ca78b0
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 9, 2025
c3dafc3
feat: cleanup work + add performFullSyncAtLeastOnce + better promise …
mathieuartu Sep 10, 2025
ea6406b
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 10, 2025
90fa526
fix: remaining pr feedbacks
mathieuartu Sep 11, 2025
9dd05f3
Merge branch 'main' into feat/multichain-account-syncing
mathieuartu Sep 11, 2025
4093fe6
fix: align new (and removed) state properties metadata
mathieuartu Sep 11, 2025
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 @@ -23,7 +23,10 @@ import { KeyringTypes } from '@metamask/keyring-controller';
import type { InternalAccount } from '@metamask/keyring-internal-api';
import type { GetSnap as SnapControllerGetSnap } from '@metamask/snaps-controllers';

import { AccountTreeController } from './AccountTreeController';
import {
AccountTreeController,
getDefaultAccountTreeControllerState,
} from './AccountTreeController';
import type { BackupAndSyncAnalyticsEventPayload } from './backup-and-sync/analytics';
import { BackupAndSyncService } from './backup-and-sync/service';
import { getAccountWalletNameFromKeyringType } from './rules/keyring';
Expand Down Expand Up @@ -225,6 +228,7 @@ function getAccountTreeControllerMessenger(
'AccountsController:getAccount',
'AccountsController:getSelectedAccount',
'AccountsController:setSelectedAccount',
'UserStorageController:getState',
'UserStorageController:performGetStorage',
'UserStorageController:performGetStorageAllFeatureEntries',
'UserStorageController:performSetStorage',
Expand All @@ -248,15 +252,22 @@ function getAccountTreeControllerMessenger(
* @param options.config - Configuration options for the controller.
* @param options.config.backupAndSync - Configuration options for backup and sync.
* @param options.config.backupAndSync.onBackupAndSyncEvent - Event handler for backup and sync events.
* @param options.config.backupAndSync.enableDebugLogging - Flag to enable debug logging.
* @param options.config.backupAndSync.isAccountSyncingEnabled - Flag to enable account syncing.
* @param options.config.backupAndSync.isBackupAndSyncEnabled - Flag to enable backup and sync.
* @returns An object containing the controller instance and the messenger.
*/
function setup({
state = {},
messenger = getRootMessenger(),
accounts = [],
keyrings = [],
config,
config = {
backupAndSync: {
isAccountSyncingEnabled: true,
isBackupAndSyncEnabled: true,
onBackupAndSyncEvent: jest.fn(),
},
},
}: {
state?: Partial<AccountTreeControllerState>;
messenger?: Messenger<
Expand All @@ -267,10 +278,11 @@ function setup({
keyrings?: KeyringObject[];
config?: {
backupAndSync?: {
isAccountSyncingEnabled?: boolean;
isBackupAndSyncEnabled?: boolean;
onBackupAndSyncEvent?: (
event: BackupAndSyncAnalyticsEventPayload,
) => void;
enableDebugLogging?: boolean;
};
};
} = {}): {
Expand Down Expand Up @@ -317,6 +329,7 @@ function setup({
getSelectedAccount: jest.fn(),
},
UserStorageController: {
getState: jest.fn(),
performGetStorage: jest.fn(),
performGetStorageAllFeatureEntries: jest.fn(),
performSetStorage: jest.fn(),
Expand Down Expand Up @@ -372,6 +385,15 @@ function setup({
);

// Mock UserStorageController methods
mocks.UserStorageController.getState.mockImplementation(() => ({
isBackupAndSyncEnabled: config?.backupAndSync?.isBackupAndSyncEnabled,
isAccountSyncingEnabled: config?.backupAndSync?.isAccountSyncingEnabled,
}));
messenger.registerActionHandler(
'UserStorageController:getState',
mocks.UserStorageController.getState,
);

messenger.registerActionHandler(
'UserStorageController:performGetStorage',
mocks.UserStorageController.performGetStorage,
Expand Down Expand Up @@ -2833,11 +2855,12 @@ describe('AccountTreeController', () => {
jest.clearAllMocks();
});

it('calls performFullSync on the syncing service', async () => {
it('calls fullSync on the syncing service', async () => {
// Spy on the BackupAndSyncService constructor and methods
const performFullSyncSpy = jest
.spyOn(BackupAndSyncService.prototype, 'performFullSync')
.mockResolvedValue(undefined);
const fullSyncSpy = jest.spyOn(
BackupAndSyncService.prototype,
'performFullSync',
);

const { controller } = setup({
accounts: [MOCK_HARDWARE_ACCOUNT_1], // Use hardware account to avoid entropy calls
Expand All @@ -2848,12 +2871,12 @@ describe('AccountTreeController', () => {

await controller.syncWithUserStorage();

expect(performFullSyncSpy).toHaveBeenCalledTimes(1);
expect(fullSyncSpy).toHaveBeenCalledTimes(1);
});

it('handles sync errors gracefully', async () => {
const syncError = new Error('Sync failed');
const performFullSyncSpy = jest
const fullSyncSpy = jest
.spyOn(BackupAndSyncService.prototype, 'performFullSync')
.mockRejectedValue(syncError);

Expand All @@ -2867,7 +2890,7 @@ describe('AccountTreeController', () => {
await expect(controller.syncWithUserStorage()).rejects.toThrow(
syncError.message,
);
expect(performFullSyncSpy).toHaveBeenCalledTimes(1);
expect(fullSyncSpy).toHaveBeenCalledTimes(1);
});
});

Expand Down Expand Up @@ -2895,28 +2918,27 @@ describe('AccountTreeController', () => {
expect(controller.state.accountWalletsMetadata).not.toStrictEqual({});

// Clear the metadata
controller.clearPersistedMetadataAndSyncingState();
controller.clearState();

// Verify everything is cleared
expect(controller.state.accountGroupsMetadata).toStrictEqual({});
expect(controller.state.accountWalletsMetadata).toStrictEqual({});
expect(controller.state.hasAccountTreeSyncingSyncedAtLeastOnce).toBe(
false,
expect(controller.state).toStrictEqual(
getDefaultAccountTreeControllerState(),
);
});
});

describe('backup and sync config initialization', () => {
it('initializes backup and sync config with provided analytics callback and debug logging', async () => {
it('initializes backup and sync config with provided analytics callback', async () => {
const mockAnalyticsCallback = jest.fn();

const { controller } = setup({
accounts: [MOCK_HD_ACCOUNT_1],
keyrings: [MOCK_HD_KEYRING_1],
config: {
backupAndSync: {
isAccountSyncingEnabled: true,
isBackupAndSyncEnabled: true,
onBackupAndSyncEvent: mockAnalyticsCallback,
enableDebugLogging: true,
},
},
});
Expand Down
18 changes: 7 additions & 11 deletions packages/account-tree-controller/src/AccountTreeController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1110,19 +1110,15 @@ export class AccountTreeController extends BaseController<
}

/**
* Clears all persisted metadata and syncing state.
*
* This will reset the account groups and wallets metadata, as well as
* the syncing state. It also calls `init()` to reinitialize the controller.
* This should be used when we want to completely reset the controller's state.
* Clears the controller state and resets to default values.
*/
clearPersistedMetadataAndSyncingState(): void {
this.update((state) => {
state.accountGroupsMetadata = {};
state.accountWalletsMetadata = {};
state.hasAccountTreeSyncingSyncedAtLeastOnce = false;
clearState(): void {
this.update(() => {
return {
...getDefaultAccountTreeControllerState(),
};
});
this.init();
this.#backupAndSyncService.clearState();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AtomicSyncQueue } from './atomic-sync-queue';
import { backupAndSyncLogger } from '../../logger';
import type { BackupAndSyncContext } from '../types';

jest.mock('../../logger', () => ({
backupAndSyncLogger: jest.fn(),
Expand All @@ -12,18 +11,10 @@ const mockBackupAndSyncLogger = backupAndSyncLogger as jest.MockedFunction<

describe('BackupAndSync - Service - AtomicSyncQueue', () => {
let atomicSyncQueue: AtomicSyncQueue;
const mockContext = {
controller: {
state: {
isAccountTreeSyncingInProgress: false,
hasAccountTreeSyncingSyncedAtLeastOnce: true,
},
},
} as unknown as BackupAndSyncContext;

beforeEach(() => {
jest.clearAllMocks();
atomicSyncQueue = new AtomicSyncQueue(mockContext);
atomicSyncQueue = new AtomicSyncQueue();
});

afterEach(() => {
Expand All @@ -32,13 +23,13 @@ describe('BackupAndSync - Service - AtomicSyncQueue', () => {

describe('constructor', () => {
it('initializes with default debug logging function', () => {
const queue = new AtomicSyncQueue(mockContext);
const queue = new AtomicSyncQueue();
expect(queue.size).toBe(0);
expect(queue.isProcessing).toBe(false);
});

it('initializes with provided debug logging function', () => {
const queue = new AtomicSyncQueue(mockContext);
const queue = new AtomicSyncQueue();
expect(queue.size).toBe(0);
expect(queue.isProcessing).toBe(false);
});
Expand All @@ -53,52 +44,6 @@ describe('BackupAndSync - Service - AtomicSyncQueue', () => {
expect(atomicSyncQueue.size).toBe(1);
});

it('does not enqueue when big sync is in progress', () => {
const mockContextWithBigSyncInProgress = {
...mockContext,
controller: {
...mockContext.controller,
state: {
...mockContext.controller.state,
isAccountTreeSyncingInProgress: true,
},
},
} as unknown as BackupAndSyncContext;

const atomicSyncQueueWithBigSyncInProgress = new AtomicSyncQueue(
mockContextWithBigSyncInProgress,
);

const mockSyncFunction = jest.fn().mockResolvedValue(undefined);

atomicSyncQueueWithBigSyncInProgress.enqueue(mockSyncFunction);

expect(atomicSyncQueueWithBigSyncInProgress.size).toBe(0);
});

it('does not enqueue if big sync has never been ran', () => {
const mockContextWithNoBigSyncEver = {
...mockContext,
controller: {
...mockContext.controller,
state: {
...mockContext.controller.state,
hasAccountTreeSyncingSyncedAtLeastOnce: false,
},
},
} as unknown as BackupAndSyncContext;

const atomicSyncQueueWithNoBigSyncEver = new AtomicSyncQueue(
mockContextWithNoBigSyncEver,
);

const mockSyncFunction = jest.fn().mockResolvedValue(undefined);

atomicSyncQueueWithNoBigSyncEver.enqueue(mockSyncFunction);

expect(atomicSyncQueueWithNoBigSyncEver.size).toBe(0);
});

it('triggers async processing after enqueueing', async () => {
jest.useFakeTimers();
const mockSyncFunction = jest.fn().mockResolvedValue(undefined);
Expand Down Expand Up @@ -131,32 +76,6 @@ describe('BackupAndSync - Service - AtomicSyncQueue', () => {
expect(atomicSyncQueue.size).toBe(0);
});

it('does not process when big sync is in progress', async () => {
const mockContextWithBigSyncInProgress = {
...mockContext,
controller: {
...mockContext.controller,
state: {
...mockContext.controller.state,
isAccountTreeSyncingInProgress: true,
},
},
} as unknown as BackupAndSyncContext;

const atomicSyncQueueWithBigSyncInProgress = new AtomicSyncQueue(
mockContextWithBigSyncInProgress,
);

const mockSyncFunction = jest.fn().mockResolvedValue(undefined);

atomicSyncQueueWithBigSyncInProgress.enqueue(mockSyncFunction);

await atomicSyncQueueWithBigSyncInProgress.process();

expect(mockSyncFunction).not.toHaveBeenCalled();
expect(atomicSyncQueueWithBigSyncInProgress.size).toBe(0);
});

it('does not process when already processing', async () => {
const mockSyncFunction = jest.fn().mockImplementation(async () => {
// While first function is processing, try to process again
Expand Down Expand Up @@ -241,7 +160,7 @@ describe('BackupAndSync - Service - AtomicSyncQueue', () => {

it('accesses size property correctly', () => {
// Create a fresh queue to test size property
const freshQueue = new AtomicSyncQueue(mockContext);
const freshQueue = new AtomicSyncQueue();
expect(freshQueue.size).toBe(0);

// Add multiple items
Expand Down Expand Up @@ -275,6 +194,19 @@ describe('BackupAndSync - Service - AtomicSyncQueue', () => {
error,
);
});

it('rejects promise when awaited sync function throws error', async () => {
const error = new Error('Sync function failed');
const mockSyncFunction = jest.fn().mockRejectedValue(error);

const promise = atomicSyncQueue.enqueue(mockSyncFunction, {
await: true,
});

await expect(promise).rejects.toThrow('Sync function failed');
expect(mockSyncFunction).toHaveBeenCalled();
});

it('handles empty queue after shift operation', async () => {
// Test the scenario where shift() might return undefined/null
// This can happen in race conditions or edge cases
Expand Down
Loading