Skip to content

feat: add qr keyring package#60

Merged
mikesposito merged 52 commits into
mainfrom
add-qr-keyring
Aug 12, 2025
Merged

feat: add qr keyring package#60
mikesposito merged 52 commits into
mainfrom
add-qr-keyring

Conversation

@mikesposito

@mikesposito mikesposito commented Sep 25, 2024

Copy link
Copy Markdown
Member

This is a new Keyring implementation compatible with QR-based wallets, meant to replace @keystonehq/metamask-airgapped-keyring.

The goals are:

  • Internalize the package
  • Simplify internal logic (e.g. no event-based logic)
  • Remove the need of KeyringController special methods (See methods nuked in chore(keyring-controller): remove QRKeyring-related code core#6031)
  • Provide a consumer-injectable transport bridge to allow clients to control QR scanning implementation
  • Improve API ergonomics
  • Allow clients to easily support multiple QR devices per user

This package can be tested on Extension through this PR: MetaMask/metamask-extension#33893

How does it work?

The QrKeyring in this package is split into two main components:

  • QrKeyring: The main keyring class that exposes the Keyring API. Some of its logic is implemented in internal utility classes:
    • Device: A stateful internal wrapper for the keyring's public keys derivation logic and account management. In the code it's implemented by a separate class, but it can be seen as an internal component of the keyring, and It is not exported from the package.
  • QrKeyringBridge: The interface that a transport bridge must implement: it allows the client to control how QR codes are scanned. It's supposed to be implemented by a class that is injected by the client: when using KeyringController, it can be injected through a custom keyring builder (See this keyring builder that will be used by Extension).
    • QrKeyringScannerBridge: A generic bridge that allows the client to pass function hooks at construction time, which will be called when the keyring needs to scan a QR code.

Device pairing flow

Pairing is the process of initializing the keyring with a device, which is done by submitting the device details to the keyring in the form of a serialized UR, which usually comes from the "sync" QR scanned on the device. The client can trigger this process in three ways:

  • At construction time, by passing a SerializedUR object to the QrKeyring constructor.
    • In this case, the scan is done before the keyring is constructed, and thus the keyring will directly receive the QR contents.
  • By calling the QrKeyring.pairDevice() method, which allows the client to pass a SerializedUR object at any time.
    • Same as above, the scan is done outside the keyring, and the keyring will directly receive the QR contents.
  • By calling the QrKeyring.getFirstPage() method, which will trigger a scan request if no device is paired yet.
    • In this case, the keyring will request a scan through the bridge, and the client will implement the scan logic.

Extension uses getFirstPage() to trigger the pairing flow, which will then request a scan through the bridge

Like for other Hardare wallets, the pairing flow is initiated in connectHardware() method. See the sequence diagram below for the pairing flow on Extension, and see the Extension requestScan callback for the implementation of the scan logic.

sequenceDiagram
        autonumber
        participant A as MM Extension
        create participant B as QrKeyring
        A->>B: getFirstPage()
        alt if not initialized
        create participant C as Bridge
        B->>C: requestScan(PAIRING)
        C-->>+A: 
        note over A,C: The client controls the bridge and implements the scan logic
        A-->>-C: 
        destroy C
        C->>B: return CBOR 
        create participant D as Int. derivation logic
        B->>D: init(CBOR)
        end
        B->>D: accountAtIndex(0..4)
        destroy D
        D->>B: return accounts
        destroy B
        B->>A: return accounts
Loading

Mobile

TBD - Likely using .pairDevice() given the current implementation of QR interactions on mobile.

Signing flow

sequenceDiagram
        autonumber
        participant A as Client
        create participant B as QrKeyring
        A->>B: sign{Transaction, PersonalMessage, TypedData}()
        alt not initialized
        B->>A: Fail
        end
        create participant C as Bridge
        B->>C: requestScan(SIGN)
        C-->>+A: 
        note over A,C: The client controls the bridge and implements the scan logic
        A-->>-C: 
        destroy C
        C->>B: return signature CBOR 
        destroy B
        B->>A: return hex signature
Loading

Account addition flow

No scan required after the initial pairing.

sequenceDiagram
        autonumber
        participant A as Client
        alt non sequential account addition 
        create participant B as QrKeyring
        A->>B: setAccountToUnlock( i )
        end
        A->>B: addAccounts( n )
        loop n-times
        create participant D as Int. derivation logic
        B->>D: accountAtIndex( i )
        destroy D
        D->>B: return account
        end
        B->>A: return created accounts
Loading

Examples

@danroc

danroc commented Sep 25, 2024

Copy link
Copy Markdown
Contributor

Thank you @mikesposito! Maybe we should import this using git merge --allow-unrelated-histories (+ manual fixes) to keep the history from the original repo.

@mikesposito

mikesposito commented Sep 25, 2024

Copy link
Copy Markdown
Member Author

@danroc this PR is not moving the package from another source - this is a new implementation, and only shares a similar account derivation strategy from URs with the original keyring package currently used on clients

Do you think we should bring in its commit history anyway? Also, the package is in a Keystone monorepo, which would include commits to other packages potentially unrelated

@danroc

danroc commented Sep 25, 2024

Copy link
Copy Markdown
Contributor

Sorry @mikesposito, I didn't realise it was a new implementation! In this case it makes sense to start fresh, thank you for the clarification.

@github-actions

Copy link
Copy Markdown

This PR is marked as stale because it has been open for 60 days with no activity. Please remove the stale label or leave a comment, or it will be closed in 14 days.

@github-actions github-actions Bot added the Stale label Nov 25, 2024
@danroc danroc removed the Stale label Nov 28, 2024
@socket-security

socket-security Bot commented Feb 12, 2025

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​keystonehq/​metamask-airgapped-keyring@​0.15.2971007183100
Added@​keystonehq/​bc-ur-registry-eth@​0.19.11001007283100
Updated@​metamask/​utils@​9.3.0 ⏵ 9.2.1991009389 -2100

View full report

@socket-security

socket-security Bot commented Feb 12, 2025

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

Ignoring alerts on:

  • assert@2.1.0

View full report

@github-actions

This comment has been minimized.

@mikesposito

This comment has been minimized.

@mikesposito

This comment has been minimized.

@mikesposito

This comment has been minimized.

@github-actions

Copy link
Copy Markdown

Preview builds have been published. See these instructions (from the core monorepo) for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/keyring-api": "18.0.0-d2e6996",
  "@metamask-previews/eth-hd-keyring": "12.1.0-d2e6996",
  "@metamask-previews/eth-ledger-bridge-keyring": "11.1.1-d2e6996",
  "@metamask-previews/eth-qr-keyring": "0.0.0-d2e6996",
  "@metamask-previews/eth-simple-keyring": "10.0.0-d2e6996",
  "@metamask-previews/eth-trezor-keyring": "9.0.0-d2e6996",
  "@metamask-previews/keyring-internal-api": "6.2.0-d2e6996",
  "@metamask-previews/keyring-internal-snap-client": "4.1.0-d2e6996",
  "@metamask-previews/eth-snap-keyring": "13.0.0-d2e6996",
  "@metamask-previews/keyring-snap-client": "5.0.0-d2e6996",
  "@metamask-previews/keyring-snap-sdk": "4.0.0-d2e6996",
  "@metamask-previews/keyring-utils": "3.0.0-d2e6996"
}

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

Comment thread packages/keyring-eth-qr/src/device.ts
Comment thread packages/keyring-eth-qr/src/index.ts
cursor[bot]

This comment was marked as outdated.

cryptodev-2s
cryptodev-2s previously approved these changes Aug 11, 2025

@cryptodev-2s cryptodev-2s left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM! I’ll just leave one small suggestion for the changelog so we won’t need to reference the initial PR later. Everything else looks good!

Comment thread packages/keyring-eth-qr/CHANGELOG.md
@mikesposito

Copy link
Copy Markdown
Member Author
@SocketSecurity ignore npm/assert@2.1.0

The new author seems trustworthy

@cryptodev-2s cryptodev-2s left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

@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

@dawnseeker8 dawnseeker8 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@mikesposito mikesposito enabled auto-merge August 12, 2025 19:17
@mikesposito mikesposito added this pull request to the merge queue Aug 12, 2025
Merged via the queue into main with commit 021c648 Aug 12, 2025
34 checks passed
@mikesposito mikesposito deleted the add-qr-keyring branch August 12, 2025 19:19
mikesposito added a commit to MetaMask/core that referenced this pull request Aug 20, 2025
## Explanation

<!--
Thanks for your contribution! Take a moment to answer these questions so
that reviewers have the information they need to properly understand
your changes:

* What is the current state of things and why does it need to change?
* What is the solution your changes offer and how does it work?
* Are there any changes whose purpose might not obvious to those
unfamiliar with the domain?
* If your primary goal was to update one package but you found you had
to update another one along the way, why did you do so?
* If you had to upgrade a dependency, why did you do so?
-->
Dependent on:
  - ~~MetaMask/accounts#60

This PR removes all code related to the QRKeyring from KeystoneHQ, which
we intend to deprecate in favor of the new QRKeyring implementation in
the MetaMask accounts monorepo, that fully supports our Keyring type.

## References

<!--
Are there any issues that this pull request is tied to?
Are there other links that reviewers should consult to understand these
changes better?
Are there client or consumer pull requests to adopt any breaking
changes?

For example:

* Fixes #12345
* Related to #67890
-->
* Fixes #4341 

## Changelog

<!--
THIS SECTION IS NO LONGER NEEDED.

The process for updating changelogs has changed. Please consult the
"Updating changelogs" section of the Contributing doc for more.
-->

## 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/contributing.md#updating-changelogs),
highlighting breaking changes as necessary
- [ ] I've prepared draft pull requests for clients and consumer
packages to resolve any breaking changes
github-merge-queue Bot pushed a commit to MetaMask/metamask-extension that referenced this pull request Sep 1, 2025
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->
dependent on:
  - ~~MetaMask/accounts#60
  - ~~MetaMask/core#6031

## **Description**

<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->
This PR uses the `@metamask/eth-qr-keyring` package to handle QR code
keyring functionality, replacing the previous implementation from
`@keystonehq/metamask-airgapped-keyring`.

The change should be transparent to users, as the package is a drop-in
replacement. Though, under the hood, it allows to greatly simplify code
related to QRKeyring-related scanning and signing, and should improve
the overall ergonomics of interactions with QRKeyring.

Moreover, this PR adds the first e2e tests for QR code hardware wallets,
covering:
  - Pairing a QR code hardware wallet
  - Adding multiple accounts from the QR code hardware wallet

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/33893?quickstart=1)

## **Related issues**

Fixes:

## **Manual testing steps**

### **Does it work with new HW devices?**
1. Checkout to this branch, and run `yarn && yarn webpack` or `yarn &&
yarn start` to build the extension.
2. Install the extension in your browser, and pair a QR-based hardware
wallet (e.g. a Keystone device, or install AirGap Vault or imToken on an
iOS/Android device).
3. Add some (3) accounts to the QRKeyring, and ensure that you can see
them in the extension.
4. Go to `https://metamask.github.io/test-dapp/`, connect to the
extension.
5. Ensure that you can:
  - Sign transactions
  - Sign typed data
  - Sign personal messages
6. Remove one QR account
7. Forget the QR device

### **Does it work with HW devices already paired?**
1. Checkout to main, and run `yarn && yarn webpack` or `yarn && yarn
start` to build the extension.
2. Install the extension in your browser, and pair a QR-based hardware
wallet (e.g. a Keystone device, or install AirGap Vault on an
iOS/Android device).
3. Add some (3) accounts to the QRKeyring, and ensure that you can see
them in the extension.
4. Checkout to this branch, and run `yarn && yarn webpack` or `yarn &&
yarn start` to build the extension.
5. Go to `chrome://extensions/`, and reload the extension.
6. Login to the extension, and ensure that you can see the same accounts
as before.

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

---------

Co-authored-by: MetaMask Bot <metamaskbot@users.noreply.github.com>
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.

Internalize simplified QRKeyring implementation

6 participants