Skip to content

release(runway): cherry-pick fix(hardware-wallets): bound stuck account-creation spinner with device-read timeout cp-13.42.0 - #45053

Merged
HowardBraham merged 1 commit into
release/13.42.0from
runway-cherry-pick-13.42.0-1785438105
Jul 30, 2026
Merged

HowardBraham merged 1 commit into
release/13.42.0from
runway-cherry-pick-13.42.0-1785438105

Conversation

@runway-github

@runway-github runway-github Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Description

unlockHardwareWalletAccount calls keyring.createAccounts, which
derives the account address from the hardware device. When the device is
locked or unresponsive, that device read can hang indefinitely.

Every other device-reading method (connectHardware,
checkHardwareStatus, getLedgerPublicKey, …) runs on the lock-free
deviceRead: true path in #withKeyringForDevice, which wraps the call
in a Promise.race against HARDWARE_DEVICE_READ_TIMEOUT_MS so a
wedged device rejects with an actionable error.
unlockHardwareWalletAccount did not.

createAccounts mutates vault state, so it cannot use the lock-free
deviceRead path (the restrictKeyringForDeviceRead facade
intentionally omits createAccounts along with every other mutating
method). It must run under the controller lock — but it still does
device I/O that can hang, and there was no timeout backstop. As a
result, a hung device during account creation:

  1. Never settled
    submitRequestToBackground('unlockHardwareWalletAccount', …), so the
    unlockHardwareWalletAccounts thunk never reached
    hideLoadingIndication() → appState.isLoading stayed true → the
    global <Loading /> overlay in ui/pages/routes/routes.component.tsx
    spun forever.
  2. Held the controller operation mutex across the hang, wedging other
    locked keyring operations until the browser restarted.

The user-facing symptom: after pressing Unlock on the "Select an
account" page at /new-account/connect, the spinner never resolved and
the UX appeared stuck.

Solution: wrap the create operation in the same
HARDWARE_DEVICE_READ_TIMEOUT_MS Promise.race backstop used by
#withKeyringForDevice's device-read branch. On timeout the call
rejects with an actionable error, which propagates back through the
thunk so hideLoadingIndication() runs, the spinner clears, and the
error surfaces to the user. The abandoned create operation's eventual
rejection is observed (.catch) so it never surfaces as an unhandled
rejection — mirroring the existing device-read path.

Changelog

CHANGELOG entry: Fixed a bug where connecting a hardware wallet account
could leave the loading spinner stuck forever if the device stopped
responding.

Related issues

Fixes: TBD

Manual testing steps

  1. Build the extension (yarn start or yarn build:test).
  2. Go to Add account → Add hardware wallet (the
    /new-account/connect route) and connect a Ledger or Trezor.
  3. On the "Select an account" page, select one or more accounts and
    press Unlock.
  4. With the device responsive, confirm the spinner shows briefly and the
    account imports successfully.
  5. Repeat, but leave the device locked / disconnected / unresponsive
    after pressing Unlock.
  6. Verify the spinner no longer spins forever — after the timeout it
    dismisses and an actionable "Hardware wallet account creation timed out"
    error is shown, and the rest of the wallet remains usable.

Screenshots/Recordings

Pre-merge author checklist

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.

Made with Cursor

Co-authored-by: Cursor cursoragent@cursor.com d90d1e7

…nt-creation spinner with device-read timeout cp-13.42.0 (#45048)

## **Description**

`unlockHardwareWalletAccount` calls `keyring.createAccounts`, which
derives the account address from the hardware device. When the device is
locked or unresponsive, that device read can hang indefinitely.

Every other device-reading method (`connectHardware`,
`checkHardwareStatus`, `getLedgerPublicKey`, …) runs on the lock-free
`deviceRead: true` path in `#withKeyringForDevice`, which wraps the call
in a `Promise.race` against `HARDWARE_DEVICE_READ_TIMEOUT_MS` so a
wedged device rejects with an actionable error.
`unlockHardwareWalletAccount` did not.

`createAccounts` mutates vault state, so it cannot use the lock-free
`deviceRead` path (the `restrictKeyringForDeviceRead` facade
intentionally omits `createAccounts` along with every other mutating
method). It must run under the controller lock — but it still does
device I/O that can hang, and there was no timeout backstop. As a
result, a hung device during account creation:

1. Never settled
`submitRequestToBackground('unlockHardwareWalletAccount', …)`, so the
`unlockHardwareWalletAccounts` thunk never reached
`hideLoadingIndication()` → `appState.isLoading` stayed `true` → the
global `<Loading />` overlay in `ui/pages/routes/routes.component.tsx`
spun forever.
2. Held the controller operation mutex across the hang, wedging other
locked keyring operations until the browser restarted.

The user-facing symptom: after pressing **Unlock** on the "Select an
account" page at `/new-account/connect`, the spinner never resolved and
the UX appeared stuck.

**Solution:** wrap the create operation in the same
`HARDWARE_DEVICE_READ_TIMEOUT_MS` `Promise.race` backstop used by
`#withKeyringForDevice`'s device-read branch. On timeout the call
rejects with an actionable error, which propagates back through the
thunk so `hideLoadingIndication()` runs, the spinner clears, and the
error surfaces to the user. The abandoned create operation's eventual
rejection is observed (`.catch`) so it never surfaces as an unhandled
rejection — mirroring the existing device-read path.

## **Changelog**

CHANGELOG entry: Fixed a bug where connecting a hardware wallet account
could leave the loading spinner stuck forever if the device stopped
responding.

## **Related issues**

Fixes: TBD

## **Manual testing steps**

1. Build the extension (`yarn start` or `yarn build:test`).
2. Go to **Add account → Add hardware wallet** (the
`/new-account/connect` route) and connect a Ledger or Trezor.
3. On the "Select an account" page, select one or more accounts and
press **Unlock**.
4. With the device responsive, confirm the spinner shows briefly and the
account imports successfully.
5. Repeat, but leave the device locked / disconnected / unresponsive
after pressing **Unlock**.
6. Verify the spinner no longer spins forever — after the timeout it
dismisses and an actionable "Hardware wallet account creation timed out"
error is shown, and the rest of the wallet remains usable.

## **Screenshots/Recordings**

<!--
### **Before**
### **After**
-->

## **Pre-merge author checklist**

- [x] 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).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] 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.

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>
@runway-github
runway-github Bot requested review from a team as code owners July 30, 2026 19:01
@metamask-ci metamask-ci Bot added the team-bots Bot team (for MetaMask Bot, Runway Bot, etc.) label Jul 30, 2026
@sonarqubecloud

Copy link
Copy Markdown

@metamask-ci

metamask-ci Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
Builds ready [99246fd]
⚡ Performance Benchmarks (Total: 🟢 12 pass · 🟡 10 warn · 🔴 2 fail)

Baseline (latest main): 171ed20 | Date: 7/28/2026 | Pipeline: 30573045791 | Baseline logs

Metricschrome-webpackfirefox-webpack
onboardingImportWallet
[Sentry log · main/release]
🔴 srpButtonToSrpForm(p95) [CI log]🟢 [CI log]
onboardingNewWallet
[Sentry log · main/release]
🟢 [CI log]🔴 [CI log]

Regressions (🔴 2 failures)

Interaction Benchmarks · Samples: 5
Benchmarkchrome-webpackfirefox-webpack
loadNewAccount
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]
confirmTx
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]
bridgeUserActions
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]

📈 Results compared to the previous 5 runs on main

  • ↓ confirmTx/tbt: -10%
  • ↑ bridgeUserActions/bridge_load_page: +131%
  • ↑ bridgeUserActions/longTaskCount: +11%
  • ↑ bridgeUserActions/longTaskTotalDuration: +10%
  • ↑ bridgeUserActions/longTaskMaxDuration: +12%
  • ↑ loadNewAccount/load_new_account: +10%
  • ↑ loadNewAccount/total: +10%
  • ↓ loadNewAccount/inp: -17%
  • ↑ loadNewAccount/lcp: +1134%
  • ↑ confirmTx/confirm_tx: +11%
  • ↓ confirmTx/longTaskCount: -100%
  • ↓ confirmTx/longTaskTotalDuration: -100%
  • ↓ confirmTx/longTaskMaxDuration: -100%
  • ↓ confirmTx/tbt: -100%
  • ↑ confirmTx/total: +11%
  • ↓ confirmTx/inp: -24%
  • ↑ confirmTx/fcp: +12%
  • ↑ confirmTx/lcp: +1234%
  • ↑ bridgeUserActions/bridge_load_page: +202%
  • ↑ bridgeUserActions/bridge_load_asset_picker: +114%
  • ↓ bridgeUserActions/bridge_search_token: -11%
  • ↓ bridgeUserActions/longTaskCount: -100%
  • ↓ bridgeUserActions/longTaskTotalDuration: -100%
  • ↓ bridgeUserActions/longTaskMaxDuration: -100%
  • ↓ bridgeUserActions/tbt: -100%
  • ↑ bridgeUserActions/total: +175%
  • ↓ bridgeUserActions/inp: -23%
  • ↑ bridgeUserActions/lcp: +1175%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/FCP: p75 1.9s
  • 🟡 confirmTx/FCP: p75 1.9s
  • 🟡 bridgeUserActions/FCP: p75 1.9s
Startup Benchmarks · Samples: 100
Benchmarkchrome-webpackfirefox-webpack
startupStandardHome
[Sentry log · main/release]
🟢 [CI log]🟢 [CI log]
startupPowerUserHome
[Sentry log · main/release]
–🟡 [CI log]

📈 Results compared to the previous 5 runs on main

  • ↑ startupStandardHome/setupStore: +14%
  • ↓ startupStandardHome/uiStartup: -28%
  • ↓ startupStandardHome/load: -27%
  • ↓ startupStandardHome/domContentLoaded: -27%
  • ↓ startupStandardHome/domInteractive: -17%
  • ↓ startupStandardHome/backgroundConnect: -21%
  • ↓ startupStandardHome/firstReactRender: -28%
  • ↓ startupStandardHome/initialActions: -50%
  • ↓ startupStandardHome/loadScripts: -27%
  • ↓ startupStandardHome/setupStore: -30%
  • ↓ startupStandardHome/fcp: -16%
  • ↓ startupStandardHome/lcp: -28%
  • ↑ startupPowerUserHome/uiStartup: +26%
  • ↑ startupPowerUserHome/load: +13%
  • ↑ startupPowerUserHome/domContentLoaded: +13%
  • ↑ startupPowerUserHome/domInteractive: +15%
  • ↑ startupPowerUserHome/backgroundConnect: +37%
  • ↑ startupPowerUserHome/firstReactRender: +30%
  • ↑ startupPowerUserHome/initialActions: +11%
  • ↑ startupPowerUserHome/loadScripts: +13%
  • ↑ startupPowerUserHome/setupStore: +269%
  • ↑ startupPowerUserHome/inp: +10%
  • ↑ startupPowerUserHome/fcp: +13%
  • ↑ startupPowerUserHome/lcp: +23%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 startupPowerUserHome/INP: p75 208ms
  • 🟡 startupPowerUserHome/LCP: p75 3.5s
User Journey Benchmarks · Samples: 5 · mock API 🔴 2
Benchmarkchrome-webpackfirefox-webpack
onboardingImportWallet
[Sentry log · main/release]
🔴 [CI log]
🔴 total
🟢 [CI log]
onboardingNewWallet
[Sentry log · main/release]
🟢 [CI log]🔴 [CI log]
🔴 total
assetDetails
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]
solanaAssetDetails
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]
importSrpHome
[Sentry log · main/release]
🟡 [CI log]🟡 [CI log]
sendTransactions
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]
swap
[Sentry log · main/release]
🟢 [CI log]🟡 [CI log]

📈 Results compared to the previous 5 runs on main

  • ↑ onboardingImportWallet/srpButtonToSrpForm: +10%
  • ↑ onboardingImportWallet/confirmSrpToPwForm: +12%
  • ↑ onboardingImportWallet/pwFormToMetricsScreen: +11%
  • ↓ onboardingImportWallet/doneButtonToHomeScreen: -76%
  • ↓ onboardingImportWallet/openAccountMenuToAccountListLoaded: -45%
  • ↓ onboardingImportWallet/longTaskCount: -86%
  • ↓ onboardingImportWallet/longTaskTotalDuration: -94%
  • ↓ onboardingImportWallet/longTaskMaxDuration: -91%
  • ↓ onboardingImportWallet/tbt: -99%
  • ↓ onboardingImportWallet/total: -79%
  • ↓ onboardingNewWallet/agreeButtonToOnboardingSuccess: -13%
  • ↓ onboardingNewWallet/doneButtonToAssetList: -80%
  • ↓ onboardingNewWallet/longTaskCount: -69%
  • ↓ onboardingNewWallet/longTaskTotalDuration: -78%
  • ↓ onboardingNewWallet/longTaskMaxDuration: -49%
  • ↓ onboardingNewWallet/tbt: -100%
  • ↓ onboardingNewWallet/total: -77%
  • ↓ solanaAssetDetails/longTaskCount: -100%
  • ↓ solanaAssetDetails/longTaskTotalDuration: -100%
  • ↓ solanaAssetDetails/longTaskMaxDuration: -100%
  • ↓ solanaAssetDetails/tbt: -100%
  • ↑ solanaAssetDetails/fcp: +13%
  • ↑ solanaAssetDetails/lcp: +11%
  • ↓ importSrpHome/homeAfterImportWithNewWallet: -15%
  • ↓ importSrpHome/longTaskCount: -37%
  • ↓ importSrpHome/longTaskTotalDuration: -32%
  • ↓ importSrpHome/tbt: -29%
  • ↓ importSrpHome/total: -14%
  • ↑ importSrpHome/cls: +446%
  • ↑ sendTransactions/openSendPageFromHome: +31%
  • ↓ sendTransactions/reviewTransactionToConfirmationPage: -97%
  • ↓ sendTransactions/longTaskCount: -100%
  • ↓ sendTransactions/longTaskTotalDuration: -100%
  • ↓ sendTransactions/longTaskMaxDuration: -100%
  • ↓ sendTransactions/tbt: -100%
  • ↓ sendTransactions/total: -95%
  • ↓ sendTransactions/inp: -26%
  • ↓ sendTransactions/lcp: -63%
  • ↓ sendTransactions/cls: -20%
  • ↓ swap/openSwapPageFromHome: -16%
  • ↑ swap/fetchAndDisplaySwapQuotes: +107%
  • ↑ swap/longTaskCount: +33%
  • ↓ swap/tbt: -47%
  • ↑ swap/total: +105%
  • ↑ swap/inp: +18%
  • ↓ swap/lcp: -75%
  • ↓ swap/cls: -92%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 importSrpHome/INP: p75 280ms
  • 🟡 assetDetails/FCP: p75 2.0s
  • 🟡 solanaAssetDetails/FCP: p75 1.9s
  • 🟡 importSrpHome/FCP: p75 1.9s
  • 🟡 sendTransactions/FCP: p75 1.9s
  • 🟡 swap/FCP: p75 1.9s
Dapp Page Load Benchmarks · Samples: 100
Benchmarkchrome-webpack
dappPageLoad
[Sentry log · main/release]
🟢 [CI log]
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -25.59 KiB (-0.17%)
  • ui: 1.71 KiB (0.01%)
  • common: 0 Bytes (0%)
  • other: 0 Bytes (0%)
  • contentScripts: 430 Bytes (0.02%)
  • zip: -6.11 MiB (-22.5%)

@HowardBraham
HowardBraham enabled auto-merge (squash) July 30, 2026 19:32
@HowardBraham HowardBraham added the retry-ci Tells GitHub Actions to retry failed jobs, label removed automatically before the retry label Jul 30, 2026
@github-actions github-actions Bot removed the retry-ci Tells GitHub Actions to retry failed jobs, label removed automatically before the retry label Jul 30, 2026
@HowardBraham
HowardBraham merged commit 501c175 into release/13.42.0 Jul 30, 2026
470 of 479 checks passed
@HowardBraham
HowardBraham deleted the runway-cherry-pick-13.42.0-1785438105 branch July 30, 2026 20:13
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 30, 2026
@metamaskbot metamaskbot added the release-13.42.0 Issue or pull request that will be included in release 13.42.0 label Jul 31, 2026
@gauthierpetetin

Copy link
Copy Markdown
Contributor

No release label on PR. Adding release label release-13.42.0 on PR, as PR was cherry-picked in branch 13.42.0.

This branch was previously deployed

1 inactive deployment
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-13.42.0 Issue or pull request that will be included in release 13.42.0 risk:low team-bots Bot team (for MetaMask Bot, Runway Bot, etc.)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants