Conversation
macsrock
force-pushed
the
zcash-ledger
branch
from
September 16, 2026 20:28
e897fea to
57bb0fe
Compare
A Zcash wallet can now be created from a Ledger (Official Zcash app 3.9.2+, BLE or USB) and send from it. The device exports its unified viewing key while pairing, the wallet tracks transparent and Ironwood funds from that key, and every send goes back to the device for review and signing; the backend proves and finalizes the transaction afterwards. Pairing reuses the birth-height page Monero hardware wallets use, since the viewing key is fetched during the restore rather than picked from a list: each export needs an approval on the device screen. Ledger wallets keep their transparent funds spendable instead of auto-shielding them: shielding needs the device, which a background sync cannot ask for. The zkool dependency moves to the ledger-official branch (upstream 6.30.0 plus the Cake and Keystone patches and a Flutter-driven APDU transport), which also removes PaymentOptions.mode, renames NewAccount.ledger to hw and replaces Coin.setUseTor with a transport setting; flutter_rust_bridge is pinned to 2.12.0 to match its bindings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Requiring the device before the wallet opens routed a Zcash wallet through the app's startup reconnect flow, which is written for Monero: after the Bluetooth link came up it dereferenced the Monero proxy, threw in a Zcash-only build, and never sent a command, so the Ledger stayed on its pairing animation. A Zcash Ledger wallet opens from its viewing key and only needs the device to sign, so it now behaves like Bitcoin and connects from the send screen. Signing also checks that the Zcash app is open first, so the wrong app is reported as such instead of as a rejected transaction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The device review ran after the slide-to-send, once the transaction was being committed. Cake's hardware wallets do it the other way round: the device signs while the transaction is prepared (the send screen shows "proceed on your device" then), the confirmation sheet shows the signed result, and the slide only broadcasts. Match that: a Ledger wallet signs in createTransaction and the pending transaction carries the finalized package for commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sending again right after a send finds no spendable notes, because the change is not recorded until the network picks the transaction up. The planner's "No feasible note selection found" arrived with a Rust backtrace in the error dialog; say what it means instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A hardware wallet cannot auto-shield: every transaction is a device review, which a background sync cannot ask for. Follow the Keystone design instead: when a hardware Zcash wallet holds transparent funds above the dust floor, the dashboard shows a card that sweeps them into the shielded pool. On a Ledger the sweep is reviewed and signed on the device while it is prepared, then broadcast, from one tap. The card hides and the balance stops counting the swept notes until the sweep confirms, and the transaction is marked as a shield so history shows it as one rather than a transfer whose value is only the fee. Sends draw from the shielded pools only again, as for every other Zcash wallet; transparent funds are shielded first through the card. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pins zkool (and its rlz plugin) to macsrock/zkool2 ledger-official and ledger_flutter_plus to macsrock/ledger-flutter-plus ble-frame-length, the branches under review alongside this change. Re-point them at cake-tech once those are merged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Added with tool/append_translation.dart; the non-English entries are machine translations and "Ledger" should stay untranslated in them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
macsrock
force-pushed
the
zcash-ledger
branch
from
September 16, 2026 21:07
57bb0fe to
c7bbbd8
Compare
- Sends and shields planned the transaction and then waited for the device review inside the shared coin lock, so sync, balance, history and mempool refreshes all stalled for as long as the Ledger sat on its review screen. The plan is now built under the lock and the review runs on its own database handle after it is released; the signer only reads this account's keys and addresses. - Pairing exported the viewing key on mainnet regardless of the wallet being restored; it now uses that wallet's network (coin type 1 off mainnet). - A Zcash app version string that cannot be parsed no longer skips the minimum-version check; it is reported instead. - A transport error recorded for one exchange is cleared by the next successful one, so it cannot be attached to an unrelated later failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ing to show "Proceed on your device" went up the moment send was tapped, but the Ledger draws its review only once the whole transaction has reached it. Before that the phone plans the transaction (two lightwalletd round trips and the note witnesses), checks the app version and streams the transaction in 255-byte packets, so the user was told to look at a device that showed nothing for several seconds. The wallet now reports where a Ledger send or shield is (preparing, sending to the device, awaiting the device, signing, finalizing) through a shared HardwareSigningStage, driven by the signer's own progress events; the sheet on both send screens and the shield card's notice show that stage, and only say "proceed on your device" when the device is actually waiting on the user. Three new strings, translated for every locale. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… error Shielding with the Ledger locked produced "Open the Zcash app on your Ledger (0x6e01)" followed by the Rust error and a full backtrace. The code was wrong (0x5515 is "device locked", not "wrong app") and the rest was noise. Status words the device answers with are now translated for the person holding it: locked, wrong app, rejected on the device, unreadable transaction, app internal error, and a fallback that shows the code; the backtrace is stripped from anything else. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…issing The send screen showed "Exception: Open the Zcash app on your Ledger, then try again (Your Ledger is locked. ...)": the version probe's guard had already explained the status word, and the app check wrapped it again. Ledger problems are now a ZcashLedgerException whose text is the message itself, so nothing prefixes or nests it. A connection that has dropped is refused up front with "The Ledger is not connected. Make sure it is unlocked and nearby, then try again." instead of after a failed exchange with the transport's own words; the transport cause goes to the log. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The signer now reports "Sending to Ledger" while streaming and "Confirm on your Ledger" only once the device can draw its review; the sheet follows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ce screen A security review of the pairing flow found that the viewing key crosses the Bluetooth/USB link with nothing authenticating it, and the wallet imported whatever arrived: a key substituted on the link would have made every receive address an attacker's, with nothing for the user to notice until a spend failed. After the export the device now derives the account's unified address and shows it on its own screen (GET_SHIELD_ADDR with display), while the app shows the address it derived from the key it received and asks the user to approve on the device only if the two match. The device's reply is compared as well, so a plain mismatch (wrong account or path) fails without the user having to spot it. Pairing is refused if either check fails. zkool re-pinned to d34590e0 for the new command, which also refuses a transparent signature reply with no DER bytes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…een approved The signer announced signing before its first signing command, which is the one that waits for the user's approval, so the sheet showed "proceed on your device" for a moment and then "signing" while the Ledger was still waiting. zkool now reports signing only after a signature has come back (re-pinned to 67a2513e); the wallet follows those events. The stage is also set before the sheet opens and can only move forward, so nothing earlier can flash back in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A logged send showed the device answers the last packet only after the user approves, so the sheet said "sending to your device" through the whole review and "proceed" for the second the signature took. The signer now reports the confirm step just before that packet goes out. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… as a lone fee The history already splits a shield into what left the transparent side and what arrived shielded, so it never reads as money vanishing. The rule only looked for funds arriving in Orchard; since Ironwood activated a shield lands there, the rule stopped firing, and the entry showed just the net of the transaction, which is the fee. Both pools now count. This affects automatic shielding on software wallets as much as the Ledger shield card. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s fee Until a shield confirms the wallet only knows the transaction's net effect on the account, which is the fee, so the history briefly showed a small amount going out and nothing else. The swept amount is now recorded when the shield is broadcast and shown for the pending entry; once the shielded note is attached the entry splits into "shielding" and "received" as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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 shielded Zcash support with a Ledger running the official Zcash app (LedgerHQ app-zcash 3.9.2+, Ironwood/NU6.3). A Zcash wallet can be paired from the device over Bluetooth or USB, syncs from the exported unified viewing key, sends from the shielded pool with every transaction reviewed and signed on the Ledger, and shields transparent funds from a dashboard card.
Tested end to end on an iPhone 17 Pro Max with a Ledger Nano Gen5 over Bluetooth: pairing (viewing-key export approved on the device), receive, two shielded Ironwood sends (
2f1d5c68a8fb819e5766c69816b19a460e2cc85356ce45149d3f8bce6cc90bf6,5f81c937ec8e45f25fd59ab932425e863263f5f9061f663f6fcf2615411e1354) and a transparent-to-shielded sweep from the card.How it works
official_sign.rs). This branch adds aDeviceimplementation that hands every APDU to a Dart closure, so the phone'sledger_flutter_plusconnection carries the protocol; exposed throughapi::ledger(app version probe, UFVK export, signing with progress events). Official accounts can also be created from a UFVK the host already exported.ZcashLedgerServicebridgesledger_flutter_plusto those calls. Pairing goes through the Monero-style options page (name + birth height): the viewing key is exported during the restore, one device approval per account. Sends sign on the device insidecreateTransaction, so the send screen shows "proceed on your device", and the slide only broadcasts, matching the other hardware wallets. Shielding follows the Keystone design: a dashboard card sweeps transparent funds into the shielded pool, reviewed on the device, then broadcast; the card hides and the swept notes stop counting until the sweep confirms.requireHardwareWalletConnectionstays false because the startup reconnect flow is Monero-specific.Dependencies (merge these first, then re-point the refs at cake-tech)
macsrock/zkool2@ledger-official: upstream 6.30.0 + Cake/Keystone patches + the Ledger transport).macsrock/ledger-flutter-plus@ble-frame-length). This one is a real bug fix on its own: the BLE reader took only the low byte of the first frame's u16 length, so any device reply of 256 bytes or more never completed (the Zcash viewing-key export is the first reply that large; Bitcoin's replies are all smaller).flutter_rust_bridgeoverride bumped to v2.12.0 to match zkool's regenerated bindings.The last commit pins these three to the review branches on
macsrock; swap the URLs/refs once merged.Notes for review
tool/append_translation.dart; the machine translations should keep "Ledger" untranslated.cupcake-testbranch are not included here; they merge on top of this.Audit follow-ups (added after an external review)
Wallet lock no longer held during the device review. Sends and shields plan the transaction under the shared coin lock and run the Ledger review on a separate database handle afterwards, so sync, balance and history keep refreshing while the device waits for the user.
Pairing uses the restored wallet's network for the viewing-key export (coin type 1 off mainnet) instead of always mainnet.
An unparseable Zcash app version is reported instead of skipping the minimum-version check; a stale transport error can no longer be attached to a later, unrelated failure.
zkool re-pinned to
2c792005, which adds the matching Rust fixes (Ledger header fields, viewing-key chunk guard) and repairs the Keystone/Cupcake PCZT translation for the upgraded pczt; see Upstream 6.30.0 + Flutter-driven Ledger transport for the official Zcash app zkool2#1.Device prompt timing (
caf4500d0): "Proceed on your device" used to appear the moment send was tapped, several seconds before the Ledger had anything to show (the app was still planning the transaction and streaming it to the device). The wallet now reports the signing stage (new sharedHardwareSigningStagein cw_core, driven by the Rust signer's progress events); the send sheet and the shield notice show "Preparing transaction" / "Sending to your device" and switch to "Proceed on your device" only when the device's review is up. Three new strings, translated for all locales.Device errors in plain words: a Ledger status word (locked 0x5515, wrong app, rejected on device, unreadable data, app internal error) is now shown as a one-line instruction, and the Rust backtrace is stripped from any other signer error. Before, a locked device produced a wrong "open the Zcash app (0x6e01)" line followed by a full backtrace. A dropped connection is refused up front with "The Ledger is not connected…" (new
ZcashLedgerException, whose text is the message itself, so nothing prefixes or nests it).zkool re-pinned to
aeb698d0: the signer's "Confirm on your Ledger" event now follows the last packet, so the sheet says "Sending to your device…" during the stream and "Proceed on your device" exactly when the review is up.Second security review (Seth's panel)
Its P1 finding (unauthenticated viewing-key import at pairing) is fixed in
8a534d49f: after the export the device shows the account's unified address on its own screen while the app shows the address it derived from the received key, and asks the user to approve on the device only if they match; the device's reply is also compared so a wrong account or path fails outright. New stringsledger_verify_address/ledger_verify_address_description, all locales. zkool re-pinned tod34590e0.Sheet timing: the stage is set before the sheet opens and only moves forward; zkool re-pinned to
67a2513eso "Proceed on your device" stays until the device has been approved (it used to flip to "Signing…" while the Ledger was still waiting).History (
4bc6cb568): a shield into Ironwood showed as an outgoing transaction of only the fee. The existing two-entry display ("Shielding" for what left the transparent side, "Received" for what arrived shielded) only recognised Orchard; it now counts Ironwood too. Affects software-wallet auto-shielding as well.🤖 Generated with Claude Code