Skip to content

Conversation

@mathieuartu
Copy link
Contributor

@mathieuartu mathieuartu commented Dec 10, 2025

Related to: https://consensyssoftware.atlassian.net/browse/MUL-1320

Examples


Note

Adds a V2 wrapper around the legacy QR keyring to expose accounts via the unified API and support controlled account creation.

  • New QrKeyringV2 (extends EthKeyringWrapper) implementing KeyringV2 with capabilities for EOA and BIP-44 derive-index
  • Implements getAccounts, createAccounts (HD-only, validates entropy source), deleteAccount, serialize/deserialize; treats Account-mode as private-key accounts
  • Legacy QrKeyring gains getPathFromAddress used for HD derivation metadata
  • Export QrKeyringV2 from package; add deps on @metamask/keyring-api and @metamask/account-api; update tsconfig references and README graph
  • Comprehensive tests for HD and Account modes, error cases, and caching

Written by Cursor Bugbot for commit a5fa59d. This will update automatically on new commits. Configure here.

@mathieuartu mathieuartu self-assigned this Dec 10, 2025
@mathieuartu mathieuartu changed the title feat: Add QrKeyringV2 wrapper feat(keyring-eth-qr): Add QrKeyringV2 wrapper Dec 10, 2025
@mathieuartu mathieuartu changed the title feat(keyring-eth-qr): Add QrKeyringV2 wrapper feat(keyring-eth-qr): add QrKeyringV2 wrapper Dec 10, 2025
@mathieuartu mathieuartu changed the title feat(keyring-eth-qr): add QrKeyringV2 wrapper feat: add QrKeyringV2 wrapper Dec 10, 2025
@mathieuartu mathieuartu marked this pull request as ready for review December 10, 2025 14:33
@mathieuartu mathieuartu requested a review from a team as a code owner December 10, 2025 14:33
entropy: {
type: KeyringAccountEntropyTypeOption.Mnemonic,
id: this.entropySource,
groupIndex: addressIndex,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the addressIndex only make sense for DeviceMode.HD? Since for DeviceMode.ACCOUNT we're defaulting to the arrayIndex, this won't really match the real account/address index used during the derivation?

This might also be pretty hard to go from derivation path -> index if the "QR device" can use any arbitrary path. So I wonder if DeviceMode.ACCOUNT should be treated as private key here? 🤔

NOTE: I might have misunderstood the legacy QR keyring, so correct me if I'm wrong 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm a bit confused here as well. I'll set up something internally so we can share knowledge around this area 😅

Comment on lines 189 to 192
const currentAccounts = await this.getAccounts();
const existingAccount = currentAccounts.find(
(account) => account.options.entropy.groupIndex === targetIndex,
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Following-up my previous questions, we would have to consider only BIP-44 accounts in this case.

Comment on lines +198 to +199
this.inner.setAccountToUnlock(targetIndex);
const [newAddress] = await this.inner.addAccounts(1);
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: (Might be a bit too much) but, I wonder if we should reset the #accountToUnlock back to what it was here? Like if for some reason the previous value had to be kept, then, our implementation side effect could have an impact on that? 🤔

Basically I'm thinking about something like:

const previousAccountToUnlock = this.inner.getAccountToUnlock();
let newAddress;
try {
  this.inner.setAccountToUnlock(targetIndex);
  [newAddress] = await this.inner.addAccounts(1);
} finally {
  this.inner.setAccountToUnlock(previousAccountToUnlock);
}

But even this, is not entirely concurrent-safe (if another addAccounts happens in parallel 🙃).

Copy link
Contributor

Choose a reason for hiding this comment

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

We can discuss this internally, but I think the legacy implementation is a bit fragile regarding this logic anyway, like, if you use setAccountToUnlock(n) and addAccounts(2), this will try to create the same account twice (since #accountToUnlock is not cleared after creating an account 😅)

The legacy logic should probably be fixed to only accept addAccounts(1) if #accountToUnlock is set!

But our wrapper implementation is good otherwise!

}

// Derive the account at the specified index
this.inner.setAccountToUnlock(targetIndex);
Copy link

Choose a reason for hiding this comment

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

Missing validation for groupIndex allows invalid derivation paths

Medium Severity

The createAccounts method accepts options.groupIndex without validating it's a non-negative integer. Negative numbers, NaN, Infinity, or non-integers (like 1.5) are passed directly to setAccountToUnlock and ultimately used to construct a BIP-32 derivation path (e.g., m/0/-1 or m/0/NaN). The underlying hdkey library will fail with a confusing error or potentially produce incorrect behavior when attempting to derive from these invalid paths.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants