Skip to content

feat: replace Ledger iframe bridge with direct WebHID transport#39537

Merged
gantunesr merged 34 commits intomainfrom
ledger-direct-webhid
Feb 5, 2026
Merged

feat: replace Ledger iframe bridge with direct WebHID transport#39537
gantunesr merged 34 commits intomainfrom
ledger-direct-webhid

Conversation

@cloudonshore
Copy link
Copy Markdown
Contributor

@cloudonshore cloudonshore commented Jan 26, 2026

Description

The current Ledger integration uses a cross-origin iframe from metamask.github.io/ledger-iframe-bridge. This architecture has issues because cross-origin iframes cannot reliably inherit device permissions from the parent extension, and offscreen documents cannot provide user gestures for requestDevice().

This PR replaces the iframe bridge with direct WebHID usage via @ledgerhq/hw-transport-webhid in the offscreen document. The UI already requests device permission via requestDevice() in click handlers. The offscreen document can then access those permitted devices using getDevices() and TransportWebHID.openConnected() without requiring a gesture.

Open in GitHub Codespaces

Changelog

CHANGELOG entry: Fixed Ledger connectivity issues by replacing the iframe bridge with direct WebHID transport

Related issues

Fixes:

Manual testing steps

  1. Load the extension and connect a Ledger device
  2. Add a Ledger account via Settings > Add Hardware Wallet
  3. Send a transaction and confirm on the Ledger device
  4. Sign a message and confirm on the Ledger device
  5. Sign typed data (EIP-712) on a dApp like OpenSea or Uniswap and confirm on device
  6. Disconnect and reconnect the Ledger device, verify it reconnects without re-prompting for permission

Screenshots/Recordings

Before

After

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.

Note

High Risk
Touches Ledger signing and device-transport code paths and changes the underlying connectivity mechanism from an iframe to direct WebHID, which can affect hardware-wallet availability, permission handling, and transaction/message signing reliability.

Overview
Removes the cross-origin ledger-iframe-bridge iframe flow and implements direct Ledger communication in app/offscreen/ledger.ts using @ledgerhq/hw-transport-webhid + @ledgerhq/hw-app-eth, including connect/disconnect detection via navigator.hid, per-action transport lifecycle management, and action handling for getPublicKey, clear-signed signTransaction, signPersonalMessage, EIP-712 signTypedData, and app name/version retrieval.

Adds a comprehensive unit test suite for the new offscreen handler and updates the offscreen bridge/error propagation to serialize and rehydrate transport status errors. Updates build tooling/LavaMoat policies, Jest config coverage/matching, dependency versions, and patches @ledgerhq/hw-transport-webhid to fix its hid-framing import path; removes now-unneeded e2e mocking of the iframe bridge.

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

@github-actions
Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@socket-security
Copy link
Copy Markdown

socket-security bot commented Jan 26, 2026

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.

View full report

@github-actions github-actions bot added size-L and removed size-M labels Jan 26, 2026
@socket-security
Copy link
Copy Markdown

socket-security bot commented Jan 26, 2026

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

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​ledgerhq/​hw-transport-webhid@​6.30.0 ⏵ 6.31.010010010099 -1100
Updated@​ledgerhq/​hw-transport@​6.31.4 ⏵ 6.32.010010010099100

View full report

@cloudonshore
Copy link
Copy Markdown
Contributor Author

@metamaskbot update-policies

Restores clear signing with NFT/ERC20/externalPlugins support,
which shows human-readable info on the Ledger device screen.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

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.

const version = response
.subarray(offset, offset + versionLength)
.toString('ascii');
return { appName, version };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing bounds checking in APDU response parsing

Low Severity

The getAppNameAndVersion handler parses the Ledger APDU response without validating the response length. If the device returns a shorter-than-expected response, response[offset] could be undefined, causing nameLength to be undefined. This would make subarray(offset, offset + undefined) behave unexpectedly (producing empty strings) rather than throwing a clear error. Adding a minimum length check before parsing would make failures more diagnosable.

Fix in Cursor Fix in Web

@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 bot commented Feb 3, 2026

Builds ready [4173451]
UI Startup Metrics (1446 ± 112 ms)
PlatformBuildTypePageMetricTest Title (ms)Persona (ms)Mean (ms)Min (ms)Max (ms)Std Dev (ms)P 75 (ms)P 95 (ms)
ChromeBrowserifyStandard Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--14461195184911215171628
load--12401017165411213091437
domContentLoaded--12341012164911113031409
domInteractive--271792182375
firstPaint--169691417187198346
backgroundConnect--23421630614239261
firstReactRender--17103851927
initialActions--109114
loadScripts--1015785141711210891197
setupStore--1364651722
numNetworkReqs--221581171573
19--------
BrowserifyPower User Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--251016958332109423305107
load--12331077186716012901623
domContentLoaded--12141065184915912621595
domInteractive--3720166253694
firstPaint--231721666216264459
backgroundConnect--60929427896963912580
firstReactRender--24164572839
initialActions--103112
loadScripts--968820148914310211302
setupStore--18769102043
numNetworkReqs--1255525653150242
19--------
WebpackStandard Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--8386751104898961027
load--70159894182756863
domContentLoaded--69659493681752854
domInteractive--251686172276
firstPaint--1085929645136199
backgroundConnect--39181402941130
firstReactRender--15104061625
initialActions--103112
loadScripts--69359293480750852
setupStore--1154251221
numNetworkReqs--221589181573
19--------
WebpackPower User Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--1316926209325815831763
load--74862410921177581072
domContentLoaded--73761810861177451064
domInteractive--39191853038106
firstPaint--16065553111191523
backgroundConnect--17113050662169334
firstReactRender--23183442531
initialActions--103111
loadScripts--73561610771157421055
setupStore--1244151518
numNetworkReqs--1435531958166269
19--------
FirefoxBrowserifyStandard Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--16791393221118418242032
load--14161209188314615161669
domContentLoaded--14151204188214715161669
domInteractive--893725247127160
firstPaint--------
backgroundConnect--833118447127173
firstReactRender--12102021316
initialActions--103122
loadScripts--13671188175112314511586
setupStore--185186281464
numNetworkReqs--231387191777
19--------
BrowserifyPower User Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--29462133470550931294063
load--15321251262131115692444
domContentLoaded--15311251262031115692443
domInteractive--13433671108141357
firstPaint--------
backgroundConnect--3731171561296531989
firstReactRender--18136051924
initialActions--103122
loadScripts--14801223253728215252418
setupStore--104773114780452
numNetworkReqs--81382444894209
19--------
WebpackStandard Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--16981450237619017982131
load--14471240177912715331721
domContentLoaded--14471240177812715331721
domInteractive--903118040121157
firstPaint--------
backgroundConnect--68253495463187
firstReactRender--15125561524
initialActions--103122
loadScripts--14111216173511414761652
setupStore--1962283513130
numNetworkReqs--221287171875
19--------
WebpackPower User Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--30862314869090032054120
load--17341315692677917812549
domContentLoaded--17331315692577917812548
domInteractive--14534730134141524
firstPaint--------
backgroundConnect--33111813642813671067
firstReactRender--21163042328
initialActions--214123
loadScripts--16941295685177217322442
setupStore--1518775204144704
numNetworkReqs--80392935089192
19--------
📊 Page Load Benchmark Results

Current Commit: 4173451 | Date: 2/3/2026

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.05s (±55ms) 🟡 | historical mean value: 1.04s ⬆️ (historical data)
  • domContentLoaded-> current mean value: 736ms (±53ms) 🟢 | historical mean value: 723ms ⬆️ (historical data)
  • firstContentfulPaint-> current mean value: 78ms (±10ms) 🟢 | historical mean value: 78ms ⬇️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.05s 55ms 1.02s 1.33s 1.08s 1.33s
domContentLoaded 736ms 53ms 709ms 1.02s 763ms 1.02s
firstPaint 78ms 10ms 60ms 156ms 88ms 156ms
firstContentfulPaint 78ms 10ms 60ms 156ms 88ms 156ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -1.3 MiB (-24.73%)
  • ui: -1 Bytes (0%)
  • common: 1.3 MiB (14.19%)

gantunesr
gantunesr previously approved these changes Feb 4, 2026
montelaidev
montelaidev previously approved these changes Feb 4, 2026
@cloudonshore cloudonshore added this pull request to the merge queue Feb 4, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Feb 4, 2026
@gantunesr gantunesr added this pull request to the merge queue Feb 4, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Feb 4, 2026
@gantunesr gantunesr added this pull request to the merge queue Feb 4, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Feb 4, 2026
@metamaskbotv2
Copy link
Copy Markdown
Contributor

metamaskbotv2 bot commented Feb 5, 2026

Builds ready [fd29dbe]
UI Startup Metrics (1406 ± 114 ms)
PlatformBuildTypePageMetricTest Title (ms)Persona (ms)Mean (ms)Min (ms)Max (ms)Std Dev (ms)P 75 (ms)P 95 (ms)
ChromeBrowserifyStandard Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--14061180169811415021611
load--1211990144811413001406
domContentLoaded--1204961144311412921398
domInteractive--2715102192478
firstPaint--185661308217199339
backgroundConnect--23421535718235265
firstReactRender--1794262027
initialActions--109114
loadScripts--987746122711610761184
setupStore--1363251623
numNetworkReqs--231593201582
19--------
BrowserifyPower User Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--17631404284522617992254
load--11671031182817511561658
domContentLoaded--11541022181317511401647
domInteractive--37182413335112
firstPaint--1806851495238438
backgroundConnect--32428156138338392
firstReactRender--21144152232
initialActions--103011
loadScripts--91279615471638901355
setupStore--16792121632
numNetworkReqs--1174427649139254
19--------
WebpackStandard Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--8356681076949031012
load--69859493781752860
domContentLoaded--69358990079746853
domInteractive--2514116202276
firstPaint--1115932859134231
backgroundConnect--38181622643104
firstReactRender--16103961731
initialActions--104112
loadScripts--69058789878744847
setupStore--1155061222
numNetworkReqs--231598221588
19--------
WebpackPower User Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--1228863209620012821677
load--71362112461237051055
domContentLoaded--70461412401236941050
domInteractive--36181782933124
firstPaint--16265689111205501
backgroundConnect--17712942070185362
firstReactRender--21173432327
initialActions--102011
loadScripts--70161312331216921041
setupStore--1353951518
numNetworkReqs--1174828353144264
19--------
FirefoxBrowserifyStandard Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--16171341219219517492008
load--13631157196616814631706
domContentLoaded--13611157196516814561706
domInteractive--79332524598150
firstPaint--------
backgroundConnect--772823147101195
firstReactRender--13104041322
initialActions--103122
loadScripts--13221131187315214021685
setupStore--144219231144
numNetworkReqs--2512108231890
19--------
BrowserifyPower User Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--28142062441955731023944
load--16191237256732717692417
domContentLoaded--16181237256732717692417
domInteractive--11835611107119347
firstPaint--------
backgroundConnect--3261141399280353944
firstReactRender--201582112027
initialActions--103122
loadScripts--15601221245630416442390
setupStore--120979717899634
numNetworkReqs--67401352998123
19--------
WebpackStandard Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--15331290214416016291885
load--13321143166911614171545
domContentLoaded--13311143166811614171545
domInteractive--72272524194143
firstPaint--------
backgroundConnect--63222554776154
firstReactRender--13112521319
initialActions--103112
loadScripts--12951129165110413551469
setupStore--144104191274
numNetworkReqs--231288181777
19--------
WebpackPower User Home0--------
1--------
2--------
3--------
4--------
5--------
6--------
7--------
8--------
9--------
10--------
11--------
12--------
13--------
14--------
15--------
16--------
17--------
18--------
uiStartup--28052047396648332223856
load--15951297260933717962441
domContentLoaded--15941296260833717962441
domInteractive--14232786165125650
firstPaint--------
backgroundConnect--2891151344224297899
firstReactRender--22163442429
initialActions--213123
loadScripts--15441260255531417252330
setupStore--200101198261330794
numNetworkReqs--66391573293141
19--------
📊 Page Load Benchmark Results

Current Commit: fd29dbe | Date: 2/5/2026

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.05s (±200ms) 🟡 | historical mean value: 1.03s ⬆️ (historical data)
  • domContentLoaded-> current mean value: 744ms (±225ms) 🟢 | historical mean value: 723ms ⬆️ (historical data)
  • firstContentfulPaint-> current mean value: 96ms (±206ms) 🟢 | historical mean value: 80ms ⬆️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.05s 200ms 1.01s 2.97s 1.24s 2.97s
domContentLoaded 744ms 225ms 698ms 2.93s 930ms 2.93s
firstPaint 96ms 206ms 60ms 2.15s 84ms 2.15s
firstContentfulPaint 96ms 206ms 60ms 2.15s 84ms 2.15s
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -1.3 MiB (-24.7%)
  • ui: -1 Bytes (0%)
  • common: 1.3 MiB (14.17%)

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

Labels

release-13.18.0 Issue or pull request that will be included in release 13.18.0 size-XL team-swaps-and-bridge Swaps and Bridge team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants