@ledgerhq/coin-canton 0.9.0-nightly.1 → 0.9.0-nightly.2

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 (97) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +75 -0
  4. package/lib/bridge/deviceTransactionConfig.d.ts +1 -1
  5. package/lib/bridge/deviceTransactionConfig.d.ts.map +1 -1
  6. package/lib/bridge/deviceTransactionConfig.js +1 -1
  7. package/lib/bridge/deviceTransactionConfig.js.map +1 -1
  8. package/lib/bridge/onboard.d.ts.map +1 -1
  9. package/lib/bridge/onboard.js +48 -22
  10. package/lib/bridge/onboard.js.map +1 -1
  11. package/lib/bridge/signOperation.d.ts.map +1 -1
  12. package/lib/bridge/signOperation.js +10 -3
  13. package/lib/bridge/signOperation.js.map +1 -1
  14. package/lib/common-logic/index.d.ts +1 -0
  15. package/lib/common-logic/index.d.ts.map +1 -1
  16. package/lib/common-logic/index.js +3 -1
  17. package/lib/common-logic/index.js.map +1 -1
  18. package/lib/common-logic/transaction/sign.d.ts +8 -0
  19. package/lib/common-logic/transaction/sign.d.ts.map +1 -0
  20. package/lib/common-logic/transaction/sign.js +27 -0
  21. package/lib/common-logic/transaction/sign.js.map +1 -0
  22. package/lib/common-logic/transaction/split.d.ts +9 -0
  23. package/lib/common-logic/transaction/split.d.ts.map +1 -0
  24. package/lib/common-logic/transaction/split.js +119 -0
  25. package/lib/common-logic/transaction/split.js.map +1 -0
  26. package/lib/common-logic/utils.js +2 -2
  27. package/lib/common-logic/utils.js.map +1 -1
  28. package/lib/network/gateway.d.ts +1 -0
  29. package/lib/network/gateway.d.ts.map +1 -1
  30. package/lib/network/gateway.js +2 -7
  31. package/lib/network/gateway.js.map +1 -1
  32. package/lib/test/cantonTestUtils.d.ts +4 -9
  33. package/lib/test/cantonTestUtils.d.ts.map +1 -1
  34. package/lib/test/cantonTestUtils.js +736 -27
  35. package/lib/test/cantonTestUtils.js.map +1 -1
  36. package/lib/types/signer.d.ts +10 -1
  37. package/lib/types/signer.d.ts.map +1 -1
  38. package/lib/types/transaction-proto.json +1238 -0
  39. package/lib-es/bridge/deviceTransactionConfig.d.ts +1 -1
  40. package/lib-es/bridge/deviceTransactionConfig.d.ts.map +1 -1
  41. package/lib-es/bridge/deviceTransactionConfig.js +1 -1
  42. package/lib-es/bridge/deviceTransactionConfig.js.map +1 -1
  43. package/lib-es/bridge/onboard.d.ts.map +1 -1
  44. package/lib-es/bridge/onboard.js +49 -23
  45. package/lib-es/bridge/onboard.js.map +1 -1
  46. package/lib-es/bridge/signOperation.d.ts.map +1 -1
  47. package/lib-es/bridge/signOperation.js +10 -3
  48. package/lib-es/bridge/signOperation.js.map +1 -1
  49. package/lib-es/common-logic/index.d.ts +1 -0
  50. package/lib-es/common-logic/index.d.ts.map +1 -1
  51. package/lib-es/common-logic/index.js +1 -0
  52. package/lib-es/common-logic/index.js.map +1 -1
  53. package/lib-es/common-logic/transaction/sign.d.ts +8 -0
  54. package/lib-es/common-logic/transaction/sign.d.ts.map +1 -0
  55. package/lib-es/common-logic/transaction/sign.js +24 -0
  56. package/lib-es/common-logic/transaction/sign.js.map +1 -0
  57. package/lib-es/common-logic/transaction/split.d.ts +9 -0
  58. package/lib-es/common-logic/transaction/split.d.ts.map +1 -0
  59. package/lib-es/common-logic/transaction/split.js +83 -0
  60. package/lib-es/common-logic/transaction/split.js.map +1 -0
  61. package/lib-es/common-logic/utils.js +2 -2
  62. package/lib-es/common-logic/utils.js.map +1 -1
  63. package/lib-es/network/gateway.d.ts +1 -0
  64. package/lib-es/network/gateway.d.ts.map +1 -1
  65. package/lib-es/network/gateway.js +1 -8
  66. package/lib-es/network/gateway.js.map +1 -1
  67. package/lib-es/test/cantonTestUtils.d.ts +4 -9
  68. package/lib-es/test/cantonTestUtils.d.ts.map +1 -1
  69. package/lib-es/test/cantonTestUtils.js +697 -21
  70. package/lib-es/test/cantonTestUtils.js.map +1 -1
  71. package/lib-es/types/signer.d.ts +10 -1
  72. package/lib-es/types/signer.d.ts.map +1 -1
  73. package/lib-es/types/transaction-proto.json +1238 -0
  74. package/package.json +10 -7
  75. package/scripts/generate.js +261 -0
  76. package/src/bridge/deviceTransactionConfig.test.ts +14 -10
  77. package/src/bridge/deviceTransactionConfig.ts +2 -2
  78. package/src/bridge/getTransactionStatus.test.ts +19 -19
  79. package/src/bridge/onboard.integ.test.ts +0 -20
  80. package/src/bridge/onboard.ts +57 -33
  81. package/src/bridge/signOperation.test.ts +114 -0
  82. package/src/bridge/signOperation.ts +12 -5
  83. package/src/bridge/sync.integ.test.ts +157 -132
  84. package/src/common-logic/index.ts +1 -0
  85. package/src/common-logic/transaction/sign.test.ts +317 -0
  86. package/src/common-logic/transaction/sign.ts +33 -0
  87. package/src/common-logic/transaction/split.test.ts +50 -0
  88. package/src/common-logic/transaction/split.ts +101 -0
  89. package/src/common-logic/utils.test.ts +22 -30
  90. package/src/common-logic/utils.ts +2 -2
  91. package/src/network/gateway.integ.test.ts +2 -0
  92. package/src/network/gateway.ts +3 -8
  93. package/src/test/cantonTestUtils.ts +789 -24
  94. package/src/test/prepare-transfer-serialized.json +26 -0
  95. package/src/test/prepare-transfer.json +3298 -0
  96. package/src/types/signer.ts +17 -3
  97. package/src/types/transaction-proto.json +1238 -0
@@ -1,11 +1,28 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
1
3
  /**
2
4
  * Canton Testing Utilities for Ed25519 Key Generation
3
5
  *
4
6
  * Provides utilities to generate proper Ed25519 keypairs for testing Canton
5
7
  * onboarding without requiring physical Ledger devices.
8
+ * The mock signer implements canonical hash algorithm used by the Canton app.
6
9
  */
7
10
 
8
11
  import crypto from "crypto";
12
+ import * as protobuf from "protobufjs";
13
+ import { CantonPreparedTransaction, CantonUntypedVersionedMessage } from "../types/signer";
14
+ import * as transactionProto from "../types/transaction-proto.json";
15
+
16
+ const root: { [key: string]: any } = protobuf.Root.fromJSON(transactionProto) || {};
17
+
18
+ // Constants from app-canton
19
+ const PREPARED_TRANSACTION_HASH_PURPOSE = Buffer.from([0x00, 0x00, 0x00, 0x30]);
20
+ const HASHING_SCHEME_VERSION = 0x02;
21
+ const NODE_ENCODING_VERSION = 0x01;
22
+ const PURPOSE_PUBLIC_KEY_FINGERPRINT = 12;
23
+ const PURPOSE_TOPOLOGY_TRANSACTION_SIGNATURE = 11;
24
+ const PURPOSE_MULTI_TOPOLOGY_TRANSACTION_SIGNATURE = 55;
25
+ const MULTIHASH_SHA256_PREFIX = Buffer.from([0x12, 0x20]);
9
26
 
10
27
  export interface CantonTestKeyPair {
11
28
  publicKeyHex: string; // Ready for Canton Gateway API
@@ -31,8 +48,6 @@ export function generateMockKeyPair(): CantonTestKeyPair {
31
48
  const privateKeyHex = privateKeyDer.toString("hex");
32
49
 
33
50
  // Generate fingerprint: Canton computes SHA256(purpose_bytes + public_key_bytes)
34
- // where purpose_bytes is 4-byte big-endian representation of PURPOSE_PUBLIC_KEY_FINGERPRINT (12)
35
- const PURPOSE_PUBLIC_KEY_FINGERPRINT = 12;
36
51
  const purposeBytes = Buffer.allocUnsafe(4);
37
52
  purposeBytes.writeInt32BE(PURPOSE_PUBLIC_KEY_FINGERPRINT, 0);
38
53
 
@@ -42,8 +57,7 @@ export function generateMockKeyPair(): CantonTestKeyPair {
42
57
  const hashedContent = hash.digest();
43
58
 
44
59
  // Multihash encoding: 0x12 (SHA256) + 0x20 (32 bytes) + hash
45
- const multihashPrefix = Buffer.from([0x12, 0x20]);
46
- const fingerprintBuffer = Buffer.concat([multihashPrefix, hashedContent]);
60
+ const fingerprintBuffer = Buffer.concat([MULTIHASH_SHA256_PREFIX, hashedContent]);
47
61
  const fingerprint = fingerprintBuffer.toString("hex");
48
62
 
49
63
  return {
@@ -62,9 +76,7 @@ export function generateMockKeyPair(): CantonTestKeyPair {
62
76
  format: "pem",
63
77
  type: "pkcs8",
64
78
  });
65
-
66
- const signature = crypto.sign(null, hashBuffer, privateKeyObj);
67
- return signature.toString("hex");
79
+ return crypto.sign(null, hashBuffer, privateKeyObj).toString("hex");
68
80
  },
69
81
  };
70
82
  }
@@ -76,7 +88,7 @@ export function verifySignature(
76
88
  publicKeyHex: string,
77
89
  signatureHex: string,
78
90
  messageHashHex: string,
79
- ): { isValid: boolean; error?: string; details: any } {
91
+ ): { isValid: boolean; error?: string; details: Record<string, string | number> } {
80
92
  try {
81
93
  // Clean inputs - remove 0x prefixes if present
82
94
  const cleanPublicKey = publicKeyHex.startsWith("0x") ? publicKeyHex.slice(2) : publicKeyHex;
@@ -85,13 +97,10 @@ export function verifySignature(
85
97
  ? messageHashHex.slice(2)
86
98
  : messageHashHex;
87
99
 
88
- const details: any = {
100
+ const details: Record<string, string | number> = {
89
101
  publicKeyLength: cleanPublicKey.length,
90
102
  signatureLength: cleanSignature.length,
91
103
  messageHashLength: cleanMessageHash.length,
92
- publicKeyBytes: cleanPublicKey.length / 2,
93
- signatureBytes: cleanSignature.length / 2,
94
- messageHashBytes: cleanMessageHash.length / 2,
95
104
  };
96
105
 
97
106
  // Validate input lengths
@@ -103,16 +112,14 @@ export function verifySignature(
103
112
  };
104
113
  }
105
114
 
106
- // Ed25519 signatures should be 64 bytes, but we might receive 65 bytes with recovery ID
115
+ // Handle 65-byte signatures (remove recovery ID)
107
116
  let processedSignature = cleanSignature;
108
117
  if (cleanSignature.length === 130) {
109
118
  processedSignature = cleanSignature.slice(2, -2);
110
- details.originalSignatureLength = cleanSignature.length;
111
- details.processedSignatureLength = processedSignature.length;
112
119
  } else if (cleanSignature.length !== 128) {
113
120
  return {
114
121
  isValid: false,
115
- error: `Invalid signature length: expected 128 hex chars (64 bytes) or 130 hex chars (65 bytes), got ${cleanSignature.length}`,
122
+ error: `Invalid signature length: expected 128 or 130 hex chars, got ${cleanSignature.length}`,
116
123
  details,
117
124
  };
118
125
  }
@@ -151,11 +158,7 @@ export function verifySignature(
151
158
 
152
159
  return {
153
160
  isValid,
154
- details: {
155
- ...details,
156
- processedSignatureLength: processedSignature.length,
157
- verificationMethod: "Node.js crypto.verify with Ed25519",
158
- },
161
+ details: { ...details, processedSignatureLength: processedSignature.length },
159
162
  };
160
163
  } catch (error) {
161
164
  return {
@@ -168,14 +171,776 @@ export function verifySignature(
168
171
 
169
172
  export function createMockSigner(keyPair: CantonTestKeyPair) {
170
173
  return {
171
- getAddress: async (derivationPath: string) => ({
174
+ getAddress: async (_derivationPath: string) => ({
172
175
  address: `canton_test_${keyPair.fingerprint.slice(-8)}`,
173
176
  publicKey: keyPair.publicKeyHex,
174
177
  }),
175
178
 
176
- signTransaction: async (derivationPath: string, hashToSign: string) => {
179
+ signTransaction: async (
180
+ _derivationPath: string,
181
+ data: CantonPreparedTransaction | CantonUntypedVersionedMessage | string,
182
+ ) => {
183
+ let hashToSign: string;
184
+
185
+ if (typeof data === "object" && "transactions" in data) {
186
+ // CantonUntypedVersionedMessage - process multiple topology transactions
187
+ const hashes = data.transactions.map(tx => {
188
+ const txBytes = typeof tx === "string" ? Buffer.from(tx, "hex") : Buffer.from(tx);
189
+ return computeCantonHash(PURPOSE_TOPOLOGY_TRANSACTION_SIGNATURE, txBytes);
190
+ });
191
+
192
+ // Sort hashes lexicographically in hex format
193
+ const sortedHashes = hashes.sort((a, b) =>
194
+ a.toString("hex").localeCompare(b.toString("hex")),
195
+ );
196
+
197
+ // Concatenate with length prefixes: 4-byte count + for each hash: 4-byte length + 34-byte hash
198
+ const concatBuffer = Buffer.alloc(4 + sortedHashes.length * (4 + 34));
199
+ let offset = 0;
200
+
201
+ // Write count of hashes (4 bytes big-endian)
202
+ concatBuffer.writeUInt32BE(sortedHashes.length, offset);
203
+ offset += 4;
204
+
205
+ // Write each hash with length prefix
206
+ for (const hash of sortedHashes) {
207
+ concatBuffer.writeUInt32BE(34, offset); // Length is always 34 bytes
208
+ offset += 4;
209
+ hash.copy(concatBuffer, offset);
210
+ offset += 34;
211
+ }
212
+
213
+ // Compute final multi-hash
214
+ const finalHash = computeCantonHash(
215
+ PURPOSE_MULTI_TOPOLOGY_TRANSACTION_SIGNATURE,
216
+ concatBuffer,
217
+ );
218
+ hashToSign = finalHash.toString("hex");
219
+ } else if (typeof data === "object") {
220
+ const canonicalHash = computeCanonicalHash(data);
221
+ hashToSign = canonicalHash.toString("hex");
222
+ } else {
223
+ hashToSign = data;
224
+ }
225
+
177
226
  const cleanHash = hashToSign.startsWith("0x") ? hashToSign.slice(2) : hashToSign;
178
- return keyPair.sign(cleanHash);
227
+ const signature = keyPair.sign(cleanHash);
228
+ return signature;
179
229
  },
180
230
  };
181
231
  }
232
+
233
+ /**
234
+ * Compute Canton hash with purpose and multihash encoding
235
+ */
236
+ function computeCantonHash(purpose: number, data: Buffer): Buffer {
237
+ const purposeBytes = Buffer.allocUnsafe(4);
238
+ purposeBytes.writeUInt32BE(purpose, 0);
239
+
240
+ const hash = crypto.createHash("sha256");
241
+ hash.update(purposeBytes);
242
+ hash.update(data);
243
+ const hashedContent = hash.digest();
244
+
245
+ // Multihash encoding: 0x12 (SHA256) + 0x20 (32 bytes) + hash
246
+ return Buffer.concat([MULTIHASH_SHA256_PREFIX, hashedContent]);
247
+ }
248
+
249
+ /**
250
+ * Compute canonical hash for CantonPreparedTransaction
251
+ * Implements canonical_hash.c
252
+ */
253
+ function computeCanonicalHash(data: CantonPreparedTransaction): Buffer {
254
+ // Step 1: Parse DAML transaction
255
+ const DeviceDamlTransaction = root.lookupType(
256
+ "com.daml.ledger.api.v2.interactive.DeviceDamlTransaction",
257
+ );
258
+ const damlTx = DeviceDamlTransaction.decode(data.damlTransaction);
259
+
260
+ // Step 2: Hash transaction (hash_transaction)
261
+ const txHash = hashTransaction(damlTx);
262
+
263
+ // Step 3: Hash nodes (hash_node)
264
+ const nodesHash = hashNodes(damlTx, data.nodes);
265
+
266
+ // Step 4: Combine transaction and nodes hash
267
+ const combinedTxHash = crypto.createHash("sha256");
268
+ combinedTxHash.update(txHash);
269
+ combinedTxHash.update(nodesHash);
270
+ const finalTxHash = combinedTxHash.digest();
271
+
272
+ // Step 5: Hash metadata (hash_metadata)
273
+ const DeviceMetadata = root.lookupType("com.daml.ledger.api.v2.interactive.DeviceMetadata");
274
+ const metadata = DeviceMetadata.decode(data.metadata);
275
+ const metadataHash = hashMetadata(metadata, data.inputContracts);
276
+
277
+ // Step 6: Finalize hash (finalize_hash)
278
+ return finalizeHash(finalTxHash, metadataHash);
279
+ }
280
+
281
+ /**
282
+ * Hash DAML transaction
283
+ * Implements hash_transaction from canonical_hash.c
284
+ */
285
+ function hashTransaction(damlTx: any): Buffer {
286
+ const hash = crypto.createHash("sha256");
287
+
288
+ // Add purpose (PREPARED_TRANSACTION_HASH_PURPOSE = {0x00, 0x00, 0x00, 0x30})
289
+ hash.update(PREPARED_TRANSACTION_HASH_PURPOSE);
290
+
291
+ // Encode version string
292
+ encodeString(hash, damlTx.version || "");
293
+
294
+ // Encode roots count
295
+ encodeInt32(hash, damlTx.rootsCount || 0);
296
+
297
+ return hash.digest();
298
+ }
299
+
300
+ /**
301
+ * Hash nodes
302
+ * Implements hash_node from canonical_hash.c
303
+ */
304
+ function hashNodes(damlTx: any, nodes: Uint8Array[]): Buffer {
305
+ const hash = crypto.createHash("sha256");
306
+
307
+ // Process each node
308
+ for (const nodeBytes of nodes) {
309
+ const Node = root.lookupType("com.daml.ledger.api.v2.interactive.DeviceDamlTransaction.Node");
310
+ const node = Node.decode(nodeBytes);
311
+
312
+ // Check if this is a root node
313
+ const isRootNode = damlTx.roots?.includes(node.nodeId);
314
+
315
+ // Hash the node (encode_node_id_hash)
316
+ const nodeHash = hashNodeId(node, isRootNode, damlTx.nodeSeeds || []);
317
+
318
+ if (isRootNode) {
319
+ // For root nodes, add directly to the hash
320
+ hash.update(nodeHash);
321
+ }
322
+ // Non-root nodes are ignored in this implementation
323
+ }
324
+
325
+ return hash.digest();
326
+ }
327
+
328
+ /**
329
+ * Hash a single node
330
+ * Implements encode_node_id_hash from canonical_hash.c
331
+ */
332
+ function hashNodeId(node: any, isRootNode: boolean, nodeSeeds: any[]): Buffer {
333
+ // Create separate hash writer for the node
334
+ const nodeHash = crypto.createHash("sha256");
335
+
336
+ // Encode the node based on its type (encode_node)
337
+ if (node.v1) {
338
+ if (node.v1.create) {
339
+ encodeCreateNode(nodeHash, node.v1.create, node.nodeId, nodeSeeds);
340
+ } else if (node.v1.exercise) {
341
+ encodeExerciseNode(nodeHash, node.v1.exercise, node.nodeId, nodeSeeds);
342
+ } else if (node.v1.fetch) {
343
+ encodeFetchNode(nodeHash, node.v1.fetch);
344
+ } else if (node.v1.rollback) {
345
+ encodeRollbackNode(nodeHash, node.v1.rollback);
346
+ }
347
+ }
348
+
349
+ return nodeHash.digest();
350
+ }
351
+
352
+ /**
353
+ * Encode create node
354
+ * Implements encode_create from canonical_hash.c
355
+ */
356
+ function encodeCreateNode(hash: crypto.Hash, create: any, nodeId: string, nodeSeeds: any[]): void {
357
+ // NODE_ENCODING_VERSION = 0x01
358
+ hash.update(Buffer.from([NODE_ENCODING_VERSION]));
359
+
360
+ // lf_version string
361
+ encodeString(hash, create.lfVersion || "");
362
+
363
+ // 0x00 byte
364
+ hash.update(Buffer.from([0x00]));
365
+
366
+ // Optional seed (find_seed)
367
+ const seed = findNodeSeed(nodeId, nodeSeeds);
368
+ if (seed) {
369
+ // Seed is present - encode as hash
370
+ hash.update(seed);
371
+ } else {
372
+ // No seed - encode as optional false
373
+ hash.update(Buffer.from([0x00]));
374
+ }
375
+
376
+ // contract_id as hex string
377
+ encodeHexString(hash, create.contractId || "");
378
+
379
+ // package_name string
380
+ encodeString(hash, create.packageName || "");
381
+
382
+ // template_id identifier
383
+ if (create.templateId) {
384
+ encodeIdentifier(hash, create.templateId);
385
+ }
386
+
387
+ // argument value (this needs proper DAML value encoding)
388
+ if (create.argument) {
389
+ encodeValue(hash, create.argument);
390
+ }
391
+
392
+ // signatories repeated strings
393
+ if (create.signatories) {
394
+ encodeRepeatedStrings(hash, create.signatories);
395
+ }
396
+
397
+ // stakeholders repeated strings
398
+ if (create.stakeholders) {
399
+ encodeRepeatedStrings(hash, create.stakeholders);
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Encode exercise node
405
+ * Implements encode_exercise from canonical_hash.c
406
+ */
407
+ function encodeExerciseNode(
408
+ hash: crypto.Hash,
409
+ exercise: any,
410
+ nodeId: string,
411
+ nodeSeeds: any[],
412
+ ): void {
413
+ // NODE_ENCODING_VERSION = 0x01
414
+ hash.update(Buffer.from([NODE_ENCODING_VERSION]));
415
+
416
+ // lf_version string
417
+ encodeString(hash, exercise.lfVersion || "");
418
+
419
+ // 0x01 byte
420
+ hash.update(Buffer.from([0x01]));
421
+
422
+ // Seed is always present for exercise nodes
423
+ const seed = findNodeSeed(nodeId, nodeSeeds);
424
+ if (seed) {
425
+ encodeHash(hash, seed);
426
+ } else {
427
+ // This should not happen, but handle gracefully
428
+ hash.update(Buffer.alloc(32, 0));
429
+ }
430
+
431
+ // contract_id as hex string
432
+ encodeHexString(hash, exercise.contractId || "");
433
+
434
+ // package_name string
435
+ encodeString(hash, exercise.packageName || "");
436
+
437
+ // template_id identifier (required for exercise nodes)
438
+ if (exercise.templateId) {
439
+ encodeIdentifier(hash, exercise.templateId);
440
+ }
441
+
442
+ // choice string
443
+ encodeString(hash, exercise.choice || "");
444
+
445
+ // argument value
446
+ if (exercise.argument) {
447
+ encodeValue(hash, exercise.argument);
448
+ }
449
+
450
+ // acting_parties repeated strings
451
+ if (exercise.actingParties) {
452
+ encodeRepeatedStrings(hash, exercise.actingParties);
453
+ }
454
+
455
+ // children repeated strings
456
+ if (exercise.children) {
457
+ encodeRepeatedStrings(hash, exercise.children);
458
+ }
459
+
460
+ // exercise_result optional value
461
+ if (exercise.exerciseResult) {
462
+ // Optional present
463
+ hash.update(Buffer.from([0x01]));
464
+ encodeValue(hash, exercise.exerciseResult);
465
+ } else {
466
+ // Optional not present
467
+ hash.update(Buffer.from([0x00]));
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Encode fetch node
473
+ * Implements encode_fetch from canonical_hash.c
474
+ */
475
+ function encodeFetchNode(hash: crypto.Hash, fetch: any): void {
476
+ // NODE_ENCODING_VERSION = 0x01
477
+ hash.update(Buffer.from([NODE_ENCODING_VERSION]));
478
+
479
+ // lf_version string
480
+ encodeString(hash, fetch.lfVersion || "");
481
+
482
+ // 0x02 byte
483
+ hash.update(Buffer.from([0x02]));
484
+
485
+ // contract_id as hex string
486
+ encodeHexString(hash, fetch.contractId || "");
487
+
488
+ // package_name string
489
+ encodeString(hash, fetch.packageName || "");
490
+
491
+ // template_id identifier
492
+ if (fetch.templateId) {
493
+ encodeIdentifier(hash, fetch.templateId);
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Encode rollback node
499
+ * Implements encode_rollback from canonical_hash.c
500
+ */
501
+ function encodeRollbackNode(hash: crypto.Hash, rollback: any): void {
502
+ // NODE_ENCODING_VERSION = 0x01
503
+ hash.update(Buffer.from([NODE_ENCODING_VERSION]));
504
+
505
+ // lf_version string
506
+ encodeString(hash, rollback.lfVersion || "");
507
+
508
+ // 0x03 byte
509
+ hash.update(Buffer.from([0x03]));
510
+
511
+ // children repeated strings
512
+ if (rollback.children) {
513
+ encodeRepeatedStrings(hash, rollback.children);
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Find node seed for a given node ID
519
+ */
520
+ function findNodeSeed(nodeId: string, nodeSeeds: any[]): Buffer | null {
521
+ const nodeIdNum = Number.parseInt(nodeId, 10);
522
+ for (const seed of nodeSeeds) {
523
+ if (seed.nodeId === nodeIdNum) {
524
+ return Buffer.from(seed.seed);
525
+ }
526
+ }
527
+ return null;
528
+ }
529
+
530
+ /**
531
+ * Encode identifier
532
+ */
533
+ function encodeIdentifier(hash: crypto.Hash, identifier: any): void {
534
+ if (identifier.packageId) {
535
+ encodeString(hash, identifier.packageId);
536
+ }
537
+
538
+ if (identifier.moduleName) {
539
+ splitDotAndEncode(hash, identifier.moduleName);
540
+ }
541
+
542
+ if (identifier.entityName) {
543
+ splitDotAndEncode(hash, identifier.entityName);
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Split dot-separated string and encode
549
+ */
550
+ function splitDotAndEncode(hash: crypto.Hash, dotStr: string): void {
551
+ const parts = dotStr.split(".");
552
+ encodeInt32(hash, parts.length);
553
+
554
+ for (const part of parts) {
555
+ encodeInt32(hash, part.length);
556
+ hash.update(Buffer.from(part, "utf8"));
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Hash metadata
562
+ * Implements hash_metadata from canonical_hash.c
563
+ */
564
+ function hashMetadata(metadata: any, inputContracts: Uint8Array[]): Buffer {
565
+ const hash = crypto.createHash("sha256");
566
+
567
+ // Add purpose (PREPARED_TRANSACTION_HASH_PURPOSE = {0x00, 0x00, 0x00, 0x30})
568
+ hash.update(PREPARED_TRANSACTION_HASH_PURPOSE);
569
+
570
+ // Encode metadata (encode_metadata)
571
+ // Version marker (0x01)
572
+ hash.update(Buffer.from([0x01]));
573
+
574
+ // Encode submitter info
575
+ if (metadata.submitterInfo) {
576
+ // act_as repeated strings
577
+ if (metadata.submitterInfo.actAs) {
578
+ encodeRepeatedStrings(hash, metadata.submitterInfo.actAs);
579
+ } else {
580
+ encodeInt32(hash, 0);
581
+ }
582
+ // command_id string
583
+ encodeString(hash, metadata.submitterInfo.commandId || "");
584
+ } else {
585
+ // No submitter info - encode empty
586
+ encodeInt32(hash, 0);
587
+ encodeString(hash, "");
588
+ }
589
+
590
+ // Encode other metadata fields
591
+ encodeString(hash, metadata.transactionUuid || "");
592
+ encodeInt32(hash, metadata.mediatorGroup || 0);
593
+ encodeString(hash, metadata.synchronizerId || "");
594
+
595
+ // Encode optional time fields
596
+ if (metadata.minLedgerEffectiveTime === undefined) {
597
+ // Optional not present
598
+ hash.update(Buffer.from([0x00]));
599
+ } else {
600
+ // Optional present
601
+ hash.update(Buffer.from([0x01]));
602
+ encodeInt64(hash, metadata.minLedgerEffectiveTime);
603
+ }
604
+
605
+ if (metadata.maxLedgerEffectiveTime === undefined) {
606
+ // Optional not present
607
+ hash.update(Buffer.from([0x00]));
608
+ } else {
609
+ // Optional present
610
+ hash.update(Buffer.from([0x01]));
611
+ encodeInt64(hash, metadata.maxLedgerEffectiveTime);
612
+ }
613
+
614
+ // Encode submission time and input contracts count
615
+ encodeInt64(hash, metadata.submissionTime || 0);
616
+ encodeInt32(hash, metadata.inputContractsCount || 0);
617
+
618
+ // Hash input contracts
619
+ for (const contract of inputContracts) {
620
+ const contractHash = hashInputContract(contract);
621
+ encodeHash(hash, contractHash);
622
+ }
623
+
624
+ return hash.digest();
625
+ }
626
+
627
+ /**
628
+ * Hash input contract
629
+ * Implements hash_input_contract from canonical_hash.c
630
+ */
631
+ function hashInputContract(contract: Uint8Array): Buffer {
632
+ const InputContract = root.lookupType(
633
+ "com.daml.ledger.api.v2.interactive.DeviceMetadata.InputContract",
634
+ );
635
+ const contractData = InputContract.decode(contract);
636
+
637
+ const hash = crypto.createHash("sha256");
638
+
639
+ // Encode created_at (int64)
640
+ encodeInt64(hash, contractData.createdAt || 0);
641
+
642
+ // Hash the contract create node in separate buffer
643
+ const nodeHash = crypto.createHash("sha256");
644
+
645
+ // Encode the create node
646
+ if (contractData.v1) {
647
+ // Simplified encode_create approach
648
+ nodeHash.update(Buffer.from([NODE_ENCODING_VERSION])); // NODE_ENCODING_VERSION = 0x01
649
+ encodeString(nodeHash, contractData.v1.lfVersion || "");
650
+ nodeHash.update(Buffer.from([0x00])); // 0x00 byte
651
+ nodeHash.update(Buffer.from([0x00])); // No seed for input contracts
652
+ encodeHexString(nodeHash, contractData.v1.contractId || "");
653
+ encodeString(nodeHash, contractData.v1.packageName || "");
654
+
655
+ if (contractData.v1.templateId) {
656
+ encodeIdentifier(nodeHash, contractData.v1.templateId);
657
+ }
658
+
659
+ if (contractData.v1.argument) {
660
+ encodeValue(nodeHash, contractData.v1.argument);
661
+ }
662
+
663
+ if (contractData.v1.signatories) {
664
+ encodeRepeatedStrings(nodeHash, contractData.v1.signatories);
665
+ }
666
+
667
+ if (contractData.v1.stakeholders) {
668
+ encodeRepeatedStrings(nodeHash, contractData.v1.stakeholders);
669
+ }
670
+ }
671
+
672
+ const contractNodeHash = nodeHash.digest();
673
+ encodeHash(hash, contractNodeHash);
674
+
675
+ return hash.digest();
676
+ }
677
+
678
+ /**
679
+ * Finalize hash
680
+ * Implements finalize_hash from canonical_hash.c
681
+ */
682
+ function finalizeHash(txHash: Buffer, metadataHash: Buffer): Buffer {
683
+ const hash = crypto.createHash("sha256");
684
+
685
+ // Add purpose (PREPARED_TRANSACTION_HASH_PURPOSE = {0x00, 0x00, 0x00, 0x30})
686
+ hash.update(PREPARED_TRANSACTION_HASH_PURPOSE);
687
+
688
+ // Add hashing scheme version (HASHING_SCHEME_VERSION = 0x02)
689
+ hash.update(Buffer.from([HASHING_SCHEME_VERSION]));
690
+
691
+ // Add transaction hash (32 bytes)
692
+ hash.update(txHash);
693
+
694
+ // Add metadata hash (32 bytes)
695
+ hash.update(metadataHash);
696
+
697
+ return hash.digest();
698
+ }
699
+
700
+ // Helper functions for canonical encoding
701
+ function encodeString(hash: crypto.Hash, value: string): void {
702
+ const bytes = Buffer.from(value, "utf8");
703
+ encodeBytes(hash, bytes);
704
+ }
705
+
706
+ function encodeInt32(hash: crypto.Hash, value: number | null | undefined): void {
707
+ const buffer = Buffer.allocUnsafe(4);
708
+ const safeValue = value ?? 0;
709
+ buffer.writeUInt32BE(safeValue, 0);
710
+ hash.update(buffer);
711
+ }
712
+
713
+ function encodeInt64(hash: crypto.Hash, value: number | null | undefined): void {
714
+ const buffer = Buffer.allocUnsafe(8);
715
+ const safeValue = value ?? 0;
716
+ buffer.writeBigUInt64BE(BigInt(safeValue), 0);
717
+ hash.update(buffer);
718
+ }
719
+
720
+ function encodeBytes(hash: crypto.Hash, bytes: Buffer): void {
721
+ encodeInt32(hash, bytes.length);
722
+ hash.update(bytes);
723
+ }
724
+
725
+ function encodeRepeatedStrings(hash: crypto.Hash, strings: string[]): void {
726
+ encodeInt32(hash, strings.length);
727
+ for (const str of strings) {
728
+ encodeString(hash, str);
729
+ }
730
+ }
731
+
732
+ function encodeHash(hash: crypto.Hash, hashValue: Buffer): void {
733
+ hash.update(hashValue);
734
+ }
735
+
736
+ /**
737
+ * Encode DAML value
738
+ * Implements encode_value from canonical_hash.c
739
+ */
740
+ function encodeValue(hash: crypto.Hash, value: any): void {
741
+ if (!value || typeof value !== "object") {
742
+ // Handle primitive values
743
+ if (value === null || value === undefined) {
744
+ // VALUE_UNIT_TAG = 0x00
745
+ hash.update(Buffer.from([0x00]));
746
+ } else if (typeof value === "boolean") {
747
+ // VALUE_BOOL_TAG = 0x01
748
+ hash.update(Buffer.from([0x01]));
749
+ encodeBool(hash, value);
750
+ } else if (typeof value === "number" && Number.isInteger(value)) {
751
+ // VALUE_INT64_TAG = 0x02
752
+ hash.update(Buffer.from([0x02]));
753
+ encodeInt64(hash, value);
754
+ } else if (typeof value === "string") {
755
+ // VALUE_TEXT_TAG = 0x07
756
+ hash.update(Buffer.from([0x07]));
757
+ encodeString(hash, value);
758
+ }
759
+ return;
760
+ }
761
+
762
+ // Handle complex values based on their structure
763
+ if (value.unit !== undefined) {
764
+ // VALUE_UNIT_TAG = 0x00
765
+ hash.update(Buffer.from([0x00]));
766
+ } else if (value.bool !== undefined) {
767
+ // VALUE_BOOL_TAG = 0x01
768
+ hash.update(Buffer.from([0x01]));
769
+ encodeBool(hash, value.bool);
770
+ } else if (value.int64 !== undefined) {
771
+ // VALUE_INT64_TAG = 0x02
772
+ hash.update(Buffer.from([0x02]));
773
+ encodeInt64(hash, value.int64);
774
+ } else if (value.numeric !== undefined) {
775
+ // VALUE_NUMERIC_TAG = 0x03
776
+ hash.update(Buffer.from([0x03]));
777
+ encodeString(hash, value.numeric);
778
+ } else if (value.timestamp !== undefined) {
779
+ // VALUE_TIMESTAMP_TAG = 0x04
780
+ hash.update(Buffer.from([0x04]));
781
+ encodeInt64(hash, value.timestamp);
782
+ } else if (value.date !== undefined) {
783
+ // VALUE_DATE_TAG = 0x05
784
+ hash.update(Buffer.from([0x05]));
785
+ encodeInt32(hash, value.date);
786
+ } else if (value.party !== undefined) {
787
+ // VALUE_PARTY_TAG = 0x06
788
+ hash.update(Buffer.from([0x06]));
789
+ encodeString(hash, value.party);
790
+ } else if (value.text !== undefined) {
791
+ // VALUE_TEXT_TAG = 0x07
792
+ hash.update(Buffer.from([0x07]));
793
+ encodeString(hash, value.text);
794
+ } else if (value.contractId !== undefined) {
795
+ // VALUE_CONTRACT_ID_TAG = 0x08
796
+ hash.update(Buffer.from([0x08]));
797
+ encodeHexString(hash, value.contractId);
798
+ } else if (value.optional !== undefined) {
799
+ // VALUE_OPTIONAL_TAG = 0x09
800
+ hash.update(Buffer.from([0x09]));
801
+ if (value.optional.value === undefined) {
802
+ // Optional not present
803
+ hash.update(Buffer.from([0x00]));
804
+ } else {
805
+ // Optional present
806
+ hash.update(Buffer.from([0x01]));
807
+ encodeValue(hash, value.optional.value);
808
+ }
809
+ } else if (value.list !== undefined) {
810
+ // VALUE_LIST_TAG = 0x0A
811
+ hash.update(Buffer.from([0x0a]));
812
+ if (value.list.elements) {
813
+ encodeRepeatedValues(hash, value.list.elements);
814
+ } else {
815
+ encodeInt32(hash, 0);
816
+ }
817
+ } else if (value.textMap !== undefined) {
818
+ // VALUE_TEXT_MAP_TAG = 0x0B
819
+ hash.update(Buffer.from([0x0b]));
820
+ if (value.textMap.entries) {
821
+ encodeRepeatedTextMapEntries(hash, value.textMap.entries);
822
+ } else {
823
+ encodeInt32(hash, 0);
824
+ }
825
+ } else if (value.record !== undefined) {
826
+ // VALUE_RECORD_TAG = 0x0C
827
+ hash.update(Buffer.from([0x0c]));
828
+ if (value.record.recordId) {
829
+ // Optional present
830
+ hash.update(Buffer.from([0x01]));
831
+ encodeIdentifier(hash, value.record.recordId);
832
+ } else {
833
+ // Optional not present
834
+ hash.update(Buffer.from([0x00]));
835
+ }
836
+ if (value.record.fields) {
837
+ encodeRepeatedRecordFields(hash, value.record.fields);
838
+ } else {
839
+ encodeInt32(hash, 0);
840
+ }
841
+ } else if (value.variant !== undefined) {
842
+ // VALUE_VARIANT_TAG = 0x0D
843
+ hash.update(Buffer.from([0x0d]));
844
+ if (value.variant.variantId) {
845
+ // Optional present
846
+ hash.update(Buffer.from([0x01]));
847
+ encodeIdentifier(hash, value.variant.variantId);
848
+ } else {
849
+ // Optional not present
850
+ hash.update(Buffer.from([0x00]));
851
+ }
852
+ encodeString(hash, value.variant.constructor || "");
853
+ if (value.variant.value) {
854
+ encodeValue(hash, value.variant.value);
855
+ }
856
+ } else if (value.enum !== undefined) {
857
+ // VALUE_ENUM_TAG = 0x0E
858
+ hash.update(Buffer.from([0x0e]));
859
+ if (value.enum.enumId) {
860
+ // Optional present
861
+ hash.update(Buffer.from([0x01]));
862
+ encodeIdentifier(hash, value.enum.enumId);
863
+ } else {
864
+ // Optional not present
865
+ hash.update(Buffer.from([0x00]));
866
+ }
867
+ encodeString(hash, value.enum.constructor || "");
868
+ } else if (value.genMap === undefined) {
869
+ // Fallback for unknown value types
870
+ hash.update(Buffer.from([0x00]));
871
+ } else {
872
+ // VALUE_GEN_MAP_TAG = 0x0F
873
+ hash.update(Buffer.from([0x0f]));
874
+ if (value.genMap.entries) {
875
+ encodeRepeatedGenMapEntries(hash, value.genMap.entries);
876
+ } else {
877
+ encodeInt32(hash, 0);
878
+ }
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Encode boolean value
884
+ */
885
+ function encodeBool(hash: crypto.Hash, value: boolean): void {
886
+ hash.update(Buffer.from([value ? 0x01 : 0x00]));
887
+ }
888
+
889
+ /**
890
+ * Encode hex string (for contract IDs)
891
+ */
892
+ function encodeHexString(hash: crypto.Hash, hexStr: string): void {
893
+ const cleanHex = hexStr.startsWith("0x") ? hexStr.slice(2) : hexStr;
894
+ const bytes = Buffer.from(cleanHex, "hex");
895
+ encodeBytes(hash, bytes);
896
+ }
897
+
898
+ /**
899
+ * Encode repeated values
900
+ */
901
+ function encodeRepeatedValues(hash: crypto.Hash, values: any[]): void {
902
+ encodeInt32(hash, values.length);
903
+ for (const value of values) {
904
+ encodeValue(hash, value);
905
+ }
906
+ }
907
+
908
+ /**
909
+ * Encode repeated text map entries
910
+ */
911
+ function encodeRepeatedTextMapEntries(hash: crypto.Hash, entries: any[]): void {
912
+ encodeInt32(hash, entries.length);
913
+ for (const entry of entries) {
914
+ encodeString(hash, entry.key || "");
915
+ encodeValue(hash, entry.value);
916
+ }
917
+ }
918
+
919
+ /**
920
+ * Encode repeated record fields
921
+ */
922
+ function encodeRepeatedRecordFields(hash: crypto.Hash, fields: any[]): void {
923
+ encodeInt32(hash, fields.length);
924
+ for (const field of fields) {
925
+ if (field.label) {
926
+ // Optional present
927
+ hash.update(Buffer.from([0x01]));
928
+ encodeString(hash, field.label);
929
+ } else {
930
+ // Optional not present
931
+ hash.update(Buffer.from([0x00]));
932
+ }
933
+ encodeValue(hash, field.value);
934
+ }
935
+ }
936
+
937
+ /**
938
+ * Encode repeated gen map entries
939
+ */
940
+ function encodeRepeatedGenMapEntries(hash: crypto.Hash, entries: any[]): void {
941
+ encodeInt32(hash, entries.length);
942
+ for (const entry of entries) {
943
+ encodeValue(hash, entry.key);
944
+ encodeValue(hash, entry.value);
945
+ }
946
+ }