Cherry-picking commits from cherry-pick-13-43-0-3cad329 to release/13.43.0 for PR #45158 - #45244
Conversation
<!-- 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. --> <!-- 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? --> The Ledger integration is presenting an odd UX where the operation "Account Import" never settles, the possible root cause for this issue is a failure during a device roundtrip operation using WebHID, since this operation runs under a keyring mutex with no timeout we never see the promise resolve/reject and therefore the operation never finishes. Three changes are introduced to mitigate this issue: - Revert of #45048. A previous temporal fix. - Add timeouts to `LedgerOffscreenBridge` as a safety net - `getPublicKey` now has a 30s timeout — no user confirmation needed - `deviceSignTransaction`, `deviceSignMessage`, `deviceSignTypedData` now has a 5 min timeout - needs on device confirmation - Timeout rejection message updated to: `Ledger device did not respond to "<action>" within <n>ms` - The timeout are arbitrary - Reuse transport across actions in `LedgerLegacyHandler` - `handleAction` no longer calls await `this.closeTransport()` in its finally. Instead it cancels any pending idle close at entry and schedules a new one after the action — so consecutive actions reuse the open WebHID transport instead of open/close churning (the churn that probably desyncs WebHID). - Added `scheduleIdleClose()`/`clearIdleClose()` helpers. - `closeTransport()` now calls `clearIdleClose()` (a manual disconnect/close supersedes a pending idle close). - `destroy()` clears the idle timer before tearing down listeners/transport. - The HID disconnect listener still calls `closeTransport()` immediately on unplug. <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 3. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: reuse ledger transport for multiple operations Fixes: #45027 Fixes: https://consensyssoftware.atlassian.net/browse/MUL-2108 1. Connect a Ledger device and import multiple accounts (at least 7). 2. Forget device and disconnect 3. Connect the device again and try to import multiple accounts (more accounts than imported in step 1). 1. Using the imported Ledger accounts try to execute different operations like message sign, swap, bridge, and send <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> https://github.com/user-attachments/assets/7a52c1cb-ea98-4118-97ad-468908261b92 - [ ] 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. - [ ] 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes core Ledger offscreen transport lifecycle, mutex/serialization, and hardware account unlock paths; mistakes could cause device lock errors, premature timeouts on signing, or stuck transports. > > **Overview** > Addresses stuck Ledger flows (e.g. account import) by **keeping the WebHID transport open** across bursts of offscreen actions (5s idle close) instead of closing after every call, with **`forceReset`** to drop a hung transport when recovery is needed. > > **`LedgerOffscreenBridge`** adds action-specific timeouts (30s for `getPublicKey`, 5min for signing) and clearer timeout errors; **`unlockHardwareWalletAccount`** drops its separate account-creation timeout in favor of that stack. > > **`ledger-router`** serializes concurrent offscreen messages on one promise chain, races each action against 60s / 330s backstops (sign vs read), calls **`forceReset`** on timeout, and swallows late rejections so the chain can continue. DMK stub forwards **`forceReset`** to legacy. Tests cover concurrency, timeout recovery, bootstrap error swallowing, and init/switch races. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 70e07e5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
|
Builds ready [8cc8db8] [reused from f216617]
⚡ Performance Benchmarks (Total: 🟢 0 pass · 🟡 0 warn · 🔴 0 fail)
Bundle size diffs [🚨 Warning! Bundle size has increased!]
|
owencraston
left a comment
There was a problem hiding this comment.
Tested in depth by connecting a Ledger Flex.
- connected 6 accounts including clicking next on the account selector page
- tested removing and forgetting an account
- tested erc20 sends
- tested monad sends
- tested swaps
- tested gasless swap on BNB
- tested gasless send on Linea and BNB
- tested personal sign and sign type data 4 on the e2e test dapp
- tested adding a new account
|
No release label on PR. Adding release label release-13.43.0 on PR, as PR was cherry-picked in branch 13.43.0. |



Description
The Ledger integration is presenting an odd UX where the operation
"Account Import" never settles, the possible root cause for this issue
is a failure during a device roundtrip operation using WebHID, since
this operation runs under a keyring mutex with no timeout we never see
the promise resolve/reject and therefore the operation never finishes.
Three changes are introduced to mitigate this issue:
previous temporal fix.
LedgerOffscreenBridgeas a safety netgetPublicKeynow has a 30s timeout — no user confirmation neededdeviceSignTransaction,deviceSignMessage,deviceSignTypedDatanow has a 5 min timeout - needs on device confirmation
Ledger device did not respond to "<action>" within <n>msLedgerLegacyHandlerhandleActionno longer calls awaitthis.closeTransport()in itsfinally. Instead it cancels any pending idle close at entry and
schedules a new one after the action — so consecutive actions reuse the
open WebHID transport instead of open/close churning (the churn that
probably desyncs WebHID).
scheduleIdleClose()/clearIdleClose()helpers.closeTransport()now callsclearIdleClose()(a manualdisconnect/close supersedes a pending idle close).
destroy()clears the idle timer before tearing downlisteners/transport.
closeTransport()immediatelyon unplug.
Changelog
CHANGELOG entry: reuse ledger transport for multiple operations
Related issues
Fixes: #45027
Fixes: https://consensyssoftware.atlassian.net/browse/MUL-2108
Manual testing steps
Connect and import multiple accounts
accounts than imported in step 1).
Signature
operations like message sign, swap, bridge, and send
Screenshots/Recordings
Screen.Recording.2026-08-03.at.2.28.17.PM.mov
Pre-merge author checklist
Docs and MetaMask
Extension Coding
Standards.
if applicable
guidelines).
Not required for external contributors.
Pre-merge reviewer checklist
app, test code being changed).
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
Note
Medium Risk
Changes core Ledger offscreen transport lifecycle,
mutex/serialization, and hardware account unlock paths; mistakes could
cause device lock errors, premature timeouts on signing, or stuck
transports.
Overview
Addresses stuck Ledger flows (e.g. account import) by keeping the
WebHID transport open across bursts of offscreen actions (5s idle
close) instead of closing after every call, with
forceResettodrop a hung transport when recovery is needed.
LedgerOffscreenBridgeadds action-specific timeouts (30s forgetPublicKey, 5min for signing) and clearer timeout errors;unlockHardwareWalletAccountdrops its separate account-creationtimeout in favor of that stack.
ledger-routerserializes concurrent offscreen messages on onepromise chain, races each action against 60s / 330s backstops (sign vs
read), calls
forceReseton timeout, and swallows late rejectionsso the chain can continue. DMK stub forwards
forceResetto legacy.Tests cover concurrency, timeout recovery, bootstrap error swallowing,
and init/switch races.
Reviewed by Cursor Bugbot for commit
70e07e5. Bugbot is set up for automated
code reviews on this repo. Configure
here.
Co-authored-by: Cursor cursoragent@cursor.com 3cad329