@c7-digital/ledger 0.0.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 (98) hide show
  1. package/BUILD.md +125 -0
  2. package/LICENSE +17 -0
  3. package/README.md +97 -0
  4. package/lib/scripts/build.d.ts +9 -0
  5. package/lib/scripts/build.js +366 -0
  6. package/lib/scripts/enhance-types.d.ts +3 -0
  7. package/lib/scripts/enhance-types.js +174 -0
  8. package/lib/scripts/generate-asyncapi-types.d.ts +9 -0
  9. package/lib/scripts/generate-asyncapi-types.js +129 -0
  10. package/lib/scripts/generate-schema-modules.d.ts +3 -0
  11. package/lib/scripts/generate-schema-modules.js +33 -0
  12. package/lib/src/TemplateEmitterMap.d.ts +40 -0
  13. package/lib/src/TemplateEmitterMap.js +57 -0
  14. package/lib/src/client.d.ts +55 -0
  15. package/lib/src/client.js +62 -0
  16. package/lib/src/generated/api.d.ts +7147 -0
  17. package/lib/src/generated/api.js +1 -0
  18. package/lib/src/generated/async-api.d.ts +1665 -0
  19. package/lib/src/generated/async-api.js +1 -0
  20. package/lib/src/generated/asyncapi-schema.d.ts +1 -0
  21. package/lib/src/generated/asyncapi-schema.js +2233 -0
  22. package/lib/src/generated/openapi-schema.d.ts +1 -0
  23. package/lib/src/generated/openapi-schema.js +7321 -0
  24. package/lib/src/generated/sdk-version.d.ts +1 -0
  25. package/lib/src/generated/sdk-version.js +3 -0
  26. package/lib/src/index.d.ts +8 -0
  27. package/lib/src/index.js +8 -0
  28. package/lib/src/ledger.d.ts +188 -0
  29. package/lib/src/ledger.js +849 -0
  30. package/lib/src/ledger.test.d.ts +1 -0
  31. package/lib/src/ledger.test.js +23 -0
  32. package/lib/src/logger.d.ts +41 -0
  33. package/lib/src/logger.js +56 -0
  34. package/lib/src/multistream.d.ts +47 -0
  35. package/lib/src/multistream.js +123 -0
  36. package/lib/src/translate.d.ts +5 -0
  37. package/lib/src/translate.js +30 -0
  38. package/lib/src/types.d.ts +201 -0
  39. package/lib/src/types.js +1 -0
  40. package/lib/src/util.d.ts +3 -0
  41. package/lib/src/util.js +7 -0
  42. package/lib/src/validation.d.ts +27 -0
  43. package/lib/src/validation.js +182 -0
  44. package/lib/src/valueTypes.d.ts +34 -0
  45. package/lib/src/valueTypes.js +76 -0
  46. package/lib/src/websocket.d.ts +69 -0
  47. package/lib/src/websocket.js +125 -0
  48. package/lib/tsconfig.tsbuildinfo +1 -0
  49. package/lib-lite/scripts/build.d.ts +9 -0
  50. package/lib-lite/scripts/build.js +366 -0
  51. package/lib-lite/scripts/enhance-types.d.ts +3 -0
  52. package/lib-lite/scripts/enhance-types.js +174 -0
  53. package/lib-lite/scripts/generate-asyncapi-types.d.ts +9 -0
  54. package/lib-lite/scripts/generate-asyncapi-types.js +129 -0
  55. package/lib-lite/scripts/generate-schema-modules.d.ts +3 -0
  56. package/lib-lite/scripts/generate-schema-modules.js +33 -0
  57. package/lib-lite/src/TemplateEmitterMap.d.ts +40 -0
  58. package/lib-lite/src/TemplateEmitterMap.js +57 -0
  59. package/lib-lite/src/client.d.ts +55 -0
  60. package/lib-lite/src/client.js +62 -0
  61. package/lib-lite/src/generated/api.d.ts +7147 -0
  62. package/lib-lite/src/generated/api.js +1 -0
  63. package/lib-lite/src/generated/async-api.d.ts +1665 -0
  64. package/lib-lite/src/generated/async-api.js +1 -0
  65. package/lib-lite/src/generated/asyncapi-schema.d.ts +1 -0
  66. package/lib-lite/src/generated/asyncapi-schema.js +2 -0
  67. package/lib-lite/src/generated/openapi-schema.d.ts +1 -0
  68. package/lib-lite/src/generated/openapi-schema.js +2 -0
  69. package/lib-lite/src/generated/sdk-version.d.ts +1 -0
  70. package/lib-lite/src/generated/sdk-version.js +3 -0
  71. package/lib-lite/src/index.d.ts +8 -0
  72. package/lib-lite/src/index.js +8 -0
  73. package/lib-lite/src/ledger.d.ts +188 -0
  74. package/lib-lite/src/ledger.js +849 -0
  75. package/lib-lite/src/ledger.test.d.ts +1 -0
  76. package/lib-lite/src/ledger.test.js +23 -0
  77. package/lib-lite/src/logger.d.ts +41 -0
  78. package/lib-lite/src/logger.js +56 -0
  79. package/lib-lite/src/multistream.d.ts +47 -0
  80. package/lib-lite/src/multistream.js +123 -0
  81. package/lib-lite/src/translate.d.ts +5 -0
  82. package/lib-lite/src/translate.js +30 -0
  83. package/lib-lite/src/types.d.ts +201 -0
  84. package/lib-lite/src/types.js +1 -0
  85. package/lib-lite/src/util.d.ts +3 -0
  86. package/lib-lite/src/util.js +7 -0
  87. package/lib-lite/src/validation.d.ts +14 -0
  88. package/lib-lite/src/validation.js +31 -0
  89. package/lib-lite/src/valueTypes.d.ts +34 -0
  90. package/lib-lite/src/valueTypes.js +76 -0
  91. package/lib-lite/src/websocket.d.ts +69 -0
  92. package/lib-lite/src/websocket.js +125 -0
  93. package/lib-lite/tsconfig.temp.tsbuildinfo +1 -0
  94. package/package.json +72 -0
  95. package/scripts/build.ts +456 -0
  96. package/scripts/enhance-types.ts +223 -0
  97. package/scripts/generate-asyncapi-types.ts +158 -0
  98. package/scripts/generate-schema-modules.ts +52 -0
@@ -0,0 +1,849 @@
1
+ /**
2
+ * High-level Daml Ledger abstraction for Canton's OpenAPI v2 JSON API
3
+ *
4
+ * This class provides convenient, Template and Choice-aware methods for common
5
+ * Daml operations. It applies business logic to filter and transform raw API
6
+ * responses into the expected Daml types, ensuring results match the requested
7
+ * Template or Choice specifications.
8
+ *
9
+ * Key features:
10
+ * - Template-typed contract queries and creation
11
+ * - Choice-typed exercise operations
12
+ * - Automatic filtering of irrelevant data (non-active contracts, wrong templates)
13
+ * - Type safety with branded value.proto string types
14
+ * - Compatibility with @daml/types interfaces
15
+ *
16
+ * Use this for most Daml application needs. Use TypedHttpClient directly when
17
+ * you need access to Canton-specific endpoints or full control over API calls.
18
+ */
19
+ import { lookupTemplate, } from "@daml/types";
20
+ import { decodeJwt } from "jose";
21
+ import { EventEmitter } from "eventemitter3";
22
+ import { logger } from "./logger.js";
23
+ import { TypedHttpClient } from "./client.js";
24
+ import { WebSocketClient, isTransaction, } from "./websocket.js";
25
+ import { MultiStreamAdapter } from "./multistream.js";
26
+ import { matchesPartiallyQualified } from "./util.js";
27
+ import * as translate from "./translate.js";
28
+ import { createLedgerString, createPartyIdString, createNameString, createUserIdString, } from "./valueTypes.js";
29
+ // Type guard to check if an event is a CreatedEvent
30
+ function isCreateEvent(event) {
31
+ return "CreatedEvent" in event;
32
+ }
33
+ // This term is so overloaded, lets add a '_' to help differentiate
34
+ function createEvent_(cantonEvent, versionedRegistry) {
35
+ let t;
36
+ let packageVersion;
37
+ // Use custom registry if provided, otherwise fall back to default lookupTemplate
38
+ if (versionedRegistry) {
39
+ const result = versionedRegistry(cantonEvent.templateId);
40
+ if (!result) {
41
+ throw new Error(`Template not found in registry: ${cantonEvent.templateId}`);
42
+ }
43
+ // Check if result is a tuple [Template, version] or just Template
44
+ if (result.type === "template") {
45
+ const { template, version } = result;
46
+ t = template;
47
+ packageVersion = version;
48
+ }
49
+ else {
50
+ throw new Error(`Expected template in registry not: ${JSON.stringify(result)}`);
51
+ }
52
+ }
53
+ else {
54
+ t = lookupTemplate(cantonEvent.templateId);
55
+ }
56
+ return {
57
+ type: "create",
58
+ templateId: cantonEvent.templateId,
59
+ contractId: cantonEvent.contractId,
60
+ payload: t.decoder.runWithException(cantonEvent.createArgument),
61
+ signatories: (cantonEvent.signatories || []),
62
+ observers: (cantonEvent.observers || []),
63
+ key: cantonEvent.contractKey
64
+ ? t.keyDecoder?.runWithException(cantonEvent.contractKey)
65
+ : undefined,
66
+ createdEventBlob: cantonEvent.createdEventBlob || "",
67
+ packageVersion,
68
+ };
69
+ }
70
+ function archiveEvent_(cantonEvent) {
71
+ return {
72
+ type: "archive",
73
+ templateId: cantonEvent.templateId,
74
+ contractId: cantonEvent.contractId,
75
+ witnessParties: (cantonEvent.witnessParties || []),
76
+ offset: cantonEvent.offset,
77
+ };
78
+ }
79
+ // It is possible that we query for a given interview that we are interested in,
80
+ // but the underlying contract has multiple interview implementations that we are NOT
81
+ // interested in, and consequently not registered in our versionedRegistry.
82
+ // In this case we return null.
83
+ function interfaceEvent_(cantonEvent, interfaceView, versionedRegistry) {
84
+ const result = versionedRegistry(interfaceView.interfaceId);
85
+ if (!result) {
86
+ logger.warn(`Interface not found in registry: ${interfaceView.interfaceId}`);
87
+ return null;
88
+ }
89
+ if (result.type === "template") {
90
+ throw new Error(`Expected template in registry not: ${JSON.stringify(result)}`);
91
+ }
92
+ const { interface_, version } = result;
93
+ const i = interface_;
94
+ const decodedInterfaceView = i.decoder.runWithException(interfaceView.viewValue);
95
+ const packageVersion = version;
96
+ return {
97
+ type: "interface",
98
+ templateId: cantonEvent.templateId,
99
+ contractId: cantonEvent.contractId,
100
+ payload: cantonEvent.createArgument,
101
+ signatories: (cantonEvent.signatories || []),
102
+ observers: (cantonEvent.observers || []),
103
+ key: cantonEvent.contractKey,
104
+ createdEventBlob: cantonEvent.createdEventBlob || "",
105
+ interfaceView: decodedInterfaceView,
106
+ packageVersion,
107
+ };
108
+ }
109
+ function templateFilter(templateId, includeCreatedEventBlob) {
110
+ return {
111
+ TemplateFilter: {
112
+ value: {
113
+ templateId: templateId,
114
+ includeCreatedEventBlob,
115
+ },
116
+ },
117
+ };
118
+ }
119
+ function interfaceFilter(interfaceId, includeCreatedEventBlob) {
120
+ return {
121
+ InterfaceFilter: {
122
+ value: {
123
+ interfaceId: interfaceId,
124
+ includeInterfaceView: true,
125
+ includeCreatedEventBlob,
126
+ },
127
+ },
128
+ };
129
+ }
130
+ // Convenience constructors for commands
131
+ export function createCmd(template, payload) {
132
+ return {
133
+ type: 'create',
134
+ template,
135
+ payload,
136
+ };
137
+ }
138
+ export function createAndExerciseCmd(template, payload, choice, argument) {
139
+ return {
140
+ type: 'createAndExercise',
141
+ template,
142
+ payload,
143
+ choice,
144
+ argument,
145
+ };
146
+ }
147
+ export function exerciseCmd(contractId, choice, argument) {
148
+ return {
149
+ type: 'exercise',
150
+ contractId,
151
+ choice,
152
+ argument,
153
+ };
154
+ }
155
+ function convertCommand(command) {
156
+ switch (command.type) {
157
+ case 'create':
158
+ return {
159
+ CreateCommand: {
160
+ templateId: command.template.templateId,
161
+ createArguments: command.payload,
162
+ }
163
+ };
164
+ case 'createAndExercise':
165
+ return {
166
+ CreateAndExerciseCommand: {
167
+ templateId: command.template.templateId,
168
+ createArguments: command.payload,
169
+ choice: createNameString(command.choice.choiceName),
170
+ choiceArgument: command.argument,
171
+ }
172
+ };
173
+ case 'exercise':
174
+ return {
175
+ ExerciseCommand: {
176
+ templateId: command.choice.template().templateId,
177
+ contractId: createLedgerString(command.contractId),
178
+ choice: createNameString(command.choice.choiceName),
179
+ choiceArgument: command.argument,
180
+ }
181
+ };
182
+ default:
183
+ // TypeScript exhaustiveness check
184
+ const _exhaustive = command;
185
+ throw new Error(`Unknown command type: ${JSON.stringify(_exhaustive)}`);
186
+ }
187
+ }
188
+ /**
189
+ * Internal stream implementation for active contracts
190
+ */
191
+ class LedgerStream {
192
+ /**
193
+ * Creates a new stream for active contracts
194
+ *
195
+ * @param template The template to stream contracts for
196
+ * @param parties The parties to stream contracts for
197
+ * @param wsClient The WebSocket client to use
198
+ * @param activeAtOffset Optional offset to start streaming from
199
+ */
200
+ constructor(templateIds, parties, wsClient, startOffset, skipAcs = false, includeCreatedEventBlob = true, versionedRegistry) {
201
+ this.eventEmitter = new EventEmitter();
202
+ this.state_ = "start";
203
+ this.parties = parties;
204
+ this.wsClient = wsClient;
205
+ this.offset = startOffset;
206
+ this.skipAcs = skipAcs;
207
+ this.versionedRegistry = versionedRegistry;
208
+ let cumulative = templateIds.map(templateId => {
209
+ return {
210
+ identifierFilter: templateFilter(templateId, includeCreatedEventBlob),
211
+ };
212
+ });
213
+ this.filtersByParty = this.parties.reduce((acc, party) => {
214
+ acc[party] = { cumulative };
215
+ return acc;
216
+ }, {});
217
+ }
218
+ /**
219
+ * Start the active contracts stream to get existing contracts
220
+ */
221
+ startActiveContractsStream() {
222
+ // Create the request for active contracts
223
+ const request = {
224
+ verbose: false,
225
+ activeAtOffset: this.offset,
226
+ eventFormat: {
227
+ filtersByParty: this.filtersByParty,
228
+ verbose: true,
229
+ },
230
+ };
231
+ logger.debug(`Starting active contracts stream from offset ${this.offset}`);
232
+ // Start streaming and store the stop function
233
+ this.stopClient = this.wsClient.streamActiveContracts(request, response => this.handleActiveContractsResponse(response), error => this.handleError(error), (code, reason) => this.handleActiveContractsClose(code, reason));
234
+ }
235
+ /**
236
+ * Handle responses from the active contracts stream
237
+ */
238
+ handleActiveContractsResponse(response) {
239
+ if (response.status === "error") {
240
+ this.eventEmitter.emit("error", response.error);
241
+ return;
242
+ }
243
+ // Process the successful response
244
+ const contractEntry = response.data.contractEntry;
245
+ // Track the offset for continuing with updates stream later
246
+ // Extract offset from the active contract if available
247
+ if (contractEntry && "JsActiveContract" in contractEntry) {
248
+ const activeContract = contractEntry.JsActiveContract;
249
+ // TODO, do we have to do this?
250
+ if (activeContract.createdEvent.offset > this.offset) {
251
+ logger.warn(`While receiving ACS, updating offset from ${this.offset} to ${activeContract.createdEvent.offset}`);
252
+ this.offset = activeContract.createdEvent.offset;
253
+ }
254
+ this.eventEmitter.emit("create", createEvent_(activeContract.createdEvent, this.versionedRegistry));
255
+ }
256
+ }
257
+ /**
258
+ * Handle close event from active contracts stream
259
+ * This is the signal to transition to the updates stream
260
+ */
261
+ handleActiveContractsClose(code, reason) {
262
+ logger.debug(`Active contracts stream closed: ${code} ${reason}`);
263
+ this.stopClient?.();
264
+ this.stopClient = undefined;
265
+ if (this.state_ === "init") {
266
+ this.state_ = "live";
267
+ this.eventEmitter.emit("state", "live");
268
+ this.startUpdatesStream();
269
+ }
270
+ else {
271
+ const closeEvent = { code, reason };
272
+ this.eventEmitter.emit("close", closeEvent);
273
+ }
274
+ }
275
+ /**
276
+ * Start streaming updates after active contracts have been loaded
277
+ */
278
+ startUpdatesStream() {
279
+ // Create the request for updates stream
280
+ const request = {
281
+ beginExclusive: this.offset,
282
+ verbose: false,
283
+ updateFormat: {
284
+ includeTransactions: {
285
+ eventFormat: {
286
+ filtersByParty: this.filtersByParty,
287
+ verbose: true,
288
+ },
289
+ transactionShape: "TRANSACTION_SHAPE_ACS_DELTA",
290
+ },
291
+ },
292
+ };
293
+ logger.debug(`Starting updates stream from offset ${this.offset}`);
294
+ // Start streaming updates and store the stop function
295
+ this.stopClient = this.wsClient.streamUpdates(request, response => this.handleUpdatesResponse(response), error => this.handleError(error), (code, reason) => this.handleUpdatesClose(code, reason));
296
+ }
297
+ /**
298
+ * Handle responses from the updates stream
299
+ */
300
+ handleUpdatesResponse(response) {
301
+ if (response.status === "error") {
302
+ this.eventEmitter.emit("error", response.error);
303
+ return;
304
+ }
305
+ const update = response.data.update;
306
+ if (isTransaction(update)) {
307
+ const jsTransaction = update.Transaction.value;
308
+ for (const event of jsTransaction.events || []) {
309
+ if ("CreatedEvent" in event) {
310
+ this.offset = Math.max(this.offset, event.CreatedEvent.offset);
311
+ this.eventEmitter.emit("create", createEvent_(event.CreatedEvent, this.versionedRegistry));
312
+ }
313
+ else if ("ArchivedEvent" in event) {
314
+ this.offset = Math.max(this.offset, event.ArchivedEvent.offset);
315
+ this.eventEmitter.emit("archive", archiveEvent_(event.ArchivedEvent));
316
+ }
317
+ else {
318
+ logger.warn(`Unexpected event type in transaction stream: ${JSON.stringify(event)}`);
319
+ }
320
+ }
321
+ }
322
+ else {
323
+ logger.debug(`Ignoring update of type ${JSON.stringify(update)}`);
324
+ }
325
+ }
326
+ /**
327
+ * Handle close event from updates stream
328
+ */
329
+ handleUpdatesClose(code, reason) {
330
+ logger.debug(`Updates stream closed: ${code} ${reason}`);
331
+ this.stopClient?.();
332
+ this.stopClient = undefined;
333
+ // Emit close event
334
+ const closeEvent = { code, reason };
335
+ this.eventEmitter.emit("close", closeEvent);
336
+ // Auto-reconnect on abnormal close (1006) if stream is still active
337
+ if (code === 1006 && this.state_ === "live") {
338
+ logger.log(`WebSocket closed abnormally (1006), reconnecting in 3 seconds...`);
339
+ setTimeout(() => {
340
+ if (this.state_ === "live") {
341
+ // Double-check we're still active
342
+ logger.log(`Attempting to reconnect stream...`);
343
+ this.startUpdatesStream();
344
+ }
345
+ }, 3000);
346
+ }
347
+ }
348
+ // This is a websocket error, not a Canton error
349
+ handleError(error) {
350
+ logger.error("Stream error:", error);
351
+ this.eventEmitter.emit("error", {
352
+ code: "STREAM_ERROR",
353
+ cause: error.message,
354
+ context: {},
355
+ errorCategory: 0,
356
+ });
357
+ }
358
+ // Implementation
359
+ on(type, listener) {
360
+ this.eventEmitter.on(type, listener);
361
+ }
362
+ // Implementation
363
+ off(type, listener) {
364
+ this.eventEmitter.off(type, listener);
365
+ }
366
+ /**
367
+ * Start streaming contracts
368
+ * First loads active contracts, then transitions to streaming updates
369
+ */
370
+ start() {
371
+ if (this.state_ !== "start") {
372
+ logger.warn(`Cannot start stream in state: ${this.state_}`);
373
+ return;
374
+ }
375
+ if (this.skipAcs) {
376
+ this.state_ = "live";
377
+ this.eventEmitter.emit("state", "live");
378
+ this.startUpdatesStream();
379
+ }
380
+ else {
381
+ this.state_ = "init";
382
+ this.eventEmitter.emit("state", "init");
383
+ this.startActiveContractsStream();
384
+ }
385
+ }
386
+ state() {
387
+ return this.state_;
388
+ }
389
+ /**
390
+ * Close the stream and clean up resources
391
+ */
392
+ close() {
393
+ // Set state to stop to prevent transitions
394
+ this.state_ = "stop";
395
+ this.eventEmitter.emit("state", "stop");
396
+ if (this.stopClient) {
397
+ this.stopClient();
398
+ this.stopClient = undefined;
399
+ }
400
+ // Clean up event listeners
401
+ this.eventEmitter.removeAllListeners();
402
+ }
403
+ }
404
+ /**
405
+ * Meant to be a simple replacement for Ledger from @daml/ledger
406
+ */
407
+ export class Ledger {
408
+ constructor(options) {
409
+ this.tokenUserInfo = null;
410
+ this.httpBaseUrl = options.httpBaseUrl;
411
+ const payload = decodeJwt(options.token);
412
+ logger.debug(`Token payload: ${JSON.stringify(payload)}`);
413
+ if (payload.sub === undefined) {
414
+ throw new Error(`Token payload missing 'sub' field`);
415
+ }
416
+ this.tokenUserId = payload.sub;
417
+ this.client = new TypedHttpClient({
418
+ token: options.token,
419
+ baseUrl: this.httpBaseUrl,
420
+ validation: options.validation,
421
+ openApiSchemaPath: options.openApiSchemaPath,
422
+ });
423
+ this.options = options;
424
+ }
425
+ generateCommandId() {
426
+ return createLedgerString(`cmd-${Date.now()}-${Math.random().toString(36).substring(2, 13)}`);
427
+ }
428
+ async resolveOffset(offset) {
429
+ logger.log(`Resolving offset: ${offset}`);
430
+ if (typeof offset === "number") {
431
+ return offset;
432
+ }
433
+ if (offset === "start") {
434
+ return 0; // Ledger begin
435
+ }
436
+ if (offset === "end") {
437
+ const result = await this.getLedgerEnd();
438
+ return result;
439
+ }
440
+ throw new Error(`Invalid offset: ${offset}`);
441
+ }
442
+ async getLedgerEnd() {
443
+ // If there's already a request in flight, return that promise
444
+ if (this.ledgerEndPromise) {
445
+ return this.ledgerEndPromise;
446
+ }
447
+ // Check cache (valid for 1 second to avoid excessive API calls)
448
+ const now = Date.now();
449
+ if (this.ledgerEndCache && now - this.ledgerEndCache.timestamp < 1000) {
450
+ return this.ledgerEndCache.offset;
451
+ }
452
+ // Create new promise for ledger end request
453
+ this.ledgerEndPromise = this.client.getLedgerEnd().then(endResponse => endResponse.offset);
454
+ try {
455
+ const offset = await this.ledgerEndPromise;
456
+ // Cache the result
457
+ this.ledgerEndCache = { offset, timestamp: now };
458
+ return offset;
459
+ }
460
+ finally {
461
+ // Clear the promise so next request can make a new one if needed
462
+ this.ledgerEndPromise = undefined;
463
+ }
464
+ }
465
+ getTokenUserId() {
466
+ return this.tokenUserId;
467
+ }
468
+ async getTokenUserInfo() {
469
+ if (this.tokenUserInfo) {
470
+ return this.tokenUserInfo;
471
+ }
472
+ else {
473
+ this.tokenUserInfo = await this.getUserInfo(this.tokenUserId);
474
+ return this.tokenUserInfo;
475
+ }
476
+ }
477
+ async getTokenActAsParties() {
478
+ const userInfo = await this.getTokenUserInfo();
479
+ return (userInfo?.rights.filter(right => right.type === "canActAs").map(right => right.party) || []);
480
+ }
481
+ /**
482
+ * Query for the Active Contract Set of a specific template.
483
+ * @param template a Template instance as generated by the codegen
484
+ * @param atOffset Whether we want to get historical or just the current
485
+ * state, default.
486
+ * @param includeCreatedEventBlob
487
+ * @param verbose
488
+ * @param readAsParties
489
+ * @returns
490
+ */
491
+ async query(template, atOffset = "end", includeCreatedEventBlob = false, verbose = false, readAsParties) {
492
+ const activeAtOffset = await this.resolveOffset(atOffset);
493
+ const readAsParties_ = readAsParties || (await this.getTokenActAsParties());
494
+ let filtersByParty = readAsParties_.reduce((acc, key) => {
495
+ acc[key] = {
496
+ cumulative: [
497
+ {
498
+ identifierFilter: templateFilter(template.templateId, includeCreatedEventBlob),
499
+ },
500
+ ],
501
+ };
502
+ return acc;
503
+ }, {});
504
+ const queryRequest = {
505
+ verbose: false, // To be deprecated do not rely
506
+ activeAtOffset,
507
+ eventFormat: {
508
+ filtersByParty,
509
+ verbose,
510
+ },
511
+ };
512
+ const response = await this.client.queryActiveContracts(queryRequest);
513
+ // Convert the response to our CreateEvent format
514
+ return response.reduce((acc, item) => {
515
+ // Skip non-active contract entries
516
+ if (!("JsActiveContract" in item.contractEntry)) {
517
+ logger.debug(`Skipping non-active contract entry: ${JSON.stringify(item.contractEntry)}`);
518
+ return acc;
519
+ }
520
+ const contractEntry = item.contractEntry;
521
+ const createEvent = contractEntry.JsActiveContract.createdEvent;
522
+ // Verify we got the correct template
523
+ if (!matchesPartiallyQualified(createEvent.templateId, template.templateId)) {
524
+ logger.warn(`Template ID mismatch: expected ${template.templateId}, got ${createEvent.templateId}`);
525
+ return acc; // Skip contracts with mismatched template IDs
526
+ }
527
+ acc.push(createEvent_(createEvent, this.options.versionedRegistry));
528
+ return acc;
529
+ }, []);
530
+ }
531
+ /**
532
+ * Query for instances of a given interface.
533
+ * @param interface_
534
+ * @param atOffset
535
+ * @param includeCreatedEventBlob
536
+ * @param verbose
537
+ * @param readAsParties
538
+ * @returns
539
+ */
540
+ async queryInterface(interface_, atOffset = "end", includeCreatedEventBlob = false, verbose = false, readAsParties) {
541
+ if (this.options.versionedRegistry === undefined) {
542
+ throw new Error("Versioned registry is required for interface queries.");
543
+ }
544
+ const versionedRegistry = this.options.versionedRegistry;
545
+ const activeAtOffset = await this.resolveOffset(atOffset);
546
+ const readAsParties_ = readAsParties || (await this.getTokenActAsParties());
547
+ let filtersByParty = readAsParties_.reduce((acc, key) => {
548
+ acc[key] = {
549
+ cumulative: [
550
+ {
551
+ identifierFilter: interfaceFilter(interface_.templateId, includeCreatedEventBlob),
552
+ },
553
+ ],
554
+ };
555
+ return acc;
556
+ }, {});
557
+ const queryRequest = {
558
+ verbose: false, // To be deprecated do not rely
559
+ activeAtOffset,
560
+ eventFormat: {
561
+ filtersByParty,
562
+ verbose,
563
+ },
564
+ };
565
+ const response = await this.client.queryActiveContracts(queryRequest);
566
+ // Convert the response to our CreateEvent format
567
+ return response.reduce((acc, item) => {
568
+ // Skip non-active contract entries
569
+ if (!("JsActiveContract" in item.contractEntry)) {
570
+ logger.debug(`Skipping non-active contract entry: ${JSON.stringify(item.contractEntry)}`);
571
+ return acc;
572
+ }
573
+ const contractEntry = item.contractEntry;
574
+ const createEvent = contractEntry.JsActiveContract.createdEvent;
575
+ for (const interfaceView of createEvent.interfaceViews ?? []) {
576
+ if (matchesPartiallyQualified(interfaceView.interfaceId, interface_.templateId)) {
577
+ const interfaceEvent = interfaceEvent_(createEvent, interfaceView, versionedRegistry);
578
+ if (interfaceEvent !== null) {
579
+ acc.push(interfaceEvent);
580
+ }
581
+ }
582
+ else {
583
+ logger.debug(`Ignoring interface ${interfaceView.interfaceId} as it does not match ${interface_.templateId}.`);
584
+ }
585
+ }
586
+ return acc;
587
+ }, []);
588
+ }
589
+ /**
590
+ * Create a template
591
+ * @param template
592
+ * @param payload
593
+ * @param actAs
594
+ * @returns
595
+ */
596
+ async create(template, payload, actAs) {
597
+ const createCommand = {
598
+ templateId: template.templateId,
599
+ createArguments: payload,
600
+ };
601
+ const actAs_ = actAs || (await this.getTokenActAsParties());
602
+ const commands = {
603
+ commands: [{ CreateCommand: createCommand }],
604
+ commandId: this.generateCommandId(),
605
+ actAs: actAs_.map(party => createPartyIdString(party)),
606
+ userId: createUserIdString(this.tokenUserId),
607
+ };
608
+ const request = { commands };
609
+ const response = await this.client.submitAndWaitForTransaction(request);
610
+ const transaction = response.transaction;
611
+ logger.log(`Create Transaction: ${JSON.stringify(transaction)}`);
612
+ const createdEvents = (transaction.events || []).reduce((acc, event) => {
613
+ logger.debug(`Checking event: ${JSON.stringify(event)}`);
614
+ if (isCreateEvent(event)) {
615
+ const matchesTemplate = matchesPartiallyQualified(event.CreatedEvent.templateId, template.templateId);
616
+ if (matchesTemplate) {
617
+ logger.debug(`Event matches template, adding to results`);
618
+ acc.push(createEvent_(event.CreatedEvent, this.options.versionedRegistry));
619
+ }
620
+ else {
621
+ logger.debug(`Event templateId: ${event.CreatedEvent.templateId}, ` +
622
+ `does not match requested templateId: ${template.templateId}`);
623
+ }
624
+ }
625
+ else {
626
+ logger.debug(`Ignoring non create event in transaction: ${JSON.stringify(event)}`);
627
+ }
628
+ return acc;
629
+ }, []);
630
+ if (createdEvents.length === 0) {
631
+ throw new Error(`No created event found for template ${template.templateId}`);
632
+ }
633
+ else if (createdEvents.length > 1) {
634
+ throw new Error(`Multiple create ${JSON.stringify(createdEvents)} ` +
635
+ `events found for template ${template.templateId}`);
636
+ }
637
+ // We've already checked that createdEvents.length > 0, so this is safe
638
+ // Use non-null assertion to tell TypeScript this can't be undefined
639
+ return createdEvents[0];
640
+ }
641
+ /**
642
+ * Exercise a choice of a given contract
643
+ * @param choice
644
+ * @param contractId
645
+ * @param argument
646
+ * @param actAs
647
+ * @returns
648
+ */
649
+ async exercise(choice, contractId, argument, actAs) {
650
+ // Extract actAs from meta or use a default
651
+ const exerciseCommand = {
652
+ templateId: choice.template().templateId,
653
+ contractId: createLedgerString(contractId.toString()),
654
+ choice: createNameString(choice.choiceName),
655
+ choiceArgument: argument,
656
+ };
657
+ const actAs_ = actAs || (await this.getTokenActAsParties());
658
+ const commands = {
659
+ commands: [{ ExerciseCommand: exerciseCommand }],
660
+ commandId: this.generateCommandId(),
661
+ actAs: actAs_.map(party => createPartyIdString(party)),
662
+ // Ends up being not optional
663
+ userId: createUserIdString(this.tokenUserId),
664
+ };
665
+ const request = { commands };
666
+ const response = await this.client.submitAndWaitForTransaction(request);
667
+ const transaction = response.transaction;
668
+ const events = [];
669
+ // TODO: Convert this to the other transaction format to capture the
670
+ // exercise result and the resulting events.
671
+ for (const event of transaction.events || []) {
672
+ logger.log(`Processing exercise resulting event: ${JSON.stringify(event)}`);
673
+ if ("CreatedEvent" in event) {
674
+ // Convert to our Event format
675
+ events.push(createEvent_(event.CreatedEvent, this.options.versionedRegistry));
676
+ }
677
+ else if ("ArchivedEvent" in event) {
678
+ // Convert to our Event format
679
+ events.push(archiveEvent_(event.ArchivedEvent));
680
+ }
681
+ else {
682
+ // Since we are using ACS_DELTA we can ignore ExercisedEvent
683
+ throw new Error(`Unexpected event type: ${JSON.stringify(event)}`);
684
+ }
685
+ }
686
+ return events;
687
+ }
688
+ /**
689
+ * `submit` allows combining multiple commands into a single Canton
690
+ * transaction. Similar to `create` and `exercise` one can use `createCmd`
691
+ * and `exerciseCmd` to create commands, that are then passed to `submit`.
692
+ *
693
+ * @param commands
694
+ * @param actAs Defaults to the actAs parties of the user in the token.
695
+ * @returns Stream of events resulting from the submitted commands.
696
+ */
697
+ async submit(commands, actAs) {
698
+ const jsCommands = commands.map((command) => convertCommand(command));
699
+ // Extract actAs from meta or use a default
700
+ const actAs_ = actAs || (await this.getTokenActAsParties());
701
+ const requestCommands = {
702
+ commands: jsCommands,
703
+ commandId: this.generateCommandId(),
704
+ actAs: actAs_.map(party => createPartyIdString(party)),
705
+ // Ends up being not optional
706
+ userId: createUserIdString(this.tokenUserId),
707
+ };
708
+ const request = { commands: requestCommands };
709
+ const response = await this.client.submitAndWaitForTransaction(request);
710
+ const transaction = response.transaction;
711
+ const events = [];
712
+ for (const event of transaction.events || []) {
713
+ logger.log(`Processing exercise resulting event: ${JSON.stringify(event)}`);
714
+ if ("CreatedEvent" in event) {
715
+ // Convert to our Event format
716
+ events.push(createEvent_(event.CreatedEvent, this.options.versionedRegistry));
717
+ }
718
+ else if ("ArchivedEvent" in event) {
719
+ // Convert to our Event format
720
+ events.push(archiveEvent_(event.ArchivedEvent));
721
+ }
722
+ else {
723
+ // Since we are using ACS_DELTA we can ignore ExercisedEvent
724
+ throw new Error(`Unexpected event type: ${JSON.stringify(event)}`);
725
+ }
726
+ }
727
+ return events;
728
+ }
729
+ initClient() {
730
+ const wsBaseUrl = this.options.wsBaseUrl ||
731
+ this.httpBaseUrl.replace(/^https?:/, this.httpBaseUrl.startsWith("https:") ? "wss:" : "ws:");
732
+ return new WebSocketClient({
733
+ token: this.options.token,
734
+ wsBaseUrl: wsBaseUrl,
735
+ validation: this.options.validation,
736
+ asyncApiSchemaPath: this.options.asyncApiSchemaPath,
737
+ });
738
+ }
739
+ /**
740
+ * Stream functionality using WebSockets
741
+ *
742
+ * @param template The template to stream
743
+ * @param readAsParties Array of parties to stream for, if not specified default to
744
+ * the actAs parties of the user in the token.
745
+ * @param offset Optional offset to start streaming from
746
+ * @param skipAcs Whether to skip archived contracts
747
+ * @param includeCreatedEventBlob Whether to include created event blobs
748
+ * @returns A stream of events for the specified template
749
+ */
750
+ async streamQuery(template, offset = "end", skipAcs = false, includeCreatedEventBlob = false, readAsParties) {
751
+ const activeAtOffset = await this.resolveOffset(offset);
752
+ const parties_ = readAsParties || (await this.getTokenActAsParties());
753
+ return new LedgerStream([template.templateId], parties_, this.initClient(), activeAtOffset, skipAcs, includeCreatedEventBlob, this.options.versionedRegistry);
754
+ }
755
+ /**
756
+ * Create a type-safe MultiStream for working with multiple templates
757
+ *
758
+ * @example
759
+ * ```typescript
760
+ * // Define your template mapping
761
+ * type MyTemplates = {
762
+ * [UserTemplate.templateId]: { contractType: UserContract, keyType: UserKey },
763
+ * [AccountTemplate.templateId]: { contractType: AccountContract, keyType: AccountKey }
764
+ * };
765
+ *
766
+ * // Create a type-safe multi-stream
767
+ * const stream = await ledger.createMultiStream<MyTemplates>(
768
+ * [UserTemplate, AccountTemplate],
769
+ * [party]
770
+ * );
771
+ *
772
+ * // Use template-specific handlers with proper typing
773
+ * stream.onCreate(UserTemplate.templateId, (event) => {
774
+ * // event.payload is typed as UserContract
775
+ * logger.log("User created:", event.payload.username);
776
+ * });
777
+ *
778
+ * stream.onCreate(AccountTemplate.templateId, (event) => {
779
+ * // event.payload is typed as AccountContract
780
+ * logger.log("Account created:", event.payload.accountNumber);
781
+ * });
782
+ *
783
+ * stream.start();
784
+ * ```
785
+ *
786
+ * @param templates Array of templates to stream
787
+ * @param offset Optional offset to start streaming from
788
+ * @param includeCreatedEventBlob Whether to include created event blobs
789
+ * @param readAsParties Array of parties to stream for, if not specified default to
790
+ * the actAs parties of the user in the token.
791
+ * @returns A type-safe MultiStream for working with multiple templates
792
+ */
793
+ async createMultiStream(tm, offset = "end", skipAcs = false, includeCreatedEventBlob = false, readAsParties) {
794
+ const activeAtOffset = await this.resolveOffset(offset);
795
+ const templateIds = Object.keys(tm);
796
+ const parties_ = readAsParties || (await this.getTokenActAsParties());
797
+ const stream = new LedgerStream(templateIds, parties_, this.initClient(), activeAtOffset, skipAcs, includeCreatedEventBlob, this.options.versionedRegistry);
798
+ return new MultiStreamAdapter(stream);
799
+ }
800
+ // User information
801
+ async getUserInfo(userId) {
802
+ const response = await this.client.getUserInfo(userId);
803
+ if (!response.user) {
804
+ logger.warn(`User '${userId}' not found in v2/users/${userId}.`);
805
+ return null;
806
+ }
807
+ else {
808
+ let userRights = await this.client.getUserRights(userId);
809
+ return {
810
+ userId: createUserIdString(response.user.id),
811
+ primaryParty: response.user.primaryParty,
812
+ rights: userRights.rights?.map(translate.userRights) || [],
813
+ };
814
+ }
815
+ }
816
+ // Party management
817
+ async getParties() {
818
+ const response = await this.client.getParties();
819
+ return (response.partyDetails || []).map((party) => ({
820
+ party: party.party, // Keep as Party type for compatibility
821
+ displayName: party.localMetadata?.annotations?.displayName,
822
+ isLocal: party.isLocal || false,
823
+ }));
824
+ }
825
+ async allocateParty(request) {
826
+ const allocateRequest = {
827
+ partyIdHint: request.partyIdHint
828
+ ? createPartyIdString(request.partyIdHint)
829
+ : createPartyIdString(""),
830
+ identityProviderId: "default", // Use default identity provider
831
+ synchronizerId: "",
832
+ userId: "", // Do not assign to user.
833
+ ...(request.displayName && {
834
+ localMetadata: {
835
+ resourceVersion: "",
836
+ annotations: { displayName: request.displayName },
837
+ },
838
+ }),
839
+ };
840
+ const response = await this.client.allocateParty(allocateRequest);
841
+ return {
842
+ partyDetails: {
843
+ party: (response.partyDetails?.party || ""),
844
+ displayName: response.partyDetails?.localMetadata?.annotations?.displayName,
845
+ isLocal: response.partyDetails?.isLocal || false,
846
+ },
847
+ };
848
+ }
849
+ }