Repository navigation
feat(wallet): add Ledger hardware wallet support via WebHID - #850
Merged
Jaydbrown merged 1 commit intoOct 22, 2026
Merged
Conversation
Add a Ledger option to the connect modal that talks to the device over WebHID using @ledgerhq/hw-transport-webhid and @ledgerhq/hw-app-str. The issue suggested @stellar/transport-webhid, but no such package is published on npm. - lib/ledger-wallet.ts: WebHID feature detection, lazy-loaded transport and Stellar app client, serialised device access, disconnect notifications, and mapping of picker-cancel / locked device / Stellar app not open / user rejection / unplug errors to user-facing messages - WalletContext: connect(walletId) routes 'ledger' to the device, signTx signs Ledger sessions on the device, an unplugged Ledger tears the session down, and the Freighter watcher ignores Ledger sessions - MultiWalletModal: Ledger entry, disabled with an explanation when the browser has no WebHID - ConnectButton: pass the selected provider to connect(), skip the silent reconnect and the 20s UI timeout for Ledger - Tests for the library, context, modal and button with a mocked transport Closes conduit-protocol#792 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jaydbrown
approved these changes
Oct 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a Ledger option to the connect modal. It talks to the device over WebHID, so treasuries that keep their funds on a hardware wallet can connect and sign directly.
Why not
@stellar/transport-webhid? The issue suggests that package, but it isn't published on npm (npm view @stellar/transport-webhidreturns E404). This PR uses Ledger's official packages instead:@ledgerhq/hw-transport-webhidfor the WebHID transport and@ledgerhq/hw-app-strfor the Stellar app client. Both are lazy-loaded with dynamicimport(), so they're only fetched the first time someone picks Ledger and aren't in the main bundle.Changes
lib/ledger-wallet.ts(new)isWebHIDSupported(): an SSR-safe feature check that also requires a secure context.connectLedger(): reuses a device this site already has permission for (openConnected()). It only shows the browser's device picker when there isn't one. It reads the first account (44'/148'/0') and returns its G-address.signWithLedger(): first checks that the device still holds the session's account. It then sends the transaction's signature base to the Stellar app for on-device review and attaches the signature withaddSignature, which verifies it. Fee-bump transactions work through the same path.signTx()calls wait their turn instead of failing withTransportRaceCondition.toLedgerError()turns every failure into aLedgerErrorwith a stablecodeand a message the user can act on:TransportOpenUserCancelled)0x5515/LockedDeviceError)0x6511,0x6e00,0x6d00, ...)0x6985)DisconnectedDevice*)onLedgerDisconnect()/closeLedger()manage the transport's lifecycle.contexts/WalletContext.tsxconnect(walletId?):'ledger'connects the device, and anything else keeps the existing Freighter flow. The session is stored asLedgerwith the samesaveWalletSessionand session-expiry handling as Freighter.signTxsigns Ledger sessions on the device. It keeps the existing semaphore, abort and stale-account checks, with a 120s timeout because the user reviews the transaction on the device.WatchWalletChangescallback now ignores Ledger sessions. Without this, a locked or missing Freighter extension would disconnect a Ledger user.disconnect()releases the device.components/MultiWalletModal.tsx: new Ledger entry. In browsers without WebHID it's disabled and shows the reason (linked witharia-describedby). Feature detection runs after mount to avoid a hydration mismatch.components/ConnectButton.tsxconnect(). It used to callconnect()with no argument for every entry.package.json/package-lock.json: adds the two@ledgerhqdependencies. Regenerating the lockfile also brings it back in sync withpackage.json. Onmain,npm cicurrently fails withMissing: playwright-core@1.64.1 from lock fileand similar errors (see below).CHANGELOG.mdandREADME.mdentries.How it was tested
New Vitest suites (42 tests) mock the WebHID transport and the Stellar app, since there's no device in CI:
lib/ledger-wallet.test.tscontexts/WalletContext.ledger.test.tsx: connect and persist a Ledger session, locked-device error,signTxrouting, the Freighter watcher not tearing down a Ledger session, unplug teardown, and releasing the device on disconnect.components/__tests__/MultiWalletModal.test.tsx: enabled with WebHID, disabled with an explanation without it.components/__tests__/ConnectButton.test.tsx: provider id routing, error display, no silent re-prompt for a cached Ledger.The existing
contexts/WalletContext.test.tsx(Freighter) still passes. Lint andtscreport no errors in any file this PR touches. The only lint output there is the existingtrackOperationexhaustive-depswarning inWalletContext.tsx.CI status on
mainCI on
mainis currently red for reasons unrelated to this change, for example run 39464762854 oncf5b2b0, where every job fails atnpm cibecause the lockfile is out of sync. This PR fixes that step. The steps after it still fail onmain, and I left them alone to keep this PR focused:app/dashboard/page.tsxhas a bad merge (a duplicated date-range JSX fragment around line 439), which is a parse error. Behind it,tscreports about 70 more errors in about 30 files (lib/stellar-address.ts,lib/developer-mode.ts,app/stream/[id]/page.tsx, several tests, and others).next lintalso flagsCopyableAddressis undefined and duplicate react imports. None of these are in files this PR touches.mainhas 85 failed / 1158 passed across 26 failing test files. With this PR it's 85 failed / 1200 passed: the same 26 failing files plus the 42 new passing tests.brace-expansion,braces,follow-redirects,sharp,source-map-js,tinypool,undiciandvite. None of them come from the new@ledgerhq/*dependency tree.Closes #792
🤖 Generated with Claude Code