From 88069255157e6bf8848137e50831855894a25766 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 29 Apr 2026 20:09:43 -0500 Subject: [PATCH 1/5] feat(zcash): add wallet.zcashDisplayAddress for on-device UA verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps the firmware ZcashDisplayAddress flow as a method on KeepKeyHDWallet plus a standalone Zcash.zcashDisplayAddress() helper. Pattern mirrors zcashGetOrchardFVK: build the proto, transport.call, unpack the response. API: wallet.zcashDisplayAddress({ addressNList, // ZIP-32 path [32', 133', account'] account?, address, // host-built UA string (u1...) ak, nk, rivk, // FVK components for verification expectedSeedFingerprint?, // optional ZIP-32 §6.1 binding }): Promise<{ address: string; seedFingerprint?: Uint8Array }> Trust model is firmware-side: device re-derives its Orchard FVK at the requested account and rejects unless host's (ak, nk, rivk) matches. On match, device displays the address with QR; user accept returns the confirmed address bytes. Reject closes the call with Failure. If expectedSeedFingerprint is supplied, device checks it against BLAKE2b-256("Zcash_HD_Seed_FP", I2LEBSP_8(len) || seed) and rejects before any FVK derivation on mismatch — catches "wrong device" errors. Requires firmware ≥ 7.15.0 with the ZcashDisplayAddress proto handler (BitHighlander/keepkey-firmware:feature-zcash, PR #220). Build will fail until @bithighlander/device-protocol is republished with the ZcashDisplayAddress / ZcashAddress proto messages — both are already on BitHighlander/device-protocol:master via PR #27 and PR #28 but the npm package is currently pinned at 7.14.1 which predates them. The package.json bump is intentionally NOT included here so that the dep republish + version pick can be a separate, mechanical commit. --- packages/hdwallet-keepkey/src/keepkey.ts | 6 +++ packages/hdwallet-keepkey/src/zcash.ts | 63 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/packages/hdwallet-keepkey/src/keepkey.ts b/packages/hdwallet-keepkey/src/keepkey.ts index 4eccef89..0f3e196d 100644 --- a/packages/hdwallet-keepkey/src/keepkey.ts +++ b/packages/hdwallet-keepkey/src/keepkey.ts @@ -1557,6 +1557,12 @@ export class KeepKeyHDWallet implements core.HDWallet, core.BTCWallet, core.ETHW return Zcash.zcashGetOrchardFVK(this.transport, account); } + public zcashDisplayAddress( + params: Parameters[1], + ): Promise<{ address: string; seedFingerprint?: Uint8Array }> { + return Zcash.zcashDisplayAddress(this.transport, params); + } + public zcashSignPczt(signingRequest: Parameters[1], sighash: string): Promise { return Zcash.zcashSignPczt(this.transport, signingRequest, sighash); } diff --git a/packages/hdwallet-keepkey/src/zcash.ts b/packages/hdwallet-keepkey/src/zcash.ts index bc6e24a5..452f070d 100644 --- a/packages/hdwallet-keepkey/src/zcash.ts +++ b/packages/hdwallet-keepkey/src/zcash.ts @@ -47,6 +47,69 @@ export async function zcashGetOrchardFVK( }; } +/** + * Display a Zcash unified address on the device for user verification. + * + * Host provides the UA string + the FVK components (ak, nk, rivk) used + * to derive it. Device re-derives its own Orchard FVK at the requested + * account and rejects with Failure if the host-supplied FVK doesn't + * match — proving the address belongs to this device's seed at this + * account. On match, device renders the address with QR on the OLED; + * device returns the confirmed address bytes once the user accepts. + * + * Optional expectedSeedFingerprint binds the call to the device's + * ZIP-32 §6.1 seed fingerprint: + * BLAKE2b-256("Zcash_HD_Seed_FP", I2LEBSP_8(len(seed)) || seed) + * Device rejects before any FVK derivation if it doesn't match. + * + * Requires firmware ≥ 7.15.0 with the ZcashDisplayAddress proto handler. + */ +export async function zcashDisplayAddress( + transport: Transport, + params: { + addressNList: number[]; + account?: number; + address: string; + ak: Uint8Array; + nk: Uint8Array; + rivk: Uint8Array; + expectedSeedFingerprint?: Uint8Array; + } +): Promise<{ address: string; seedFingerprint?: Uint8Array }> { + const msg = new ZcashMessages.ZcashDisplayAddress(); + msg.setAddressNList(params.addressNList); + if (params.account !== undefined) msg.setAccount(params.account); + msg.setAddress(params.address); + msg.setAk(params.ak); + msg.setNk(params.nk); + msg.setRivk(params.rivk); + if (params.expectedSeedFingerprint) { + msg.setExpectedSeedFingerprint(params.expectedSeedFingerprint); + } + + const response = await transport.call( + Messages.MessageType.MESSAGETYPE_ZCASHDISPLAYADDRESS, + msg, + { msgTimeout: core.LONG_TIMEOUT }, + ); + + if (response.message_enum !== Messages.MessageType.MESSAGETYPE_ZCASHADDRESS) { + throw new Error(`zcash: unexpected response ${response.message_type}`); + } + + const addressResp = response.proto as ZcashMessages.ZcashAddress; + const out: { address: string; seedFingerprint?: Uint8Array } = { + address: addressResp.getAddress(), + }; + // seed_fingerprint is optional on the response; only populate when + // the device included it (firmware with PR #27 fields). + const fpBytes = addressResp.getSeedFingerprint_asU8?.(); + if (fpBytes && fpBytes.length === 32) { + out.seedFingerprint = fpBytes; + } + return out; +} + /** * Transparent input descriptor for hybrid shielding transactions. */ From 1a80f7f9bb6b34704d7bbcb36f472d58f75da309 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 29 Apr 2026 20:17:30 -0500 Subject: [PATCH 2/5] chore(deps): bump @bithighlander/device-protocol to 7.15.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picks up ZcashDisplayAddress / ZcashAddress proto messages and the seed_fingerprint binding fields on FVK / SignPCZT / DisplayAddress / Address messages. Required for the wallet.zcashDisplayAddress wrapper introduced in this PR to compile. Also tightens the response unpack: throw on empty address instead of silently returning a possibly-undefined string. Empty means something went wrong on the wire. Verified locally: yarn build → success. --- packages/hdwallet-keepkey/package.json | 2 +- packages/hdwallet-keepkey/src/zcash.ts | 6 +++++- yarn.lock | 8 ++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/hdwallet-keepkey/package.json b/packages/hdwallet-keepkey/package.json index 18940e44..8ffbbbad 100644 --- a/packages/hdwallet-keepkey/package.json +++ b/packages/hdwallet-keepkey/package.json @@ -17,7 +17,7 @@ "dependencies": { "@ethereumjs/common": "^2.4.0", "@ethereumjs/tx": "^3.3.0", - "@keepkey/device-protocol": "npm:@bithighlander/device-protocol@7.14.1", + "@keepkey/device-protocol": "npm:@bithighlander/device-protocol@7.15.0", "@keepkey/hdwallet-core": "1.53.16", "@keepkey/proto-tx-builder": "^0.9.1", "@metamask/eth-sig-util": "^7.0.0", diff --git a/packages/hdwallet-keepkey/src/zcash.ts b/packages/hdwallet-keepkey/src/zcash.ts index 452f070d..1356bde0 100644 --- a/packages/hdwallet-keepkey/src/zcash.ts +++ b/packages/hdwallet-keepkey/src/zcash.ts @@ -98,8 +98,12 @@ export async function zcashDisplayAddress( } const addressResp = response.proto as ZcashMessages.ZcashAddress; + const confirmedAddress = addressResp.getAddress(); + if (!confirmedAddress) { + throw new Error("zcash: device returned an empty address"); + } const out: { address: string; seedFingerprint?: Uint8Array } = { - address: addressResp.getAddress(), + address: confirmedAddress, }; // seed_fingerprint is optional on the response; only populate when // the device included it (firmware with PR #27 fields). diff --git a/yarn.lock b/yarn.lock index 2d825c79..8833b68e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1151,10 +1151,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@keepkey/device-protocol@npm:@bithighlander/device-protocol@7.14.1": - version "7.14.1" - resolved "https://registry.yarnpkg.com/@bithighlander/device-protocol/-/device-protocol-7.14.1.tgz#7306feaf9766974bc5295e990661038475f7f4ce" - integrity sha512-zJOCc1SzU856KcUXoS+nbd3ISEFmgnQTDdzutniyFe9DH83YY0rCwt3bfHcGI1QD7K37g5pH1O6t8zT8xay19A== +"@keepkey/device-protocol@npm:@bithighlander/device-protocol@7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@bithighlander/device-protocol/-/device-protocol-7.15.0.tgz#c6e74d53abad4d681c363b3ab3c7e911bc762f17" + integrity sha512-twYWMWAjALEs5RE2dCWP2hsMtDRCbB8F5cpNe/Iwev5V1d+nrX5V15ftKZVK8uTAMmOk/L6IpcEdFPe2xzZkXA== dependencies: google-protobuf "^3.7.0-rc.2" pbjs "^0.0.5" From 2b47e855450fcb6b437882af7d72efece982a2f0 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 29 Apr 2026 20:20:51 -0500 Subject: [PATCH 3/5] style: prettier auto-fix on zcashDisplayAddress wrapper --- packages/hdwallet-keepkey/src/keepkey.ts | 2 +- packages/hdwallet-keepkey/src/zcash.ts | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/hdwallet-keepkey/src/keepkey.ts b/packages/hdwallet-keepkey/src/keepkey.ts index 0f3e196d..26f3af6b 100644 --- a/packages/hdwallet-keepkey/src/keepkey.ts +++ b/packages/hdwallet-keepkey/src/keepkey.ts @@ -1558,7 +1558,7 @@ export class KeepKeyHDWallet implements core.HDWallet, core.BTCWallet, core.ETHW } public zcashDisplayAddress( - params: Parameters[1], + params: Parameters[1] ): Promise<{ address: string; seedFingerprint?: Uint8Array }> { return Zcash.zcashDisplayAddress(this.transport, params); } diff --git a/packages/hdwallet-keepkey/src/zcash.ts b/packages/hdwallet-keepkey/src/zcash.ts index 1356bde0..1ffde5de 100644 --- a/packages/hdwallet-keepkey/src/zcash.ts +++ b/packages/hdwallet-keepkey/src/zcash.ts @@ -87,11 +87,9 @@ export async function zcashDisplayAddress( msg.setExpectedSeedFingerprint(params.expectedSeedFingerprint); } - const response = await transport.call( - Messages.MessageType.MESSAGETYPE_ZCASHDISPLAYADDRESS, - msg, - { msgTimeout: core.LONG_TIMEOUT }, - ); + const response = await transport.call(Messages.MessageType.MESSAGETYPE_ZCASHDISPLAYADDRESS, msg, { + msgTimeout: core.LONG_TIMEOUT, + }); if (response.message_enum !== Messages.MessageType.MESSAGETYPE_ZCASHADDRESS) { throw new Error(`zcash: unexpected response ${response.message_type}`); From 8767941c30608edcbca049531f15f03939bf6d5e Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 30 Apr 2026 00:25:44 -0500 Subject: [PATCH 4/5] refactor(zcash): zcashDisplayAddress is account-path-only Firmware derives the Orchard UA from device seed material and displays it; host no longer supplies UA / ak / nk / rivk. Wrapper now accepts only { addressNList?, account? } and returns { address }. --- packages/hdwallet-keepkey/src/keepkey.ts | 4 +- packages/hdwallet-keepkey/src/zcash.ts | 52 +++++------------------- 2 files changed, 13 insertions(+), 43 deletions(-) diff --git a/packages/hdwallet-keepkey/src/keepkey.ts b/packages/hdwallet-keepkey/src/keepkey.ts index 26f3af6b..35d3cec5 100644 --- a/packages/hdwallet-keepkey/src/keepkey.ts +++ b/packages/hdwallet-keepkey/src/keepkey.ts @@ -1558,8 +1558,8 @@ export class KeepKeyHDWallet implements core.HDWallet, core.BTCWallet, core.ETHW } public zcashDisplayAddress( - params: Parameters[1] - ): Promise<{ address: string; seedFingerprint?: Uint8Array }> { + params: Parameters[1] = {} + ): Promise<{ address: string }> { return Zcash.zcashDisplayAddress(this.transport, params); } diff --git a/packages/hdwallet-keepkey/src/zcash.ts b/packages/hdwallet-keepkey/src/zcash.ts index 1ffde5de..be22506e 100644 --- a/packages/hdwallet-keepkey/src/zcash.ts +++ b/packages/hdwallet-keepkey/src/zcash.ts @@ -48,44 +48,23 @@ export async function zcashGetOrchardFVK( } /** - * Display a Zcash unified address on the device for user verification. + * Display the device-derived Orchard unified address on the device. * - * Host provides the UA string + the FVK components (ak, nk, rivk) used - * to derive it. Device re-derives its own Orchard FVK at the requested - * account and rejects with Failure if the host-supplied FVK doesn't - * match — proving the address belongs to this device's seed at this - * account. On match, device renders the address with QR on the OLED; - * device returns the confirmed address bytes once the user accepts. - * - * Optional expectedSeedFingerprint binds the call to the device's - * ZIP-32 §6.1 seed fingerprint: - * BLAKE2b-256("Zcash_HD_Seed_FP", I2LEBSP_8(len(seed)) || seed) - * Device rejects before any FVK derivation if it doesn't match. - * - * Requires firmware ≥ 7.15.0 with the ZcashDisplayAddress proto handler. + * Host sends only the ZIP-32 account path. Firmware derives the UA from + * seed material internally, displays it, and returns the confirmed address + * after user approval. */ export async function zcashDisplayAddress( transport: Transport, params: { - addressNList: number[]; + addressNList?: number[]; account?: number; - address: string; - ak: Uint8Array; - nk: Uint8Array; - rivk: Uint8Array; - expectedSeedFingerprint?: Uint8Array; - } -): Promise<{ address: string; seedFingerprint?: Uint8Array }> { + } = {} +): Promise<{ address: string }> { + const account = params.account ?? 0; const msg = new ZcashMessages.ZcashDisplayAddress(); - msg.setAddressNList(params.addressNList); - if (params.account !== undefined) msg.setAccount(params.account); - msg.setAddress(params.address); - msg.setAk(params.ak); - msg.setNk(params.nk); - msg.setRivk(params.rivk); - if (params.expectedSeedFingerprint) { - msg.setExpectedSeedFingerprint(params.expectedSeedFingerprint); - } + msg.setAddressNList(params.addressNList ?? [0x80000000 + 32, 0x80000000 + 133, 0x80000000 + account]); + msg.setAccount(account); const response = await transport.call(Messages.MessageType.MESSAGETYPE_ZCASHDISPLAYADDRESS, msg, { msgTimeout: core.LONG_TIMEOUT, @@ -100,16 +79,7 @@ export async function zcashDisplayAddress( if (!confirmedAddress) { throw new Error("zcash: device returned an empty address"); } - const out: { address: string; seedFingerprint?: Uint8Array } = { - address: confirmedAddress, - }; - // seed_fingerprint is optional on the response; only populate when - // the device included it (firmware with PR #27 fields). - const fpBytes = addressResp.getSeedFingerprint_asU8?.(); - if (fpBytes && fpBytes.length === 32) { - out.seedFingerprint = fpBytes; - } - return out; + return { address: confirmedAddress }; } /** From d83a65c3cac22dc66d641897d6964effbed441dc Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 30 Apr 2026 18:23:00 -0500 Subject: [PATCH 5/5] fix(zcash): stream transparent shield inputs --- packages/hdwallet-keepkey/src/zcash.test.ts | 119 ++++++++++++++++++++ packages/hdwallet-keepkey/src/zcash.ts | 72 +++++++----- 2 files changed, 165 insertions(+), 26 deletions(-) create mode 100644 packages/hdwallet-keepkey/src/zcash.test.ts diff --git a/packages/hdwallet-keepkey/src/zcash.test.ts b/packages/hdwallet-keepkey/src/zcash.test.ts new file mode 100644 index 00000000..cb1c4bf0 --- /dev/null +++ b/packages/hdwallet-keepkey/src/zcash.test.ts @@ -0,0 +1,119 @@ +import * as Messages from "@keepkey/device-protocol/lib/messages_pb"; +import * as ZcashMessages from "@keepkey/device-protocol/lib/messages-zcash_pb"; + +import { zcashSignPczt } from "./zcash"; + +function makeMockTransport(callImpl: jest.Mock) { + return { + debugLink: false, + call: callImpl, + lockDuring: (fn: () => Promise) => fn(), + } as any; +} + +const hex32 = (byte: string) => byte.repeat(32); + +function action(index: number) { + return { + index, + alpha: hex32("aa"), + cv_net: hex32("bb"), + nullifier: "", + cmx: "", + epk: "", + enc_compact: "", + enc_memo: "", + enc_noncompact: "", + rk: "", + out_ciphertext: "", + value: 0, + is_spend: false, + }; +} + +describe("zcashSignPczt", () => { + it("streams multiple transparent inputs using ZcashTransparentSig.next_index", async () => { + const calls: number[] = []; + const call = jest.fn().mockImplementation((mtype: number, msg: any) => { + calls.push(mtype); + + if (calls.length === 1) { + expect(mtype).toBe(Messages.MessageType.MESSAGETYPE_ZCASHSIGNPCZT); + const ack = new ZcashMessages.ZcashPCZTActionAck(); + ack.setNextIndex(0); + return Promise.resolve({ + message_enum: Messages.MessageType.MESSAGETYPE_ZCASHPCZTACTIONACK, + message_type: "ZcashPCZTActionAck", + proto: ack, + }); + } + + if (calls.length >= 2 && calls.length <= 4) { + const expectedInputIndex = calls.length - 2; + expect(mtype).toBe(Messages.MessageType.MESSAGETYPE_ZCASHTRANSPARENTINPUT); + expect(msg.getIndex()).toBe(expectedInputIndex); + + const sig = new ZcashMessages.ZcashTransparentSig(); + sig.setSignature(new Uint8Array([0x30, expectedInputIndex])); + sig.setNextIndex(expectedInputIndex === 2 ? 0xff : expectedInputIndex + 1); + return Promise.resolve({ + message_enum: Messages.MessageType.MESSAGETYPE_ZCASHTRANSPARENTSIG, + message_type: "ZcashTransparentSig", + proto: sig, + }); + } + + expect(mtype).toBe(Messages.MessageType.MESSAGETYPE_ZCASHPCZTACTION); + expect(msg.getIndex()).toBe(0); + + const signed = new ZcashMessages.ZcashSignedPCZT(); + signed.addSignatures(new Uint8Array(64).fill(0x42)); + return Promise.resolve({ + message_enum: Messages.MessageType.MESSAGETYPE_ZCASHSIGNEDPCZT, + message_type: "ZcashSignedPCZT", + proto: signed, + }); + }); + + const result = (await zcashSignPczt( + makeMockTransport(call), + { + n_actions: 1, + digests: { + header: hex32("01"), + transparent: hex32("02"), + sapling: hex32("03"), + orchard: hex32("04"), + }, + bundle_meta: { + flags: 3, + value_balance: 0, + anchor: hex32("05"), + }, + actions: [action(0)], + display: { + amount: "0.001 ZEC", + fee: "0.0001 ZEC", + to: "Orchard", + }, + transparent_inputs: [0, 1, 2].map((index) => ({ + index, + sighash: hex32("06"), + addressNList: [0x80000000 + 44, 0x80000000 + 133, 0x80000000, 0, index], + amount: 1000, + })), + }, + hex32("07") + )) as any; + + expect(result).toHaveLength(1); + expect(result._transparentSignatures).toEqual(["3000", "3001", "3002"]); + expect(calls).toEqual([ + Messages.MessageType.MESSAGETYPE_ZCASHSIGNPCZT, + Messages.MessageType.MESSAGETYPE_ZCASHTRANSPARENTINPUT, + Messages.MessageType.MESSAGETYPE_ZCASHTRANSPARENTINPUT, + Messages.MessageType.MESSAGETYPE_ZCASHTRANSPARENTINPUT, + Messages.MessageType.MESSAGETYPE_ZCASHPCZTACTION, + ]); + }); +}); diff --git a/packages/hdwallet-keepkey/src/zcash.ts b/packages/hdwallet-keepkey/src/zcash.ts index be22506e..f1663a2d 100644 --- a/packages/hdwallet-keepkey/src/zcash.ts +++ b/packages/hdwallet-keepkey/src/zcash.ts @@ -182,38 +182,58 @@ export async function zcashSignPczt( // Step 2: Transparent phase (if hybrid shielding) const transparentSignatures: string[] = []; - for (let i = 0; i < nTransparentInputs; i++) { + if (nTransparentInputs > 0) { if (response.message_enum !== Messages.MessageType.MESSAGETYPE_ZCASHPCZTACTIONACK) { - throw new Error(`zcash: expected ActionAck for transparent input ${i}, got ${response.message_type}`); + throw new Error(`zcash: expected ActionAck before transparent input 0, got ${response.message_type}`); } - const input = transparentInputs[i]; - const inputMsg = new ZcashMessages.ZcashTransparentInput(); - inputMsg.setIndex(input.index); - inputMsg.setSighash(hexToBytes(input.sighash)); - inputMsg.setAddressNList(input.addressNList); - inputMsg.setAmount(input.amount); + const initialAck = response.proto as ZcashMessages.ZcashPCZTActionAck; + let inputIndex = initialAck.hasNextIndex() ? initialAck.getNextIndex() ?? 0 : 0; - response = await transport.call(Messages.MessageType.MESSAGETYPE_ZCASHTRANSPARENTINPUT, inputMsg, { - msgTimeout: core.LONG_TIMEOUT, - omitLock: true, - }); + for (let signedCount = 0; signedCount < nTransparentInputs; signedCount++) { + if (inputIndex === 0xff) { + throw new Error(`zcash: device finished transparent inputs after ${signedCount}, expected ${nTransparentInputs}`); + } + if (inputIndex >= nTransparentInputs) { + throw new Error(`zcash: device requested transparent input ${inputIndex}, only ${nTransparentInputs} provided`); + } - if (response.message_enum !== Messages.MessageType.MESSAGETYPE_ZCASHTRANSPARENTSIG) { - throw new Error(`zcash: expected TransparentSig for input ${i}, got ${response.message_type}`); - } + const input = transparentInputs[inputIndex]; + if (input.index !== inputIndex) { + throw new Error(`zcash: transparent input descriptor index mismatch: requested ${inputIndex}, got ${input.index}`); + } + + const inputMsg = new ZcashMessages.ZcashTransparentInput(); + inputMsg.setIndex(input.index); + inputMsg.setSighash(hexToBytes(input.sighash)); + inputMsg.setAddressNList(input.addressNList); + inputMsg.setAmount(input.amount); - const sigResp = response.proto as ZcashMessages.ZcashTransparentSig; - transparentSignatures.push(bytesToHex(sigResp.getSignature_asU8())); - - // After last transparent input, device transitions to Orchard phase. - // If there are Orchard actions, we need to get the next ActionAck. - if (i === nTransparentInputs - 1 && signingRequest.n_actions > 0) { - // The device sends a ZcashPCZTActionAck after the last TransparentSig - // to signal readiness for Orchard actions. But the TransparentSig - // already has next_index=0xFF meaning "done with transparent". - // We need to send the first Orchard action now — the device - // implicitly transitions to the Orchard phase. + response = await transport.call(Messages.MessageType.MESSAGETYPE_ZCASHTRANSPARENTINPUT, inputMsg, { + msgTimeout: core.LONG_TIMEOUT, + omitLock: true, + }); + + if (response.message_enum !== Messages.MessageType.MESSAGETYPE_ZCASHTRANSPARENTSIG) { + throw new Error(`zcash: expected TransparentSig for input ${inputIndex}, got ${response.message_type}`); + } + + const sigResp = response.proto as ZcashMessages.ZcashTransparentSig; + transparentSignatures.push(bytesToHex(sigResp.getSignature_asU8())); + + // The device does not send an ActionAck between transparent inputs. + // ZcashTransparentSig.next_index drives the next request; 0xff means + // the transparent phase is complete and the next host message should + // be the first Orchard action. + const nextIndex = sigResp.hasNextIndex() ? sigResp.getNextIndex() : signedCount + 1; + if (signedCount < nTransparentInputs - 1) { + if (nextIndex === 0xff) { + throw new Error(`zcash: device finished transparent inputs after ${signedCount + 1}, expected ${nTransparentInputs}`); + } + inputIndex = nextIndex ?? signedCount + 1; + } else if (nextIndex !== undefined && nextIndex !== 0xff) { + throw new Error(`zcash: device requested transparent input ${nextIndex} after all inputs were signed`); + } } }