Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/hdwallet-keepkey/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions packages/hdwallet-keepkey/src/keepkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,12 @@ export class KeepKeyHDWallet implements core.HDWallet, core.BTCWallet, core.ETHW
return Zcash.zcashGetOrchardFVK(this.transport, account);
}

public zcashDisplayAddress(
params: Parameters<typeof Zcash.zcashDisplayAddress>[1] = {}
): Promise<{ address: string }> {
return Zcash.zcashDisplayAddress(this.transport, params);
}

public zcashSignPczt(signingRequest: Parameters<typeof Zcash.zcashSignPczt>[1], sighash: string): Promise<string[]> {
return Zcash.zcashSignPczt(this.transport, signingRequest, sighash);
}
Expand Down
119 changes: 119 additions & 0 deletions packages/hdwallet-keepkey/src/zcash.test.ts
Original file line number Diff line number Diff line change
@@ -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: <T>(fn: () => Promise<T>) => 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);

Check failure on line 41 in packages/hdwallet-keepkey/src/zcash.test.ts

View workflow job for this annotation

GitHub Actions / build (18)

Avoid calling `expect` conditionally`

Check failure on line 41 in packages/hdwallet-keepkey/src/zcash.test.ts

View workflow job for this annotation

GitHub Actions / build (18)

Avoid calling `expect` conditionally`
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);

Check failure on line 53 in packages/hdwallet-keepkey/src/zcash.test.ts

View workflow job for this annotation

GitHub Actions / build (18)

Avoid calling `expect` conditionally`

Check failure on line 53 in packages/hdwallet-keepkey/src/zcash.test.ts

View workflow job for this annotation

GitHub Actions / build (18)

Avoid calling `expect` conditionally`
expect(msg.getIndex()).toBe(expectedInputIndex);

Check failure on line 54 in packages/hdwallet-keepkey/src/zcash.test.ts

View workflow job for this annotation

GitHub Actions / build (18)

Avoid calling `expect` conditionally`

Check failure on line 54 in packages/hdwallet-keepkey/src/zcash.test.ts

View workflow job for this annotation

GitHub Actions / build (18)

Avoid calling `expect` conditionally`

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,
]);
});
});
107 changes: 81 additions & 26 deletions packages/hdwallet-keepkey/src/zcash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,41 @@
};
}

/**
* Display the device-derived Orchard unified address on the device.
*
* 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[];
account?: number;
} = {}
): Promise<{ address: string }> {
const account = params.account ?? 0;
const msg = new ZcashMessages.ZcashDisplayAddress();
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,
});

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 confirmedAddress = addressResp.getAddress();
if (!confirmedAddress) {
throw new Error("zcash: device returned an empty address");
}
return { address: confirmedAddress };
}

/**
* Transparent input descriptor for hybrid shielding transactions.
*/
Expand Down Expand Up @@ -147,38 +182,58 @@

// 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}`);

Check failure on line 195 in packages/hdwallet-keepkey/src/zcash.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace ``zcash:·device·finished·transparent·inputs·after·${signedCount},·expected·${nTransparentInputs}`` with `⏎············`zcash:·device·finished·transparent·inputs·after·${signedCount},·expected·${nTransparentInputs}`⏎··········`

Check failure on line 195 in packages/hdwallet-keepkey/src/zcash.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace ``zcash:·device·finished·transparent·inputs·after·${signedCount},·expected·${nTransparentInputs}`` with `⏎············`zcash:·device·finished·transparent·inputs·after·${signedCount},·expected·${nTransparentInputs}`⏎··········`
}
if (inputIndex >= nTransparentInputs) {
throw new Error(`zcash: device requested transparent input ${inputIndex}, only ${nTransparentInputs} provided`);

Check failure on line 198 in packages/hdwallet-keepkey/src/zcash.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace ``zcash:·device·requested·transparent·input·${inputIndex},·only·${nTransparentInputs}·provided`` with `⏎············`zcash:·device·requested·transparent·input·${inputIndex},·only·${nTransparentInputs}·provided`⏎··········`

Check failure on line 198 in packages/hdwallet-keepkey/src/zcash.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace ``zcash:·device·requested·transparent·input·${inputIndex},·only·${nTransparentInputs}·provided`` with `⏎············`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}`);

Check failure on line 203 in packages/hdwallet-keepkey/src/zcash.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace ``zcash:·transparent·input·descriptor·index·mismatch:·requested·${inputIndex},·got·${input.index}`` with `⏎············`zcash:·transparent·input·descriptor·index·mismatch:·requested·${inputIndex},·got·${input.index}`⏎··········`

Check failure on line 203 in packages/hdwallet-keepkey/src/zcash.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace ``zcash:·transparent·input·descriptor·index·mismatch:·requested·${inputIndex},·got·${input.index}`` with `⏎············`zcash:·transparent·input·descriptor·index·mismatch:·requested·${inputIndex},·got·${input.index}`⏎··········`
}

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.
const inputMsg = new ZcashMessages.ZcashTransparentInput();
inputMsg.setIndex(input.index);
inputMsg.setSighash(hexToBytes(input.sighash));
inputMsg.setAddressNList(input.addressNList);
inputMsg.setAmount(input.amount);

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}`);

Check failure on line 231 in packages/hdwallet-keepkey/src/zcash.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace ``zcash:·device·finished·transparent·inputs·after·${signedCount·+·1},·expected·${nTransparentInputs}`` with `⏎··············`zcash:·device·finished·transparent·inputs·after·${signedCount·+·1},·expected·${nTransparentInputs}`⏎············`

Check failure on line 231 in packages/hdwallet-keepkey/src/zcash.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace ``zcash:·device·finished·transparent·inputs·after·${signedCount·+·1},·expected·${nTransparentInputs}`` with `⏎··············`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`);
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading