-
-
Notifications
You must be signed in to change notification settings - Fork 297
feat(account-tree-controller): add payload/snapshot support #9826
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ccharly
merged 12 commits into
main
from
cc/feat/account-tree-export-import-payload-and-snapshot
Aug 12, 2026
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
f380090
feat(account-tree-controller): add payload/snapshot support
ccharly bf35e90
chore: revert exported types (will get exported in final PR)
ccharly 4a3eef7
refactor: move formatValidationErrorMessages to utils
ccharly 8bca2e7
fix: use assert to prevent overwrites in id-map
ccharly e2009c4
fix: add comments about contiguous account groups for mnemonic wallets
ccharly 585b045
test: more test about secret redaction
ccharly 2db451c
refactor: cosmetic
ccharly ee63cf4
test: even more tests about secret redaction (2 layers)
ccharly e577067
chore: lint
ccharly d611e59
test: fix test with sensitive redaction and formatValidationErrorMess…
ccharly ccc122e
refactor: use encoded bytes for mnemonics/private-key values
ccharly 7aaee50
Merge branch 'main' into cc/feat/account-tree-export-import-payload-a…
ccharly File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
104 changes: 104 additions & 0 deletions
104
packages/account-tree-controller/src/state/id-map.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { IdMap } from './id-map.js'; | ||
|
|
||
| describe('IdMap', () => { | ||
| describe('constructor', () => { | ||
| it('creates an empty map when called with no arguments', () => { | ||
| const map = new IdMap(); | ||
| expect(map.getPayloadId('entropy:wallet-1')).toBeUndefined(); | ||
| expect(map.getLocalId('wallet:entropy-source-1')).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('pre-populates the map from the provided entries', () => { | ||
| const map = new IdMap([ | ||
| ['entropy:wallet-1', 'wallet:entropy-source-1'], | ||
| ['keyring:wallet-2', 'wallet:private-key'], | ||
| ]); | ||
| expect(map.getPayloadId('entropy:wallet-1')).toBe( | ||
| 'wallet:entropy-source-1', | ||
| ); | ||
| expect(map.getPayloadId('keyring:wallet-2')).toBe('wallet:private-key'); | ||
| expect(map.getLocalId('wallet:entropy-source-1')).toBe( | ||
| 'entropy:wallet-1', | ||
| ); | ||
| expect(map.getLocalId('wallet:private-key')).toBe('keyring:wallet-2'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('add', () => { | ||
| it('registers a local-to-payload pair and its reverse', () => { | ||
| const map = new IdMap(); | ||
| map.add('entropy:wallet-1', 'wallet:entropy-source-1'); | ||
| expect(map.getPayloadId('entropy:wallet-1')).toBe( | ||
| 'wallet:entropy-source-1', | ||
| ); | ||
| expect(map.getLocalId('wallet:entropy-source-1')).toBe( | ||
| 'entropy:wallet-1', | ||
| ); | ||
| }); | ||
|
|
||
| it('throws if the same local ID is registered twice', () => { | ||
| const map = new IdMap(); | ||
| map.add('entropy:wallet-1', 'wallet:entropy-source-1'); | ||
| expect(() => | ||
| map.add('entropy:wallet-1', 'wallet:entropy-source-2'), | ||
| ).toThrow('Local ID already registered: entropy:wallet-1'); | ||
| }); | ||
|
|
||
| it('throws if the same payload ID is registered twice', () => { | ||
| const map = new IdMap(); | ||
| map.add('entropy:wallet-1', 'wallet:entropy-source-1'); | ||
| expect(() => | ||
| map.add('entropy:wallet-2', 'wallet:entropy-source-1'), | ||
| ).toThrow('Payload ID already registered: wallet:entropy-source-1'); | ||
| }); | ||
|
|
||
| it('handles wallet and group IDs in the same map', () => { | ||
| const map = new IdMap(); | ||
| map.add('entropy:wallet-1', 'wallet:entropy-source-1'); | ||
| map.add('entropy:wallet-1/0', 'wallet:entropy-source-1/0'); | ||
| expect(map.getPayloadId('entropy:wallet-1')).toBe( | ||
| 'wallet:entropy-source-1', | ||
| ); | ||
| expect(map.getPayloadId('entropy:wallet-1/0')).toBe( | ||
| 'wallet:entropy-source-1/0', | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('getPayloadId', () => { | ||
| it('returns the payload ID for a known local wallet ID', () => { | ||
| const map = new IdMap([['entropy:wallet-1', 'wallet:entropy-source-1']]); | ||
| expect(map.getPayloadId('entropy:wallet-1')).toBe( | ||
| 'wallet:entropy-source-1', | ||
| ); | ||
| }); | ||
|
|
||
| it('returns undefined for an unknown local ID', () => { | ||
| const map = new IdMap(); | ||
| expect(map.getPayloadId('entropy:wallet-unknown')).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('getLocalId', () => { | ||
| it('returns the local ID for a known payload wallet ID', () => { | ||
| const map = new IdMap([['entropy:wallet-1', 'wallet:entropy-source-1']]); | ||
| expect(map.getLocalId('wallet:entropy-source-1')).toBe( | ||
| 'entropy:wallet-1', | ||
| ); | ||
| }); | ||
|
|
||
| it('returns undefined for an unknown payload ID', () => { | ||
| const map = new IdMap(); | ||
| expect(map.getLocalId('wallet:entropy-source-unknown')).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('returns the local ID for a group payload ID', () => { | ||
| const map = new IdMap([ | ||
| ['entropy:wallet-1/0', 'wallet:entropy-source-1/0'], | ||
| ]); | ||
| expect(map.getLocalId('wallet:entropy-source-1/0')).toBe( | ||
| 'entropy:wallet-1/0', | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import type { AccountGroupId, AccountWalletId } from '@metamask/account-api'; | ||
| import { assert } from '@metamask/utils'; | ||
|
|
||
| import type { | ||
| AccountGroupPayloadId, | ||
| AccountWalletPayloadId, | ||
| } from './payload.js'; | ||
|
|
||
| type LocalId = AccountWalletId | AccountGroupId; | ||
| type PayloadId = AccountWalletPayloadId | AccountGroupPayloadId; | ||
|
|
||
| /** | ||
| * Bidirectional map between local controller IDs and portable payload IDs. | ||
| * | ||
| * Populated during {@link exportState} so that callers can bridge between | ||
| * device-local wallet/group IDs and the stable cross-device IDs that appear | ||
| * in a serialized {@link AccountTreePayload}. | ||
| */ | ||
| export class IdMap { | ||
| readonly #localToPayload: Map<LocalId, PayloadId> = new Map(); | ||
|
|
||
| readonly #payloadToLocal: Map<PayloadId, LocalId> = new Map(); | ||
|
|
||
| /** | ||
| * @param entries - Optional seed pairs `[localId, payloadId]` to pre-populate the map. | ||
| */ | ||
| constructor(entries: [localId: LocalId, payloadId: PayloadId][] = []) { | ||
| for (const [localId, payloadId] of entries) { | ||
| this.add(localId, payloadId); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Registers a local↔payload ID pair. | ||
| * | ||
| * @param localId - Local controller wallet or group ID. | ||
| * @param payloadId - Corresponding portable payload ID. | ||
| */ | ||
| add(localId: LocalId, payloadId: PayloadId): void { | ||
| // Each local ID maps to exactly one payload ID and vice versa: wallet and | ||
| // group IDs are unique by construction, so a duplicate signals a bug in the | ||
| // caller (e.g. exportState visiting the same node twice). | ||
| assert( | ||
| !this.#localToPayload.has(localId), | ||
| `Local ID already registered: ${localId}`, | ||
| ); | ||
| assert( | ||
| !this.#payloadToLocal.has(payloadId), | ||
| `Payload ID already registered: ${payloadId}`, | ||
| ); | ||
| this.#localToPayload.set(localId, payloadId); | ||
| this.#payloadToLocal.set(payloadId, localId); | ||
| } | ||
|
|
||
| /** | ||
| * Returns the portable payload ID for a given local ID. | ||
| * | ||
| * @param localId - Local controller wallet or group ID. | ||
| * @returns The payload ID, or `undefined` if not registered. | ||
| */ | ||
| getPayloadId(localId: LocalId): PayloadId | undefined { | ||
| return this.#localToPayload.get(localId); | ||
| } | ||
|
|
||
| /** | ||
| * Returns the local controller ID for a given payload ID. | ||
| * | ||
| * @param payloadId - Portable payload ID. | ||
| * @returns The local wallet or group ID, or `undefined` if not registered. | ||
| */ | ||
| getLocalId(payloadId: PayloadId): LocalId | undefined { | ||
| return this.#payloadToLocal.get(payloadId); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.