@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.
- package/BUILD.md +125 -0
- package/LICENSE +17 -0
- package/README.md +97 -0
- package/lib/scripts/build.d.ts +9 -0
- package/lib/scripts/build.js +366 -0
- package/lib/scripts/enhance-types.d.ts +3 -0
- package/lib/scripts/enhance-types.js +174 -0
- package/lib/scripts/generate-asyncapi-types.d.ts +9 -0
- package/lib/scripts/generate-asyncapi-types.js +129 -0
- package/lib/scripts/generate-schema-modules.d.ts +3 -0
- package/lib/scripts/generate-schema-modules.js +33 -0
- package/lib/src/TemplateEmitterMap.d.ts +40 -0
- package/lib/src/TemplateEmitterMap.js +57 -0
- package/lib/src/client.d.ts +55 -0
- package/lib/src/client.js +62 -0
- package/lib/src/generated/api.d.ts +7147 -0
- package/lib/src/generated/api.js +1 -0
- package/lib/src/generated/async-api.d.ts +1665 -0
- package/lib/src/generated/async-api.js +1 -0
- package/lib/src/generated/asyncapi-schema.d.ts +1 -0
- package/lib/src/generated/asyncapi-schema.js +2233 -0
- package/lib/src/generated/openapi-schema.d.ts +1 -0
- package/lib/src/generated/openapi-schema.js +7321 -0
- package/lib/src/generated/sdk-version.d.ts +1 -0
- package/lib/src/generated/sdk-version.js +3 -0
- package/lib/src/index.d.ts +8 -0
- package/lib/src/index.js +8 -0
- package/lib/src/ledger.d.ts +188 -0
- package/lib/src/ledger.js +849 -0
- package/lib/src/ledger.test.d.ts +1 -0
- package/lib/src/ledger.test.js +23 -0
- package/lib/src/logger.d.ts +41 -0
- package/lib/src/logger.js +56 -0
- package/lib/src/multistream.d.ts +47 -0
- package/lib/src/multistream.js +123 -0
- package/lib/src/translate.d.ts +5 -0
- package/lib/src/translate.js +30 -0
- package/lib/src/types.d.ts +201 -0
- package/lib/src/types.js +1 -0
- package/lib/src/util.d.ts +3 -0
- package/lib/src/util.js +7 -0
- package/lib/src/validation.d.ts +27 -0
- package/lib/src/validation.js +182 -0
- package/lib/src/valueTypes.d.ts +34 -0
- package/lib/src/valueTypes.js +76 -0
- package/lib/src/websocket.d.ts +69 -0
- package/lib/src/websocket.js +125 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib-lite/scripts/build.d.ts +9 -0
- package/lib-lite/scripts/build.js +366 -0
- package/lib-lite/scripts/enhance-types.d.ts +3 -0
- package/lib-lite/scripts/enhance-types.js +174 -0
- package/lib-lite/scripts/generate-asyncapi-types.d.ts +9 -0
- package/lib-lite/scripts/generate-asyncapi-types.js +129 -0
- package/lib-lite/scripts/generate-schema-modules.d.ts +3 -0
- package/lib-lite/scripts/generate-schema-modules.js +33 -0
- package/lib-lite/src/TemplateEmitterMap.d.ts +40 -0
- package/lib-lite/src/TemplateEmitterMap.js +57 -0
- package/lib-lite/src/client.d.ts +55 -0
- package/lib-lite/src/client.js +62 -0
- package/lib-lite/src/generated/api.d.ts +7147 -0
- package/lib-lite/src/generated/api.js +1 -0
- package/lib-lite/src/generated/async-api.d.ts +1665 -0
- package/lib-lite/src/generated/async-api.js +1 -0
- package/lib-lite/src/generated/asyncapi-schema.d.ts +1 -0
- package/lib-lite/src/generated/asyncapi-schema.js +2 -0
- package/lib-lite/src/generated/openapi-schema.d.ts +1 -0
- package/lib-lite/src/generated/openapi-schema.js +2 -0
- package/lib-lite/src/generated/sdk-version.d.ts +1 -0
- package/lib-lite/src/generated/sdk-version.js +3 -0
- package/lib-lite/src/index.d.ts +8 -0
- package/lib-lite/src/index.js +8 -0
- package/lib-lite/src/ledger.d.ts +188 -0
- package/lib-lite/src/ledger.js +849 -0
- package/lib-lite/src/ledger.test.d.ts +1 -0
- package/lib-lite/src/ledger.test.js +23 -0
- package/lib-lite/src/logger.d.ts +41 -0
- package/lib-lite/src/logger.js +56 -0
- package/lib-lite/src/multistream.d.ts +47 -0
- package/lib-lite/src/multistream.js +123 -0
- package/lib-lite/src/translate.d.ts +5 -0
- package/lib-lite/src/translate.js +30 -0
- package/lib-lite/src/types.d.ts +201 -0
- package/lib-lite/src/types.js +1 -0
- package/lib-lite/src/util.d.ts +3 -0
- package/lib-lite/src/util.js +7 -0
- package/lib-lite/src/validation.d.ts +14 -0
- package/lib-lite/src/validation.js +31 -0
- package/lib-lite/src/valueTypes.d.ts +34 -0
- package/lib-lite/src/valueTypes.js +76 -0
- package/lib-lite/src/websocket.d.ts +69 -0
- package/lib-lite/src/websocket.js +125 -0
- package/lib-lite/tsconfig.temp.tsbuildinfo +1 -0
- package/package.json +72 -0
- package/scripts/build.ts +456 -0
- package/scripts/enhance-types.ts +223 -0
- package/scripts/generate-asyncapi-types.ts +158 -0
- 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
|
+
}
|