@swapkit/wallet-hardware 5.0.1 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/THIRD_PARTY_LICENSES.md +2344 -0
  2. package/dist/chunk-09e51wma.js +4 -0
  3. package/dist/chunk-09e51wma.js.map +167 -0
  4. package/dist/chunk-0arpsm5f.js +4 -0
  5. package/dist/chunk-0arpsm5f.js.map +10 -0
  6. package/dist/chunk-1amf58aq.js +4 -0
  7. package/dist/{chunk-xkh15edk.js.map → chunk-1amf58aq.js.map} +2 -2
  8. package/dist/chunk-3t17wpjg.js +17 -0
  9. package/dist/chunk-3t17wpjg.js.map +29 -0
  10. package/dist/chunk-5c7b7z2d.js +4 -0
  11. package/dist/{chunk-20wx60bn.js.map → chunk-5c7b7z2d.js.map} +2 -2
  12. package/dist/chunk-5p3ttw6y.js +4 -0
  13. package/dist/{chunk-1jpcb30j.js.map → chunk-5p3ttw6y.js.map} +1 -1
  14. package/dist/chunk-dptbnkzm.js +64 -0
  15. package/dist/chunk-dptbnkzm.js.map +260 -0
  16. package/dist/chunk-ds2m83jp.js +5 -0
  17. package/dist/chunk-ds2m83jp.js.map +11 -0
  18. package/dist/chunk-eenx3cv6.js +4 -0
  19. package/dist/chunk-eenx3cv6.js.map +33 -0
  20. package/dist/chunk-gr3eswf1.js +4 -0
  21. package/dist/chunk-gr3eswf1.js.map +73 -0
  22. package/dist/{chunk-7fsaymh4.js → chunk-hg71hp4m.js} +3 -3
  23. package/dist/{chunk-7fsaymh4.js.map → chunk-hg71hp4m.js.map} +1 -1
  24. package/dist/chunk-jewm2ag3.js +5 -0
  25. package/dist/{chunk-4tbtxdxa.js.map → chunk-jewm2ag3.js.map} +2 -2
  26. package/dist/chunk-jzrytcrf.js +4 -0
  27. package/dist/chunk-jzrytcrf.js.map +12 -0
  28. package/dist/chunk-k3thje6a.js +4 -0
  29. package/dist/chunk-k3thje6a.js.map +9 -0
  30. package/dist/chunk-nhva847d.js +5 -0
  31. package/dist/chunk-nhva847d.js.map +9 -0
  32. package/dist/chunk-tm47brht.js +4 -0
  33. package/dist/chunk-tm47brht.js.map +140 -0
  34. package/dist/chunk-xmvg4cds.js +4 -0
  35. package/dist/chunk-xmvg4cds.js.map +127 -0
  36. package/dist/chunk-xx7cwpzq.js +4 -0
  37. package/dist/chunk-xx7cwpzq.js.map +9 -0
  38. package/dist/chunk-yrxe491r.js +20 -0
  39. package/dist/chunk-yrxe491r.js.map +17 -0
  40. package/dist/index.cjs +2 -2
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/keepkey/index.cjs +2 -2
  43. package/dist/keepkey/index.cjs.map +2 -2
  44. package/dist/keepkey/index.js +2 -2
  45. package/dist/keepkey/index.js.map +2 -2
  46. package/dist/ledger/index.cjs +90 -2
  47. package/dist/ledger/index.cjs.map +830 -24
  48. package/dist/ledger/index.js +2 -2
  49. package/dist/ledger/index.js.map +21 -19
  50. package/dist/trezor/index.cjs +2 -2
  51. package/dist/trezor/index.cjs.map +5 -5
  52. package/dist/trezor/index.js +2 -2
  53. package/dist/trezor/index.js.map +3 -3
  54. package/dist/types/ledger/clients/bitcoin.d.ts +36 -0
  55. package/dist/types/ledger/clients/bitcoin.d.ts.map +1 -0
  56. package/dist/types/ledger/clients/cosmos.d.ts +38 -10
  57. package/dist/types/ledger/clients/cosmos.d.ts.map +1 -1
  58. package/dist/types/ledger/clients/evm.d.ts +37 -46
  59. package/dist/types/ledger/clients/evm.d.ts.map +1 -1
  60. package/dist/types/ledger/clients/near.d.ts +5 -2
  61. package/dist/types/ledger/clients/near.d.ts.map +1 -1
  62. package/dist/types/ledger/clients/sui.d.ts +8 -12
  63. package/dist/types/ledger/clients/sui.d.ts.map +1 -1
  64. package/dist/types/ledger/clients/thorchain/index.d.ts +34 -8
  65. package/dist/types/ledger/clients/thorchain/index.d.ts.map +1 -1
  66. package/dist/types/ledger/clients/thorchain/protocol.d.ts +58 -0
  67. package/dist/types/ledger/clients/thorchain/protocol.d.ts.map +1 -0
  68. package/dist/types/ledger/clients/tron.d.ts +11 -8
  69. package/dist/types/ledger/clients/tron.d.ts.map +1 -1
  70. package/dist/types/ledger/clients/utxo-legacy-adapter.d.ts +3 -2
  71. package/dist/types/ledger/clients/utxo-legacy-adapter.d.ts.map +1 -1
  72. package/dist/types/ledger/clients/utxo-legacy-outputs.d.ts +18 -0
  73. package/dist/types/ledger/clients/utxo-legacy-outputs.d.ts.map +1 -0
  74. package/dist/types/ledger/clients/utxo.d.ts +10 -13
  75. package/dist/types/ledger/clients/utxo.d.ts.map +1 -1
  76. package/dist/types/ledger/clients/xrp.d.ts +4 -1
  77. package/dist/types/ledger/clients/xrp.d.ts.map +1 -1
  78. package/dist/types/ledger/clients/zcash.d.ts +53 -0
  79. package/dist/types/ledger/clients/zcash.d.ts.map +1 -0
  80. package/dist/types/ledger/helpers/dmk.d.ts +24 -0
  81. package/dist/types/ledger/helpers/dmk.d.ts.map +1 -0
  82. package/dist/types/ledger/helpers/executeDeviceAction.d.ts +34 -0
  83. package/dist/types/ledger/helpers/executeDeviceAction.d.ts.map +1 -0
  84. package/dist/types/ledger/helpers/getLedgerAddress.d.ts +1 -1
  85. package/dist/types/ledger/helpers/getLedgerAddress.d.ts.map +1 -1
  86. package/dist/types/ledger/helpers/getLedgerClient.d.ts +10 -3
  87. package/dist/types/ledger/helpers/getLedgerClient.d.ts.map +1 -1
  88. package/dist/types/ledger/helpers/index.d.ts +2 -1
  89. package/dist/types/ledger/helpers/index.d.ts.map +1 -1
  90. package/dist/types/ledger/helpers/ledgerJsDmkBridge.d.ts +26 -0
  91. package/dist/types/ledger/helpers/ledgerJsDmkBridge.d.ts.map +1 -0
  92. package/dist/types/ledger/helpers/ledgerJsDmkTransport.d.ts +13 -0
  93. package/dist/types/ledger/helpers/ledgerJsDmkTransport.d.ts.map +1 -0
  94. package/dist/types/ledger/helpers/lowS.d.ts +6 -0
  95. package/dist/types/ledger/helpers/lowS.d.ts.map +1 -0
  96. package/dist/types/ledger/helpers/rawTx.d.ts +2 -0
  97. package/dist/types/ledger/helpers/rawTx.d.ts.map +1 -0
  98. package/dist/types/ledger/index.d.ts +19 -7
  99. package/dist/types/ledger/index.d.ts.map +1 -1
  100. package/dist/types/ledger/types.d.ts +6 -10
  101. package/dist/types/ledger/types.d.ts.map +1 -1
  102. package/dist/types/trezor/index.d.ts +2 -2
  103. package/dist/types/trezor/index.d.ts.map +1 -1
  104. package/package.json +36 -36
  105. package/src/ledger/clients/bitcoin.ts +575 -0
  106. package/src/ledger/clients/cosmos.ts +180 -51
  107. package/src/ledger/clients/evm.ts +238 -195
  108. package/src/ledger/clients/near.ts +43 -13
  109. package/src/ledger/clients/sui.ts +55 -68
  110. package/src/ledger/clients/thorchain/index.ts +210 -86
  111. package/src/ledger/clients/thorchain/protocol.ts +166 -0
  112. package/src/ledger/clients/tron.ts +49 -37
  113. package/src/ledger/clients/utxo-legacy-adapter.ts +35 -32
  114. package/src/ledger/clients/utxo-legacy-outputs.ts +122 -0
  115. package/src/ledger/clients/utxo.ts +108 -206
  116. package/src/ledger/clients/xrp.ts +39 -23
  117. package/src/ledger/clients/zcash.ts +440 -0
  118. package/src/ledger/helpers/dmk.ts +231 -0
  119. package/src/ledger/helpers/executeDeviceAction.ts +201 -0
  120. package/src/ledger/helpers/getLedgerClient.ts +101 -72
  121. package/src/ledger/helpers/index.ts +6 -1
  122. package/src/ledger/helpers/ledgerJsDmkBridge.ts +109 -0
  123. package/src/ledger/helpers/ledgerJsDmkTransport.ts +88 -0
  124. package/src/ledger/helpers/lowS.ts +30 -0
  125. package/src/ledger/helpers/rawTx.ts +12 -0
  126. package/src/ledger/index.ts +564 -369
  127. package/src/ledger/types.ts +15 -10
  128. package/src/trezor/index.ts +183 -68
  129. package/dist/chunk-1jpcb30j.js +0 -5
  130. package/dist/chunk-20wx60bn.js +0 -4
  131. package/dist/chunk-4tbtxdxa.js +0 -5
  132. package/dist/chunk-7v7cgm69.js +0 -4
  133. package/dist/chunk-7v7cgm69.js.map +0 -10
  134. package/dist/chunk-83g13ck6.js +0 -5
  135. package/dist/chunk-83g13ck6.js.map +0 -10
  136. package/dist/chunk-h2r568rz.js +0 -4
  137. package/dist/chunk-h2r568rz.js.map +0 -10
  138. package/dist/chunk-xkh15edk.js +0 -4
  139. package/dist/types/ledger/clients/thorchain/common.d.ts +0 -28
  140. package/dist/types/ledger/clients/thorchain/common.d.ts.map +0 -1
  141. package/dist/types/ledger/clients/thorchain/helpers.d.ts +0 -7
  142. package/dist/types/ledger/clients/thorchain/helpers.d.ts.map +0 -1
  143. package/dist/types/ledger/clients/thorchain/lib.d.ts +0 -22
  144. package/dist/types/ledger/clients/thorchain/lib.d.ts.map +0 -1
  145. package/dist/types/ledger/clients/thorchain/utils.d.ts +0 -2
  146. package/dist/types/ledger/clients/thorchain/utils.d.ts.map +0 -1
  147. package/dist/types/ledger/clients/utxo-psbt.d.ts +0 -18
  148. package/dist/types/ledger/clients/utxo-psbt.d.ts.map +0 -1
  149. package/dist/types/ledger/cosmosTypes.d.ts +0 -40
  150. package/dist/types/ledger/cosmosTypes.d.ts.map +0 -1
  151. package/dist/types/ledger/helpers/getLedgerTransport.d.ts +0 -3
  152. package/dist/types/ledger/helpers/getLedgerTransport.d.ts.map +0 -1
  153. package/dist/types/ledger/helpers/ledgerNetworkInfo.d.ts +0 -14
  154. package/dist/types/ledger/helpers/ledgerNetworkInfo.d.ts.map +0 -1
  155. package/dist/types/ledger/interfaces/CosmosLedgerInterface.d.ts +0 -14
  156. package/dist/types/ledger/interfaces/CosmosLedgerInterface.d.ts.map +0 -1
  157. package/src/ledger/clients/thorchain/common.ts +0 -93
  158. package/src/ledger/clients/thorchain/helpers.ts +0 -120
  159. package/src/ledger/clients/thorchain/lib.ts +0 -258
  160. package/src/ledger/clients/thorchain/utils.ts +0 -69
  161. package/src/ledger/clients/utxo-psbt.ts +0 -204
  162. package/src/ledger/cosmosTypes.ts +0 -98
  163. package/src/ledger/helpers/getLedgerTransport.ts +0 -102
  164. package/src/ledger/helpers/ledgerNetworkInfo.ts +0 -127
  165. package/src/ledger/interfaces/CosmosLedgerInterface.ts +0 -67
@@ -0,0 +1,440 @@
1
+ import type {
2
+ LegacyCreateTransactionArg,
3
+ LegacyTransaction,
4
+ LegacyTransactionInput,
5
+ LegacyTransactionOutput,
6
+ SignerZcash,
7
+ } from "@ledgerhq/device-signer-kit-zcash";
8
+ import type Transport from "@ledgerhq/hw-transport";
9
+ import { hex } from "@scure/base";
10
+ import {
11
+ type DerivationPathArray,
12
+ derivationPathToString,
13
+ NetworkDerivationPath,
14
+ SwapKitError,
15
+ } from "@swapkit/helpers";
16
+ import type { UTXOType } from "@swapkit/toolboxes/utxo";
17
+ import type { ZcashTransaction } from "@swapkit/utxo-signer";
18
+
19
+ import type { LedgerDMKSession } from "../helpers/dmk";
20
+ import { createLedgerSessionSigner, getLedgerDMKSession } from "../helpers/dmk";
21
+ import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../helpers/executeDeviceAction";
22
+ import { runLedgerJsOperation } from "../helpers/ledgerJsDmkBridge";
23
+ import { ZcashLedger as LegacyZcashLedger } from "./utxo";
24
+
25
+ // @swapkit/utxo-signer 3.1.0 exports only the Ironwood activation height.
26
+ const NU6_2_ACTIVATION_HEIGHT = 3_364_600;
27
+
28
+ interface ZcashLedgerParams {
29
+ derivationPath?: DerivationPathArray | string;
30
+ dmkSession?: LedgerDMKSession;
31
+ onDeviceActionState?: LedgerDeviceActionStateHandler;
32
+ transport?: Transport;
33
+ }
34
+
35
+ interface GetExtendedPublicKeyParams {
36
+ path?: string;
37
+ xpubVersion?: number;
38
+ }
39
+
40
+ interface SignZcashTransactionParams {
41
+ changePath?: string;
42
+ derivationPaths?: string[];
43
+ inputUtxos: UTXOType[];
44
+ tx: ZcashTransaction;
45
+ }
46
+
47
+ interface SignZcashTransactionWithMultiplePathsParams extends Omit<SignZcashTransactionParams, "derivationPaths"> {
48
+ derivationPaths: string[];
49
+ }
50
+
51
+ interface ParsedZcashPath {
52
+ account: number;
53
+ accountPath: string;
54
+ fullPath: string;
55
+ }
56
+
57
+ function normalizePath(path: DerivationPathArray | string) {
58
+ return (typeof path === "string" ? path : derivationPathToString(path)).replace(/^m\//, "").replace(/^\/+/, "");
59
+ }
60
+
61
+ function parseZcashPath(path: DerivationPathArray | string): ParsedZcashPath {
62
+ const normalized = normalizePath(path);
63
+ const match = /^44'\/133'\/(\d+)'(?:\/(0|1)\/(\d+))?$/.exec(normalized);
64
+
65
+ if (!match) {
66
+ throw new SwapKitError("wallet_ledger_invalid_params", {
67
+ path: normalized,
68
+ reason: "Expected a Zcash BIP44 path with coin type 133",
69
+ });
70
+ }
71
+
72
+ const account = Number(match[1]);
73
+ const accountPath = `44'/133'/${account}'`;
74
+ return { account, accountPath, fullPath: `${accountPath}/${match[2] ?? 0}/${match[3] ?? 0}` };
75
+ }
76
+
77
+ function uint32LE(value: number) {
78
+ const bytes = new Uint8Array(4);
79
+ new DataView(bytes.buffer).setUint32(0, value >>> 0, true);
80
+ return bytes;
81
+ }
82
+
83
+ function uint64LE(value: bigint) {
84
+ const bytes = new Uint8Array(8);
85
+ new DataView(bytes.buffer).setBigUint64(0, value, true);
86
+ return bytes;
87
+ }
88
+
89
+ // Keeps the failure context in the error info, and so in its message, rather than only in its cause.
90
+ function invalidParams(info: Record<string, unknown>, cause?: unknown) {
91
+ return new SwapKitError({ errorKey: "wallet_ledger_invalid_params", info }, cause);
92
+ }
93
+
94
+ function readUint32LE({ bytes, offset }: { bytes: Uint8Array; offset: number }) {
95
+ if (offset + 4 > bytes.length) throw new Error("Unexpected end of previous transaction");
96
+ return new DataView(bytes.buffer, bytes.byteOffset + offset, 4).getUint32(0, true);
97
+ }
98
+
99
+ // @swapkit/utxo-signer 3.1.0 has no bounds-checked, v5-aware decoder (ZcashTransaction.fromBytes reads only the
100
+ // v4 layout and its Reader is not exported), so previous transactions are read here.
101
+ function createPreviousTransactionReader(bytes: Uint8Array) {
102
+ let offset = 0;
103
+
104
+ function take(length: number) {
105
+ if (offset + length > bytes.length) throw new Error(`Previous transaction ends before byte ${offset + length}`);
106
+ offset += length;
107
+ return bytes.slice(offset - length, offset);
108
+ }
109
+
110
+ function compactSize() {
111
+ const prefix = take(1)[0] ?? 0;
112
+ if (prefix < 0xfd) return prefix;
113
+
114
+ const view = new DataView(take(prefix === 0xfd ? 2 : prefix === 0xfe ? 4 : 8).buffer);
115
+ const value =
116
+ view.byteLength === 2
117
+ ? view.getUint16(0, true)
118
+ : view.byteLength === 4
119
+ ? view.getUint32(0, true)
120
+ : Number(view.getBigUint64(0, true));
121
+ if (!Number.isSafeInteger(value)) throw new Error("Previous transaction compact-size value exceeds safe range");
122
+ return value;
123
+ }
124
+
125
+ return { compactSize, rest: () => take(bytes.length - offset), take, varBytes: () => take(compactSize()) };
126
+ }
127
+
128
+ // Fields are read into locals in wire order: object keys are kept sorted, which is not the order on the wire.
129
+ function parsePreviousTransaction({ bytes, version }: { bytes: Uint8Array; version: number }) {
130
+ const reader = createPreviousTransactionReader(bytes);
131
+ const header = reader.take(version === 4 ? 8 : 20);
132
+
133
+ const inputs: LegacyTransactionInput[] = [];
134
+ for (let count = reader.compactSize(); inputs.length < count; ) {
135
+ const prevout = reader.take(36);
136
+ const script = reader.varBytes();
137
+ const sequence = reader.take(4);
138
+ inputs.push({ prevout, script, sequence });
139
+ }
140
+
141
+ const outputs: LegacyTransactionOutput[] = [];
142
+ for (let count = reader.compactSize(); outputs.length < count; ) {
143
+ const amount = reader.take(8);
144
+ const script = reader.varBytes();
145
+ outputs.push({ amount, script });
146
+ }
147
+
148
+ const transparentFields = { inputs, nVersionGroupId: header.slice(4, 8), outputs, version: header.slice(0, 4) };
149
+ if (version >= 5) {
150
+ return {
151
+ ...transparentFields,
152
+ consensusBranchId: header.slice(8, 12),
153
+ locktime: header.slice(12, 16),
154
+ nExpiryHeight: header.slice(16, 20),
155
+ };
156
+ }
157
+
158
+ const locktime = reader.take(4);
159
+ const nExpiryHeight = reader.take(4);
160
+ // Value balance, Sapling spends and outputs, JoinSplits and the binding signature follow nExpiryHeight.
161
+ return { ...transparentFields, extraData: reader.rest(), locktime, nExpiryHeight };
162
+ }
163
+
164
+ // Either byte order binds a txid equally: toolbox-built transactions keep display-order txids, while
165
+ // ZcashTransaction.fromBytes keeps them in wire order.
166
+ function matchesTxid({ hash, txid }: { hash: string; txid: Uint8Array }) {
167
+ const normalized = hash.toLowerCase();
168
+ return normalized === hex.encode(txid) || normalized === hex.encode(txid.slice().reverse());
169
+ }
170
+
171
+ async function serializeOutputs(tx: ZcashTransaction) {
172
+ const { CompactSize, utils } = await import("@swapkit/utxo-signer");
173
+ const outputs = Array.from({ length: tx.outputsLength }, (_, index) => tx.getOutput(index));
174
+ return utils.concatBytes(
175
+ CompactSize.encode(BigInt(outputs.length)),
176
+ ...outputs.flatMap(({ amount, script }) => [uint64LE(amount), CompactSize.encode(BigInt(script.length)), script]),
177
+ );
178
+ }
179
+
180
+ async function activationHeight(consensusBranchId: number) {
181
+ const { ZCASH_IRONWOOD_ACTIVATION_HEIGHT, ZcashConsensusBranchId } = await import("@swapkit/utxo-signer");
182
+
183
+ switch (consensusBranchId) {
184
+ case ZcashConsensusBranchId.NU6_2:
185
+ return NU6_2_ACTIVATION_HEIGHT;
186
+ case ZcashConsensusBranchId.IRONWOOD:
187
+ return ZCASH_IRONWOOD_ACTIVATION_HEIGHT;
188
+ default:
189
+ throw new SwapKitError("wallet_ledger_invalid_params", {
190
+ consensusBranchId,
191
+ reason: "Ledger Zcash DSK signing supports NU6.2 and Ironwood target branches",
192
+ });
193
+ }
194
+ }
195
+
196
+ async function previousTransaction({
197
+ inputIndex,
198
+ utxo,
199
+ }: {
200
+ inputIndex: number;
201
+ utxo: UTXOType;
202
+ }): Promise<LegacyTransaction> {
203
+ const { hash: txid, index: outputIndex, txHex } = utxo;
204
+ const context = { inputIndex, outputIndex, txid };
205
+ if (!txHex) {
206
+ throw invalidParams({
207
+ ...context,
208
+ reason: "Zcash Ledger signing requires the full previous transaction txHex for every input",
209
+ });
210
+ }
211
+
212
+ function readPrevious<T>(read: () => T) {
213
+ try {
214
+ return read();
215
+ } catch (error) {
216
+ throw invalidParams({ ...context, reason: error instanceof Error ? error.message : String(error) }, error);
217
+ }
218
+ }
219
+
220
+ const raw = readPrevious(() => hex.decode(txHex.replace(/^0x/i, "")));
221
+ const header = readPrevious(() => readUint32LE({ bytes: raw, offset: 0 }));
222
+ const version = header & 0x7fffffff;
223
+ // The Zcash DSK can stream only v4 (Sapling) and v5/v6 previous transactions into a trusted input.
224
+ if ((header & 0x80000000) === 0 || version < 4 || version > 6) {
225
+ throw invalidParams({
226
+ ...context,
227
+ reason: "Ledger Zcash signing supports only overwintered v4, v5 and v6 previous transactions",
228
+ version,
229
+ });
230
+ }
231
+
232
+ const parsed = readPrevious(() => parsePreviousTransaction({ bytes: raw, version }));
233
+ if (outputIndex < 0 || outputIndex >= parsed.outputs.length) {
234
+ throw invalidParams({
235
+ ...context,
236
+ outputCount: parsed.outputs.length,
237
+ reason: "Zcash input references an output outside its previous transaction",
238
+ });
239
+ }
240
+
241
+ // The DSK streams a v5/v6 previous transaction from its wire bytes and the device computes its ZIP-244 txid.
242
+ // Override bytes must use that layout, so the DSK frames a v4 itself from the parsed fields instead.
243
+ if (version >= 5) return { ...parsed, serializedPreviousTransactionOverride: raw };
244
+
245
+ // A v4 txid is the double SHA-256 of its bytes, so bind the transaction to the planned input before the
246
+ // device trusts its amounts.
247
+ const { utils } = await import("@swapkit/utxo-signer");
248
+ const previousTxid = utils.sha256x2(raw);
249
+ if (!matchesTxid({ hash: txid, txid: previousTxid })) {
250
+ throw invalidParams({
251
+ ...context,
252
+ previousTxid: hex.encode(previousTxid.reverse()),
253
+ reason: "Previous transaction does not hash to the input txid",
254
+ });
255
+ }
256
+
257
+ return parsed;
258
+ }
259
+
260
+ function validateSigningParams({
261
+ accountPath,
262
+ derivationPaths,
263
+ inputUtxos,
264
+ tx,
265
+ }: {
266
+ accountPath: string;
267
+ derivationPaths: string[];
268
+ inputUtxos: UTXOType[];
269
+ tx: ZcashTransaction;
270
+ }) {
271
+ if (inputUtxos.length !== tx.inputsLength || derivationPaths.length !== tx.inputsLength) {
272
+ throw new SwapKitError("wallet_ledger_invalid_params", {
273
+ derivationPathCount: derivationPaths.length,
274
+ inputCount: tx.inputsLength,
275
+ inputUtxoCount: inputUtxos.length,
276
+ reason: "Zcash input UTXOs and derivation paths must match the transaction input count",
277
+ });
278
+ }
279
+
280
+ inputUtxos.forEach((utxo, inputIndex) => {
281
+ const input = tx.getInput(inputIndex);
282
+ if (!matchesTxid({ hash: utxo.hash, txid: input.txid }) || utxo.index !== input.index) {
283
+ throw invalidParams({
284
+ inputIndex,
285
+ reason: "Zcash input UTXO does not match the transaction input",
286
+ transactionInput: { index: input.index, txid: hex.encode(input.txid) },
287
+ utxo: { hash: utxo.hash, index: utxo.index },
288
+ });
289
+ }
290
+ });
291
+
292
+ return derivationPaths.map((path) => {
293
+ const parsedPath = parseZcashPath(path);
294
+ if (parsedPath.accountPath !== accountPath) {
295
+ throw new SwapKitError("wallet_ledger_invalid_params", {
296
+ accountPath,
297
+ path: parsedPath.fullPath,
298
+ reason: "All Zcash input derivation paths must belong to the configured account",
299
+ });
300
+ }
301
+ return parsedPath.fullPath;
302
+ });
303
+ }
304
+
305
+ function assertRawV5(rawTransaction: string) {
306
+ const normalized = rawTransaction.replace(/^0x/i, "");
307
+ const bytes = hex.decode(normalized);
308
+ if (bytes.length < 4 || (new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, true) & 0x7fffffff) !== 5) {
309
+ throw new SwapKitError("wallet_ledger_invalid_response", {
310
+ reason: "Ledger Zcash signer did not return a version 5 transaction",
311
+ });
312
+ }
313
+ return normalized;
314
+ }
315
+
316
+ export function ZcashLedger({
317
+ derivationPath = NetworkDerivationPath.ZEC,
318
+ dmkSession,
319
+ onDeviceActionState,
320
+ transport,
321
+ }: ZcashLedgerParams = {}) {
322
+ const configuredPath = parseZcashPath(derivationPath);
323
+ const legacyClient = transport ? LegacyZcashLedger(derivationPath, transport) : undefined;
324
+ const getSessionSigner = createLedgerSessionSigner<SignerZcash>({
325
+ build: async (session) => {
326
+ const { SignerZcashBuilder } = await import("@ledgerhq/device-signer-kit-zcash");
327
+ return new SignerZcashBuilder(session).build();
328
+ },
329
+ dmkSession,
330
+ });
331
+
332
+ function getSigner() {
333
+ if (legacyClient) {
334
+ throw new SwapKitError("wallet_ledger_invalid_params", {
335
+ reason: "A Device Signer Kit operation is unavailable when a legacy transport is supplied",
336
+ });
337
+ }
338
+
339
+ return getSessionSigner();
340
+ }
341
+
342
+ async function getDskAddress({ checkOnDevice = false, path = configuredPath.fullPath } = {}) {
343
+ const signer = await getSigner();
344
+ return executeLedgerDeviceAction({ action: () => signer.getAddress(path, { checkOnDevice }), onDeviceActionState });
345
+ }
346
+
347
+ async function getExtendedPublicKey({
348
+ path = configuredPath.accountPath,
349
+ xpubVersion = 0x0488b21e,
350
+ }: GetExtendedPublicKeyParams = {}) {
351
+ const parsedAccount = parseZcashPath(path);
352
+ if (parsedAccount.fullPath !== `${parsedAccount.accountPath}/0/0` || normalizePath(path).split("/").length !== 3) {
353
+ throw new SwapKitError("wallet_ledger_invalid_params", {
354
+ path,
355
+ reason: "Zcash extended public keys are exported at account level",
356
+ });
357
+ }
358
+
359
+ const connection = {
360
+ dmkSession: transport ? undefined : (dmkSession ?? (await getLedgerDMKSession())),
361
+ onDeviceActionState,
362
+ transport,
363
+ };
364
+ const { default: BitcoinApp } = await import("@ledgerhq/hw-app-btc");
365
+ return runLedgerJsOperation({
366
+ appName: "Zcash",
367
+ connection,
368
+ createApp: (operationTransport) => new BitcoinApp({ currency: "zcash", transport: operationTransport }),
369
+ operation: (app) => app.getWalletXpub({ path: parsedAccount.accountPath, xpubVersion }),
370
+ });
371
+ }
372
+
373
+ async function signTransaction({ changePath, derivationPaths, inputUtxos, tx }: SignZcashTransactionParams) {
374
+ const paths = validateSigningParams({
375
+ accountPath: configuredPath.accountPath,
376
+ derivationPaths: derivationPaths ?? Array.from({ length: tx.inputsLength }, () => configuredPath.fullPath),
377
+ inputUtxos,
378
+ tx,
379
+ });
380
+
381
+ // hw-app-btc picks a pre-Ironwood branch id without a block height and misparses v5 previous
382
+ // transactions, so the legacy transport cannot produce a transaction the network accepts.
383
+ if (legacyClient) {
384
+ throw new SwapKitError("wallet_ledger_invalid_params", {
385
+ reason: "Zcash signing requires a Ledger DMK session instead of a legacy transport",
386
+ });
387
+ }
388
+
389
+ const blockHeight = await activationHeight(tx.consensusBranchId);
390
+ const inputs = await Promise.all(
391
+ inputUtxos.map(
392
+ async (utxo, inputIndex): Promise<LegacyCreateTransactionArg["inputs"][number]> => [
393
+ await previousTransaction({ inputIndex, utxo }),
394
+ utxo.index,
395
+ null,
396
+ tx.getInput(inputIndex).sequence ?? 0xffffffff,
397
+ ],
398
+ ),
399
+ );
400
+ const args: LegacyCreateTransactionArg = {
401
+ additionals: ["zcash", "sapling"],
402
+ associatedKeysets: paths,
403
+ blockHeight,
404
+ changePath: changePath ? parseZcashPath(changePath).fullPath : configuredPath.fullPath,
405
+ expiryHeight: uint32LE(tx.expiryHeight),
406
+ inputs,
407
+ lockTime: tx.lockTime,
408
+ outputScriptHex: hex.encode(await serializeOutputs(tx)),
409
+ };
410
+ const signer = await getSigner();
411
+ const signed = await executeLedgerDeviceAction({ action: () => signer.signTransaction(args), onDeviceActionState });
412
+ return assertRawV5(signed);
413
+ }
414
+
415
+ return {
416
+ connect: async () => {
417
+ if (legacyClient) return legacyClient.connect();
418
+ await getSigner();
419
+ },
420
+ disconnect: async () => legacyClient?.disconnect(),
421
+ getAddress: async () => {
422
+ if (legacyClient) return legacyClient.getAddress();
423
+ return (await getDskAddress()).address;
424
+ },
425
+ getAddressAndPubKey: async () => {
426
+ if (legacyClient) return { address: await legacyClient.getAddress() };
427
+ const { address, chainCode, publicKey } = await getDskAddress();
428
+ return { address, chainCode: hex.encode(chainCode), publicKey: hex.encode(publicKey) };
429
+ },
430
+ getExtendedPublicKey,
431
+ showAddressAndPubKey: async () => {
432
+ if (legacyClient) return { address: await legacyClient.getAddress() };
433
+ const { address, chainCode, publicKey } = await getDskAddress({ checkOnDevice: true });
434
+ return { address, chainCode: hex.encode(chainCode), publicKey: hex.encode(publicKey) };
435
+ },
436
+ signTransaction,
437
+ signTransactionWithMultiplePaths: async (params: SignZcashTransactionWithMultiplePathsParams) =>
438
+ signTransaction(params),
439
+ };
440
+ }
@@ -0,0 +1,231 @@
1
+ import type { DeviceManagementKit, DeviceSessionId, DiscoveredDevice } from "@ledgerhq/device-management-kit";
2
+ import { SwapKitError } from "@swapkit/helpers";
3
+ import { filter, firstValueFrom, map, timeout } from "rxjs";
4
+
5
+ import { toLedgerDeviceError } from "./executeDeviceAction";
6
+
7
+ // Bounds the wait for a granted device that DMK never lists (an unrecognised model); the picker opens instead.
8
+ const AUTHORISED_DEVICE_TIMEOUT_MS = 1_000;
9
+
10
+ type WebHidNavigator = Navigator & { hid?: { getDevices?: () => Promise<Array<{ vendorId: number }>> } };
11
+
12
+ export interface LedgerDMKSession {
13
+ dmk: DeviceManagementKit;
14
+ sessionId: DeviceSessionId;
15
+ }
16
+
17
+ let defaultDMKPromise: Promise<DeviceManagementKit> | undefined;
18
+ let defaultSessionPromise: Promise<LedgerDMKSession> | undefined;
19
+
20
+ function createDefaultDMKPromise() {
21
+ if (typeof navigator === "undefined" || !("hid" in navigator)) {
22
+ return Promise.reject(new SwapKitError("wallet_ledger_webhid_not_supported"));
23
+ }
24
+
25
+ return Promise.all([import("@ledgerhq/device-management-kit"), import("@ledgerhq/device-transport-kit-web-hid")])
26
+ .then(([{ DeviceManagementKitBuilder }, { webHidTransportFactory }]) =>
27
+ new DeviceManagementKitBuilder().addTransport(webHidTransportFactory).build(),
28
+ )
29
+ .catch((error) => {
30
+ defaultDMKPromise = undefined;
31
+ if (error instanceof SwapKitError) throw error;
32
+ throw new SwapKitError("wallet_ledger_connection_error", error);
33
+ });
34
+ }
35
+
36
+ function getDefaultDMK() {
37
+ defaultDMKPromise ??= createDefaultDMKPromise();
38
+ return defaultDMKPromise;
39
+ }
40
+
41
+ function createDefaultSessionPromise() {
42
+ return getDefaultDMK().then((dmk) => connectLedgerDMK({ dmk }));
43
+ }
44
+
45
+ function isSessionNotFound(error: unknown) {
46
+ return typeof error === "object" && error !== null && "_tag" in error && error._tag === "DeviceSessionNotFound";
47
+ }
48
+
49
+ function isSessionConnected({ dmk, sessionId }: LedgerDMKSession) {
50
+ try {
51
+ dmk.getConnectedDevice({ sessionId });
52
+ return true;
53
+ } catch (error) {
54
+ void error;
55
+ return false;
56
+ }
57
+ }
58
+
59
+ async function getValidDefaultSession({ sessionPromise }: { sessionPromise: Promise<LedgerDMKSession> }) {
60
+ let activeSessionPromise = sessionPromise;
61
+
62
+ try {
63
+ const session = await activeSessionPromise;
64
+ if (isSessionConnected(session)) return session;
65
+
66
+ if (defaultSessionPromise === activeSessionPromise) {
67
+ defaultSessionPromise = createDefaultSessionPromise();
68
+ }
69
+
70
+ defaultSessionPromise ??= createDefaultSessionPromise();
71
+ activeSessionPromise = defaultSessionPromise;
72
+
73
+ return await activeSessionPromise;
74
+ } catch (error) {
75
+ if (defaultSessionPromise === activeSessionPromise) defaultSessionPromise = undefined;
76
+ if (error instanceof SwapKitError) throw error;
77
+ throw new SwapKitError("wallet_ledger_connection_error", error);
78
+ }
79
+ }
80
+
81
+ async function hasAuthorisedLedger() {
82
+ const hid = typeof navigator === "undefined" ? undefined : (navigator as WebHidNavigator).hid;
83
+ if (typeof hid?.getDevices !== "function") return false;
84
+
85
+ try {
86
+ const [devices, { LEDGER_VENDOR_ID }] = await Promise.all([
87
+ hid.getDevices(),
88
+ import("@ledgerhq/device-management-kit"),
89
+ ]);
90
+ return devices.some(({ vendorId }) => vendorId === LEDGER_VENDOR_ID);
91
+ } catch (error) {
92
+ void error;
93
+ return false;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * `startDiscovering` always opens the WebHID picker, which needs a fresh user gesture. A plugged-in Ledger the user
99
+ * already granted (page reload, replug after DMK's reconnect window) is listed without one.
100
+ */
101
+ async function findAuthorisedDevice({ dmk }: { dmk: DeviceManagementKit }) {
102
+ // Nothing granted: go straight to the picker while the caller's user gesture is still fresh.
103
+ if (!(await hasAuthorisedLedger())) return undefined;
104
+
105
+ try {
106
+ return await firstValueFrom(
107
+ dmk.listenToAvailableDevices({}).pipe(
108
+ // The WebHID list replays its last value (initially empty) before it re-reads the granted devices.
109
+ map(([device]) => device),
110
+ filter((device): device is DiscoveredDevice => device !== undefined),
111
+ timeout({ first: AUTHORISED_DEVICE_TIMEOUT_MS }),
112
+ ),
113
+ );
114
+ } catch (error) {
115
+ void error;
116
+ return undefined;
117
+ }
118
+ }
119
+
120
+ async function connectLedgerDMK({ dmk }: { dmk: DeviceManagementKit }) {
121
+ let discoveryStarted = false;
122
+
123
+ try {
124
+ const authorisedDevice = await findAuthorisedDevice({ dmk });
125
+ if (authorisedDevice) return { dmk, sessionId: await dmk.connect({ device: authorisedDevice }) };
126
+
127
+ const discoveredDevices = dmk.startDiscovering({});
128
+ discoveryStarted = true;
129
+ const device = await firstValueFrom(discoveredDevices);
130
+ const sessionId = await dmk.connect({ device });
131
+
132
+ return { dmk, sessionId };
133
+ } catch (error) {
134
+ if (error instanceof SwapKitError) throw error;
135
+ throw new SwapKitError("wallet_ledger_connection_error", error);
136
+ } finally {
137
+ if (discoveryStarted) {
138
+ try {
139
+ await dmk.stopDiscovering();
140
+ } catch (error) {
141
+ void error;
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ export function getLedgerDMKSession({ dmk }: { dmk?: DeviceManagementKit } = {}) {
148
+ if (dmk) return connectLedgerDMK({ dmk });
149
+
150
+ defaultSessionPromise ??= createDefaultSessionPromise();
151
+ const cachedSessionPromise = defaultSessionPromise;
152
+
153
+ return getValidDefaultSession({ sessionPromise: cachedSessionPromise });
154
+ }
155
+
156
+ /**
157
+ * Builds a signer kit instance for the session an operation runs on. A caller-owned session is used
158
+ * as given; the default session is re-validated on every call and the signer is rebuilt when it was
159
+ * replaced. DMK keeps a session through a brief unplug; once it drops one, the next operation connects
160
+ * again, silently to a Ledger the user already granted, otherwise through the WebHID picker, which
161
+ * needs a user gesture.
162
+ */
163
+ export function createLedgerSessionSigner<Signer>({
164
+ build,
165
+ dmkSession,
166
+ }: {
167
+ build: (session: LedgerDMKSession) => Signer | Promise<Signer>;
168
+ dmkSession?: LedgerDMKSession;
169
+ }) {
170
+ let cached: { session: LedgerDMKSession; signer: Promise<Signer> } | undefined;
171
+
172
+ return async function getSessionSigner() {
173
+ const session = dmkSession ?? (await getLedgerDMKSession());
174
+ if (cached?.session.dmk !== session.dmk || cached.session.sessionId !== session.sessionId) {
175
+ const signer = Promise.resolve().then(() => build(session));
176
+ cached = { session, signer };
177
+ const pending = cached;
178
+ void signer.catch(() => {
179
+ if (cached === pending) cached = undefined;
180
+ });
181
+ }
182
+ return cached.signer;
183
+ };
184
+ }
185
+
186
+ export function preloadLedgerDMK() {
187
+ const dmkPromise = getDefaultDMK();
188
+ // Preloading is usually fire-and-forget; the connection that follows reports the same failure.
189
+ void dmkPromise.catch(() => undefined);
190
+ return dmkPromise;
191
+ }
192
+
193
+ export async function disconnectLedgerDMKSession({ dmkSession }: { dmkSession?: LedgerDMKSession } = {}) {
194
+ const cachedSessionPromise = defaultSessionPromise;
195
+ let sessionToDisconnect = dmkSession;
196
+
197
+ if (!sessionToDisconnect) {
198
+ if (!cachedSessionPromise) return;
199
+ if (defaultSessionPromise === cachedSessionPromise) defaultSessionPromise = undefined;
200
+
201
+ try {
202
+ sessionToDisconnect = await cachedSessionPromise;
203
+ } catch {
204
+ return;
205
+ }
206
+ }
207
+
208
+ if (cachedSessionPromise) {
209
+ try {
210
+ const cachedSession = await cachedSessionPromise;
211
+ if (
212
+ defaultSessionPromise === cachedSessionPromise &&
213
+ cachedSession.dmk === sessionToDisconnect.dmk &&
214
+ cachedSession.sessionId === sessionToDisconnect.sessionId
215
+ ) {
216
+ defaultSessionPromise = undefined;
217
+ }
218
+ } catch (error) {
219
+ void error;
220
+ if (defaultSessionPromise === cachedSessionPromise) defaultSessionPromise = undefined;
221
+ }
222
+ }
223
+
224
+ try {
225
+ await sessionToDisconnect.dmk.disconnect({ sessionId: sessionToDisconnect.sessionId });
226
+ } catch (error) {
227
+ // DMK already dropped the session (device unplugged past its reconnect window): nothing left to release.
228
+ if (isSessionNotFound(error)) return;
229
+ throw toLedgerDeviceError(error);
230
+ }
231
+ }