Skip to content

feat(account-tree-controller): add payload/snapshot support - #9826

Merged
ccharly merged 12 commits into
mainfrom
cc/feat/account-tree-export-import-payload-and-snapshot
Aug 12, 2026
Merged

feat(account-tree-controller): add payload/snapshot support#9826
ccharly merged 12 commits into
mainfrom
cc/feat/account-tree-export-import-payload-and-snapshot

Conversation

@ccharly

@ccharly ccharly commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Explanation

First part of the new :{import,export}State actions for the account-tree.

This PR only exposes the new payload/snapshot types that will be used to import/export.

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Touches secret-bearing payload shapes and validation redaction for mnemonics/private keys; behavior is new and isolated to types/tests until controller actions land.

Overview
Introduces a new state/ layer for upcoming account-tree import/export: a versioned AccountTreePayload (v1, mnemonic + private-key wallets), IdMap for local controller IDs ↔ stable wallet:… payload IDs, and an immutable AccountTreeSnapshot for serialize/deserialize and selective export via filterWallets / filterGroups / filterAllGroups.

payload.ts defines portable wallet/group shapes (optional secrets), Superstruct validation with fail-closed version pinning, contiguous mnemonic group indices, and sensitive() on mnemonic/private-key fields so validation errors do not leak secrets. utils adds byte encoding helpers, deepFreeze, and safe validation error formatting.

Controller exportState / importState are not wired in this PR—only the types and snapshot machinery plus tests.

Reviewed by Cursor Bugbot for commit 7aaee50. Bugbot is set up for automated code reviews on this repo. Configure here.

@ccharly

ccharly commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Using no-changelog for this one, since it's purely internal for the moment. I'll make sure to update the main PR with this PR reference:

See: 9e0e993

@ccharly
ccharly marked this pull request as ready for review August 11, 2026 16:28
@ccharly
ccharly requested a review from a team as a code owner August 11, 2026 16:28
Comment thread packages/account-tree-controller/src/state/id-map.ts
Comment thread packages/account-tree-controller/src/state/snapshot.ts
@ccharly
ccharly enabled auto-merge August 11, 2026 17:11

@gantunesr gantunesr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall

  • Would be good to have an integration test for the flow export→filter→serialize→deserialize. This is probably covered in the next PR for the controller wiring

Comment thread packages/account-tree-controller/src/state/payload.ts
Comment on lines +212 to +215
export type ExportStateOptions = {
/** When `true`, secrets (mnemonic / private keys) are included. Requires the vault to be unlocked. */
includeSecrets?: boolean;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think there is any explicit cleanup/teardown to do when secrets are exported?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To like clearing them out of memory?

Like you'd like to have them being passed as options to control this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I investigated this a bit with claude.

The real problem is that we use raw string for the secrets, and it makes it hard to zero-ing memory completely with those (since we don't own the real memory buffer for those, only the view/value on it).

If we really wanted to do this, we would need to use real buffer types like ArrayBuffer or Uint8Array. But that means our payload will need special encoding functions (at least, to make it compatible with JSON-encoding for example).

Like we could have a .stringify() and .parse(json) for example.

But that would yet another steps before/after the snapshot's serialize and deserialize.

@ccharly ccharly Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussing a bit about this internally, I actually went for a different way of encoding those in our payload, so we "limit" the window of when those sensitive values are used.

They are just encoded differently, so it's more or less security-by-obscurity, but it's slightly better than having them in clear I think (and that's aligned with how we encode them elsewhere in the codebase)

ccc122e

* @throws If `raw` is not a valid payload or its version is unsupported.
*/
static async deserialize(raw: unknown): Promise<AccountTreeSnapshot> {
// TODO: Use migration framework here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to omit the migration part for now. My plan was to use our new generic migration framework that we created for keyrings. Though, it's not needed here (we expect a v1 payload + the migration framework needs a slight rework around version handling + flattening the state/data being migrated).

Delaying this work for a bit to not block this PR.

Comment thread packages/account-tree-controller/src/state/payload.test.ts
Comment thread packages/account-tree-controller/src/state/utils.test.ts
@ccharly

ccharly commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Looks good overall

  • Would be good to have an integration test for the flow export→filter→serialize→deserialize. This is probably covered in the next PR for the controller wiring

We indeed have some round-trips "integration" tests on the controller itself, to make sure the combo :exportState and :importState can be used together.

If needed, I'll add even more scenarios to also include filtering in this mix!

gantunesr
gantunesr previously approved these changes Aug 12, 2026
@ccharly
ccharly added this pull request to the merge queue Aug 12, 2026
@ccharly
ccharly removed this pull request from the merge queue due to a manual request Aug 12, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7aaee50. Configure here.

value: exactOptional(sensitive(BytesStruct)),
metadata: AccountWalletPayloadMetadataStruct,
groups: AccountWalletMnemonicGroupsStruct,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Group IDs lack cross-field checks

Medium Severity

Mnemonic group validation checks contiguous groupIndex values but never checks that each group id belongs to the parent wallet or that its sub-id matches groupIndex, even though the documented format is &lt;walletPayloadId&gt;/&lt;groupIndex&gt;. Inconsistent ids therefore pass deserialize and can desync import mapping from derivation indices.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7aaee50. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true even though we are the producer of this payload, so we "know" we won't be doing that.

We can fix that with refine and a stricter validation, but that's not vital honestly.

@ccharly
ccharly enabled auto-merge August 12, 2026 19:20
@ccharly
ccharly added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit c6f8528 Aug 12, 2026
276 of 280 checks passed
@ccharly
ccharly deleted the cc/feat/account-tree-export-import-payload-and-snapshot branch August 12, 2026 21:40
pull Bot pushed a commit to dmrazzy/core that referenced this pull request Aug 14, 2026
…etaMask#9663)

## Explanation

New `{export,import}State` actions.

This is the implementation for this ADR:
- MetaMask/decisions#231

## References

- ADR MetaMask/decisions#231
- Split to:
  - MetaMask#9826
  - MetaMask#9863
  - MetaMask#9864

## Checklist

- [ ] I've updated the test suite for new or updated code as appropriate
- [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [ ] I've communicated my changes to consumers by [updating changelogs
for packages I've
changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [ ] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> Touches secret export/import and keyring/multichain wallet creation;
incorrect handling could leak or corrupt credentials or account
metadata.
> 
> **Overview**
> **Adds `AccountTreeController:exportState` and
`AccountTreeController:importState`** so clients can snapshot and
restore the account tree via a **version-1 wire format**
(`AccountTreeSnapshot`, payload types, and `IdMap` are re-exported from
the package).
> 
> `exportState` builds a snapshot of wallets/groups (names, pinned,
hidden, etc.); with **`includeSecrets: true`** and an unlocked vault it
can include mnemonics and private keys. **`exportState` rejects when the
vault is locked**, including metadata-only exports. `importState`
accepts only validated `AccountTreeSnapshot` instances (untrusted data
must use `deserialize` first); it can create new HD wallets via
`MultichainAccountService` and private-key accounts via
`KeyringController`, then apply metadata to existing and new entries.
> 
> Tests add messenger action coverage, a controller-level metadata
round-trip, dedicated **export→serialize→deserialize→import**
integration tests for mnemonic/private-key secrets, and test harness
updates (`KeyringController:withController`, multichain create actions,
shared wallet fixtures).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
6078457. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants