@distrohelena/canton-explorer 0.1.0

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 (94) hide show
  1. package/.env.example +9 -0
  2. package/LICENSE +184 -0
  3. package/README.md +63 -0
  4. package/bin/canton-explorer.js +68 -0
  5. package/config/nodes.example.json +35 -0
  6. package/dist/public/assets/index-NfJYgVwb.css +1 -0
  7. package/dist/public/assets/index-ybPCCjei.js +1 -0
  8. package/dist/public/cantonexplorer.png +0 -0
  9. package/dist/public/favicon.svg +1 -0
  10. package/dist/public/icons.svg +24 -0
  11. package/dist/public/index.html +14 -0
  12. package/dist/scripts/list-local-parties.d.ts +1 -0
  13. package/dist/scripts/list-local-parties.js +64 -0
  14. package/dist/scripts/list-local-parties.js.map +1 -0
  15. package/dist/src/api/nodes.controller.d.ts +181 -0
  16. package/dist/src/api/nodes.controller.js +873 -0
  17. package/dist/src/api/nodes.controller.js.map +1 -0
  18. package/dist/src/app-bootstrap.d.ts +22 -0
  19. package/dist/src/app-bootstrap.js +52 -0
  20. package/dist/src/app-bootstrap.js.map +1 -0
  21. package/dist/src/app.module.d.ts +2 -0
  22. package/dist/src/app.module.js +48 -0
  23. package/dist/src/app.module.js.map +1 -0
  24. package/dist/src/cache/node-cache.service.d.ts +54 -0
  25. package/dist/src/cache/node-cache.service.js +154 -0
  26. package/dist/src/cache/node-cache.service.js.map +1 -0
  27. package/dist/src/config/node-config.schema.d.ts +100 -0
  28. package/dist/src/config/node-config.schema.js +83 -0
  29. package/dist/src/config/node-config.schema.js.map +1 -0
  30. package/dist/src/config/node-config.service.d.ts +7 -0
  31. package/dist/src/config/node-config.service.js +36 -0
  32. package/dist/src/config/node-config.service.js.map +1 -0
  33. package/dist/src/domain/node-health.d.ts +9 -0
  34. package/dist/src/domain/node-health.js +16 -0
  35. package/dist/src/domain/node-health.js.map +1 -0
  36. package/dist/src/domain/node.types.d.ts +563 -0
  37. package/dist/src/domain/node.types.js +3 -0
  38. package/dist/src/domain/node.types.js.map +1 -0
  39. package/dist/src/grpc/grpc-client.factory.d.ts +297 -0
  40. package/dist/src/grpc/grpc-client.factory.js +67 -0
  41. package/dist/src/grpc/grpc-client.factory.js.map +1 -0
  42. package/dist/src/grpc/grpc-error.util.d.ts +7 -0
  43. package/dist/src/grpc/grpc-error.util.js +31 -0
  44. package/dist/src/grpc/grpc-error.util.js.map +1 -0
  45. package/dist/src/grpc/grpc-operations.service.d.ts +96 -0
  46. package/dist/src/grpc/grpc-operations.service.js +844 -0
  47. package/dist/src/grpc/grpc-operations.service.js.map +1 -0
  48. package/dist/src/grpc/shared-secret-jwt.d.ts +6 -0
  49. package/dist/src/grpc/shared-secret-jwt.js +22 -0
  50. package/dist/src/grpc/shared-secret-jwt.js.map +1 -0
  51. package/dist/src/main.d.ts +1 -0
  52. package/dist/src/main.js +6 -0
  53. package/dist/src/main.js.map +1 -0
  54. package/dist/src/namespaces/namespace-fingerprint.service.d.ts +22 -0
  55. package/dist/src/namespaces/namespace-fingerprint.service.js +125 -0
  56. package/dist/src/namespaces/namespace-fingerprint.service.js.map +1 -0
  57. package/dist/src/orchestrator/node-poller.service.d.ts +26 -0
  58. package/dist/src/orchestrator/node-poller.service.js +224 -0
  59. package/dist/src/orchestrator/node-poller.service.js.map +1 -0
  60. package/dist/src/packages/daml-decoder.types.d.ts +69 -0
  61. package/dist/src/packages/daml-decoder.types.js +3 -0
  62. package/dist/src/packages/daml-decoder.types.js.map +1 -0
  63. package/dist/src/packages/daml-lf-archive-v2.descriptor.pb +0 -0
  64. package/dist/src/packages/daml-lf-archive.descriptor.pb +0 -0
  65. package/dist/src/packages/daml-lf-loader.d.ts +3 -0
  66. package/dist/src/packages/daml-lf-loader.js +51 -0
  67. package/dist/src/packages/daml-lf-loader.js.map +1 -0
  68. package/dist/src/packages/daml-lf-raw.util.d.ts +12 -0
  69. package/dist/src/packages/daml-lf-raw.util.js +117 -0
  70. package/dist/src/packages/daml-lf-raw.util.js.map +1 -0
  71. package/dist/src/packages/daml-lf-value.descriptor.pb +0 -0
  72. package/dist/src/packages/daml-value-decoder.service.d.ts +39 -0
  73. package/dist/src/packages/daml-value-decoder.service.js +463 -0
  74. package/dist/src/packages/daml-value-decoder.service.js.map +1 -0
  75. package/dist/src/packages/package-cache.service.d.ts +53 -0
  76. package/dist/src/packages/package-cache.service.js +288 -0
  77. package/dist/src/packages/package-cache.service.js.map +1 -0
  78. package/dist/src/packages/package-registry.service.d.ts +44 -0
  79. package/dist/src/packages/package-registry.service.js +625 -0
  80. package/dist/src/packages/package-registry.service.js.map +1 -0
  81. package/dist/src/packages/package-sync.service.d.ts +22 -0
  82. package/dist/src/packages/package-sync.service.js +129 -0
  83. package/dist/src/packages/package-sync.service.js.map +1 -0
  84. package/dist/src/packages/pqs-package.service.d.ts +9 -0
  85. package/dist/src/packages/pqs-package.service.js +118 -0
  86. package/dist/src/packages/pqs-package.service.js.map +1 -0
  87. package/dist/src/pqs/pqs-client.factory.d.ts +7 -0
  88. package/dist/src/pqs/pqs-client.factory.js +37 -0
  89. package/dist/src/pqs/pqs-client.factory.js.map +1 -0
  90. package/dist/src/pqs/pqs-summary.service.d.ts +229 -0
  91. package/dist/src/pqs/pqs-summary.service.js +4652 -0
  92. package/dist/src/pqs/pqs-summary.service.js.map +1 -0
  93. package/dist/tsconfig.build.tsbuildinfo +1 -0
  94. package/package.json +97 -0
@@ -0,0 +1,4652 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.PqsSummaryService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const node_config_schema_1 = require("../config/node-config.schema");
18
+ const node_config_service_1 = require("../config/node-config.service");
19
+ const pqs_client_factory_1 = require("./pqs-client.factory");
20
+ const daml_value_decoder_service_1 = require("../packages/daml-value-decoder.service");
21
+ const package_cache_service_1 = require("../packages/package-cache.service");
22
+ const package_registry_service_1 = require("../packages/package-registry.service");
23
+ const grpc_operations_service_1 = require("../grpc/grpc-operations.service");
24
+ const package_sync_service_1 = require("../packages/package-sync.service");
25
+ const SEARCH_GROUP_LIMIT = 25;
26
+ const CANTON_COIN_TOKEN_ID = 'canton-coin';
27
+ const CANTON_COIN_TOKEN_NAME = 'Canton Coin';
28
+ const CANTON_COIN_TRANSFER_TEMPLATE_ID = 'Splice.AmuletTransferInstruction:AmuletTransferInstruction';
29
+ const CANTON_COIN_AMULET_TEMPLATE_ID = 'Splice.Amulet:Amulet';
30
+ const CIP56_HOLDING_TEMPLATE_ID = 'Splice.Api.Token.HoldingV1:Holding';
31
+ const CIP56_TRANSFER_TEMPLATE_ID = 'Splice.Api.Token.TransferInstructionV1:Transfer';
32
+ const CIP112_TEMPLATE_ID_LIKE_PATTERN = '%.CIP112:%';
33
+ const TOKEN_DISCOVERY_TEMPLATE_IDS = [
34
+ CANTON_COIN_TRANSFER_TEMPLATE_ID,
35
+ CANTON_COIN_AMULET_TEMPLATE_ID,
36
+ CIP56_HOLDING_TEMPLATE_ID,
37
+ CIP56_TRANSFER_TEMPLATE_ID,
38
+ ];
39
+ const TOKEN_DISCOVERY_TEMPLATE_PATTERNS = [CIP112_TEMPLATE_ID_LIKE_PATTERN];
40
+ const TOKEN_DISCOVERY_NON_CIP112_TEMPLATE_IDS = [
41
+ CANTON_COIN_TRANSFER_TEMPLATE_ID,
42
+ CANTON_COIN_AMULET_TEMPLATE_ID,
43
+ CIP56_HOLDING_TEMPLATE_ID,
44
+ CIP56_TRANSFER_TEMPLATE_ID,
45
+ ];
46
+ const TOKEN_DISCOVERY_NON_CIP112_TEMPLATE_PATTERNS = [];
47
+ const TOKEN_HOLDER_TEMPLATE_IDS = [
48
+ CANTON_COIN_AMULET_TEMPLATE_ID,
49
+ CIP56_HOLDING_TEMPLATE_ID,
50
+ ];
51
+ const TOKEN_HOLDER_TEMPLATE_PATTERNS = [CIP112_TEMPLATE_ID_LIKE_PATTERN];
52
+ const TOKEN_HOLDER_NON_CIP112_TEMPLATE_IDS = [
53
+ CANTON_COIN_AMULET_TEMPLATE_ID,
54
+ CIP56_HOLDING_TEMPLATE_ID,
55
+ ];
56
+ const TOKEN_HOLDER_NON_CIP112_TEMPLATE_PATTERNS = [];
57
+ const TOKEN_TRANSFER_TEMPLATE_IDS = [
58
+ CANTON_COIN_TRANSFER_TEMPLATE_ID,
59
+ CANTON_COIN_AMULET_TEMPLATE_ID,
60
+ CIP56_TRANSFER_TEMPLATE_ID,
61
+ ];
62
+ const INFERRED_HOLDING_V2_SOURCE = 'pqs_inferred_holding_v2';
63
+ const TOKEN_TRANSFER_CACHE_TTL_MS = 5 * 60 * 1000;
64
+ const TOKEN_TRANSFER_CACHE_LIMIT = 250;
65
+ const ACTIVE_QUERY = `
66
+ select
67
+ current_database() as pqs_database,
68
+ count(*)::int as active_contract_count,
69
+ max(created_at_offset) as latest_offset,
70
+ max(created_effective_at)::text as latest_event_at,
71
+ (
72
+ select count(*)::int
73
+ from participant.lapi_update_meta
74
+ ) as total_update_count
75
+ from active()
76
+ `;
77
+ function compareGlobalMergedUpdates(left, right) {
78
+ const leftRecordTimeMs = Date.parse(left.recordTime ?? '');
79
+ const rightRecordTimeMs = Date.parse(right.recordTime ?? '');
80
+ const leftHasRecordTime = Number.isFinite(leftRecordTimeMs);
81
+ const rightHasRecordTime = Number.isFinite(rightRecordTimeMs);
82
+ if (leftHasRecordTime && rightHasRecordTime && leftRecordTimeMs !== rightRecordTimeMs) {
83
+ return rightRecordTimeMs - leftRecordTimeMs;
84
+ }
85
+ if (leftHasRecordTime !== rightHasRecordTime) {
86
+ return leftHasRecordTime ? -1 : 1;
87
+ }
88
+ if (left.nodeId !== right.nodeId) {
89
+ return left.nodeId.localeCompare(right.nodeId);
90
+ }
91
+ if (left.eventOffset !== right.eventOffset) {
92
+ return right.eventOffset.localeCompare(left.eventOffset);
93
+ }
94
+ return right.updateId.localeCompare(left.updateId);
95
+ }
96
+ function encodeGlobalUpdateCursor(update) {
97
+ return Buffer.from(JSON.stringify(update), 'utf8').toString('base64url');
98
+ }
99
+ function decodeGlobalUpdateCursor(cursor) {
100
+ if (!cursor || !cursor.trim()) {
101
+ return null;
102
+ }
103
+ try {
104
+ const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
105
+ if ((decoded.recordTime === null || typeof decoded.recordTime === 'string') &&
106
+ typeof decoded.nodeId === 'string' &&
107
+ typeof decoded.eventOffset === 'string' &&
108
+ typeof decoded.updateId === 'string') {
109
+ return {
110
+ recordTime: decoded.recordTime ?? null,
111
+ nodeId: decoded.nodeId,
112
+ eventOffset: decoded.eventOffset,
113
+ updateId: decoded.updateId,
114
+ };
115
+ }
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ return null;
121
+ }
122
+ function compareGlobalMergedContracts(left, right) {
123
+ const leftRecordTimeMs = Date.parse(left.recordTime ?? '');
124
+ const rightRecordTimeMs = Date.parse(right.recordTime ?? '');
125
+ const leftHasRecordTime = Number.isFinite(leftRecordTimeMs);
126
+ const rightHasRecordTime = Number.isFinite(rightRecordTimeMs);
127
+ if (leftHasRecordTime && rightHasRecordTime && leftRecordTimeMs !== rightRecordTimeMs) {
128
+ return rightRecordTimeMs - leftRecordTimeMs;
129
+ }
130
+ if (leftHasRecordTime !== rightHasRecordTime) {
131
+ return leftHasRecordTime ? -1 : 1;
132
+ }
133
+ if (left.nodeId !== right.nodeId) {
134
+ return left.nodeId.localeCompare(right.nodeId);
135
+ }
136
+ return right.contractId.localeCompare(left.contractId);
137
+ }
138
+ function encodeGlobalContractCursor(contract) {
139
+ return Buffer.from(JSON.stringify(contract), 'utf8').toString('base64url');
140
+ }
141
+ function decodeGlobalContractCursor(cursor) {
142
+ if (!cursor || !cursor.trim()) {
143
+ return null;
144
+ }
145
+ try {
146
+ const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
147
+ if ((decoded.recordTime === null || typeof decoded.recordTime === 'string') &&
148
+ typeof decoded.nodeId === 'string' &&
149
+ typeof decoded.contractId === 'string') {
150
+ return {
151
+ recordTime: decoded.recordTime ?? null,
152
+ nodeId: decoded.nodeId,
153
+ contractId: decoded.contractId,
154
+ };
155
+ }
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ return null;
161
+ }
162
+ function compareSearchUpdates(left, right) {
163
+ if (left.exact !== right.exact) {
164
+ return left.exact ? -1 : 1;
165
+ }
166
+ const leftRecordTimeMs = Date.parse(left.recordTime ?? '');
167
+ const rightRecordTimeMs = Date.parse(right.recordTime ?? '');
168
+ const leftHasRecordTime = Number.isFinite(leftRecordTimeMs);
169
+ const rightHasRecordTime = Number.isFinite(rightRecordTimeMs);
170
+ if (leftHasRecordTime && rightHasRecordTime && leftRecordTimeMs !== rightRecordTimeMs) {
171
+ return rightRecordTimeMs - leftRecordTimeMs;
172
+ }
173
+ if (leftHasRecordTime !== rightHasRecordTime) {
174
+ return leftHasRecordTime ? -1 : 1;
175
+ }
176
+ if (left.label !== right.label) {
177
+ return left.label.localeCompare(right.label);
178
+ }
179
+ return right.eventOffset.localeCompare(left.eventOffset);
180
+ }
181
+ function compareSearchContracts(left, right) {
182
+ if (left.exact !== right.exact) {
183
+ return left.exact ? -1 : 1;
184
+ }
185
+ const leftRecordTimeMs = Date.parse(left.createdRecordTime ?? '');
186
+ const rightRecordTimeMs = Date.parse(right.createdRecordTime ?? '');
187
+ const leftHasRecordTime = Number.isFinite(leftRecordTimeMs);
188
+ const rightHasRecordTime = Number.isFinite(rightRecordTimeMs);
189
+ if (leftHasRecordTime && rightHasRecordTime && leftRecordTimeMs !== rightRecordTimeMs) {
190
+ return rightRecordTimeMs - leftRecordTimeMs;
191
+ }
192
+ if (leftHasRecordTime !== rightHasRecordTime) {
193
+ return leftHasRecordTime ? -1 : 1;
194
+ }
195
+ if (left.label !== right.label) {
196
+ return left.label.localeCompare(right.label);
197
+ }
198
+ return left.contractId.localeCompare(right.contractId);
199
+ }
200
+ function compareGlobalTokenTransfers(left, right) {
201
+ const leftRecordTimeMs = Date.parse(left.recordTime ?? '');
202
+ const rightRecordTimeMs = Date.parse(right.recordTime ?? '');
203
+ const leftHasRecordTime = Number.isFinite(leftRecordTimeMs);
204
+ const rightHasRecordTime = Number.isFinite(rightRecordTimeMs);
205
+ if (leftHasRecordTime && rightHasRecordTime && leftRecordTimeMs !== rightRecordTimeMs) {
206
+ return rightRecordTimeMs - leftRecordTimeMs;
207
+ }
208
+ if (leftHasRecordTime !== rightHasRecordTime) {
209
+ return leftHasRecordTime ? -1 : 1;
210
+ }
211
+ if (left.updateId !== right.updateId) {
212
+ return right.updateId.localeCompare(left.updateId);
213
+ }
214
+ if (left.tokenId !== right.tokenId) {
215
+ return right.tokenId.localeCompare(left.tokenId);
216
+ }
217
+ if ((left.sender ?? '') !== (right.sender ?? '')) {
218
+ return (right.sender ?? '').localeCompare(left.sender ?? '');
219
+ }
220
+ if ((left.receiver ?? '') !== (right.receiver ?? '')) {
221
+ return (right.receiver ?? '').localeCompare(left.receiver ?? '');
222
+ }
223
+ if ((left.rowId ?? '') !== (right.rowId ?? '')) {
224
+ return (right.rowId ?? '').localeCompare(left.rowId ?? '');
225
+ }
226
+ return (right.amount ?? '').localeCompare(left.amount ?? '');
227
+ }
228
+ function encodeGlobalTokenTransferCursor(transfer) {
229
+ return Buffer.from(JSON.stringify({
230
+ rowId: transfer.rowId,
231
+ recordTime: transfer.recordTime,
232
+ updateId: transfer.updateId,
233
+ tokenId: transfer.tokenId,
234
+ amount: transfer.amount,
235
+ sender: transfer.sender,
236
+ receiver: transfer.receiver,
237
+ }), 'utf8').toString('base64url');
238
+ }
239
+ function decodeGlobalTokenTransferCursor(cursor) {
240
+ if (!cursor || !cursor.trim()) {
241
+ return null;
242
+ }
243
+ try {
244
+ const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
245
+ if ((decoded.rowId === undefined || typeof decoded.rowId === 'string') &&
246
+ (decoded.recordTime === null || typeof decoded.recordTime === 'string') &&
247
+ typeof decoded.updateId === 'string' &&
248
+ typeof decoded.tokenId === 'string' &&
249
+ (decoded.amount === null || typeof decoded.amount === 'string') &&
250
+ (decoded.sender === null || typeof decoded.sender === 'string') &&
251
+ (decoded.receiver === null || typeof decoded.receiver === 'string')) {
252
+ return {
253
+ rowId: decoded.rowId,
254
+ recordTime: decoded.recordTime ?? null,
255
+ updateId: decoded.updateId,
256
+ tokenId: decoded.tokenId,
257
+ amount: decoded.amount ?? null,
258
+ sender: decoded.sender ?? null,
259
+ receiver: decoded.receiver ?? null,
260
+ };
261
+ }
262
+ }
263
+ catch {
264
+ return null;
265
+ }
266
+ return null;
267
+ }
268
+ function compareGlobalTokenHolders(left, right) {
269
+ const leftValue = left.amount === null ? Number.NEGATIVE_INFINITY : Number(left.amount);
270
+ const rightValue = right.amount === null ? Number.NEGATIVE_INFINITY : Number(right.amount);
271
+ const leftValid = Number.isFinite(leftValue);
272
+ const rightValid = Number.isFinite(rightValue);
273
+ if (leftValid && rightValid && leftValue !== rightValue) {
274
+ return rightValue - leftValue;
275
+ }
276
+ if (leftValid !== rightValid) {
277
+ return leftValid ? -1 : 1;
278
+ }
279
+ return left.partyId.localeCompare(right.partyId);
280
+ }
281
+ function compareGlobalTokens(left, right) {
282
+ if (left.name !== right.name) {
283
+ return left.name.localeCompare(right.name);
284
+ }
285
+ return left.tokenId.localeCompare(right.tokenId);
286
+ }
287
+ function encodeGlobalTokenCursor(token) {
288
+ return Buffer.from(JSON.stringify({
289
+ tokenId: token.tokenId,
290
+ name: token.name,
291
+ }), 'utf8').toString('base64url');
292
+ }
293
+ function decodeGlobalTokenCursor(cursor) {
294
+ if (!cursor || !cursor.trim()) {
295
+ return null;
296
+ }
297
+ try {
298
+ const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
299
+ if (typeof decoded.tokenId === 'string' && typeof decoded.name === 'string') {
300
+ return {
301
+ tokenId: decoded.tokenId,
302
+ name: decoded.name,
303
+ };
304
+ }
305
+ }
306
+ catch {
307
+ return null;
308
+ }
309
+ return null;
310
+ }
311
+ function encodeGlobalTokenHolderCursor(holder) {
312
+ return Buffer.from(JSON.stringify({
313
+ partyId: holder.partyId,
314
+ amount: holder.amount,
315
+ }), 'utf8').toString('base64url');
316
+ }
317
+ function decodeGlobalTokenHolderCursor(cursor) {
318
+ if (!cursor || !cursor.trim()) {
319
+ return null;
320
+ }
321
+ try {
322
+ const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
323
+ if (typeof decoded.partyId === 'string' && (decoded.amount === null || typeof decoded.amount === 'string')) {
324
+ return {
325
+ partyId: decoded.partyId,
326
+ amount: decoded.amount ?? null,
327
+ };
328
+ }
329
+ }
330
+ catch {
331
+ return null;
332
+ }
333
+ return null;
334
+ }
335
+ function compareSearchParties(left, right) {
336
+ if (left.exact !== right.exact) {
337
+ return left.exact ? -1 : 1;
338
+ }
339
+ return left.partyId.localeCompare(right.partyId);
340
+ }
341
+ function compareSearchPackageIds(left, right) {
342
+ if (left.exact !== right.exact) {
343
+ return left.exact ? -1 : 1;
344
+ }
345
+ const leftName = left.name ?? '';
346
+ const rightName = right.name ?? '';
347
+ if (leftName !== rightName) {
348
+ return leftName.localeCompare(rightName);
349
+ }
350
+ const leftVersion = left.version ?? '';
351
+ const rightVersion = right.version ?? '';
352
+ if (leftVersion !== rightVersion) {
353
+ return rightVersion.localeCompare(leftVersion);
354
+ }
355
+ return left.packageId.localeCompare(right.packageId);
356
+ }
357
+ function compareSearchPackageNames(left, right) {
358
+ if (left.exact !== right.exact) {
359
+ return left.exact ? -1 : 1;
360
+ }
361
+ return left.name.localeCompare(right.name);
362
+ }
363
+ const PARTICIPANT_FALLBACK_QUERY = `
364
+ select
365
+ current_database() as pqs_database,
366
+ (
367
+ select count(*)::int
368
+ from participant.par_active_contracts
369
+ ) as active_contract_count,
370
+ (
371
+ select max(event_offset)::text
372
+ from participant.lapi_update_meta
373
+ ) as latest_offset,
374
+ (
375
+ select to_char(
376
+ to_timestamp(max(record_time) / 1000000.0) at time zone 'UTC',
377
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
378
+ )
379
+ from participant.lapi_update_meta
380
+ ) as latest_event_at,
381
+ (
382
+ select count(*)::int
383
+ from participant.lapi_update_meta
384
+ ) as total_update_count
385
+ `;
386
+ function activityBucketsQuery(days, bucketMinutes) {
387
+ const normalizedDays = Number.isFinite(days) && days > 0 ? Math.trunc(days) : 30;
388
+ const normalizedBucketMinutes = Number.isFinite(bucketMinutes) && bucketMinutes > 0 ? Math.trunc(bucketMinutes) : 15;
389
+ const bucketSeconds = normalizedBucketMinutes * 60;
390
+ const minRecordTimeMicros = Math.floor((Date.now() - normalizedDays * 24 * 60 * 60 * 1000) * 1000);
391
+ return `
392
+ select
393
+ to_char(
394
+ to_timestamp(floor((record_time / 1000000.0) / ${bucketSeconds}) * ${bucketSeconds}) at time zone 'UTC',
395
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
396
+ ) as bucket_timestamp,
397
+ count(*)::int as activity_value,
398
+ max(event_offset)::text as latest_offset
399
+ from participant.lapi_update_meta
400
+ where record_time >= ${minRecordTimeMicros}
401
+ group by 1
402
+ order by 1 asc
403
+ `;
404
+ }
405
+ function normalizePartyFilters(parties) {
406
+ if (!parties) {
407
+ return [];
408
+ }
409
+ return Array.from(new Set(parties
410
+ .map((party) => party.trim())
411
+ .filter((party) => party.length > 0)));
412
+ }
413
+ function normalizePartyFilterMode(partyMode) {
414
+ return partyMode === 'and' ? 'and' : 'or';
415
+ }
416
+ function normalizeTemplateFilterValue(templateId) {
417
+ return templateId.trim().replace(/^t\|#[^:]+:/, '');
418
+ }
419
+ function normalizeTemplateFilters(templates) {
420
+ if (!templates) {
421
+ return [];
422
+ }
423
+ return Array.from(new Set(templates
424
+ .map((templateId) => normalizeTemplateFilterValue(templateId))
425
+ .filter((templateId) => templateId.length > 0)));
426
+ }
427
+ function normalizeTokenTextFilters(values) {
428
+ if (!values) {
429
+ return [];
430
+ }
431
+ return Array.from(new Set(values
432
+ .map((value) => value.trim().toLowerCase())
433
+ .filter((value) => value.length > 0)));
434
+ }
435
+ function normalizeTokenIssuerFilters(values) {
436
+ if (!values) {
437
+ return [];
438
+ }
439
+ return Array.from(new Set(values
440
+ .map((value) => value.trim())
441
+ .filter((value) => value.length > 0)));
442
+ }
443
+ function buildUpdatePartyExistsCondition(partyId) {
444
+ const createMatch = partyWitnessArrayMatchCondition('create_event.tree_event_witnesses', partyId);
445
+ const consumingMatch = partyWitnessArrayMatchCondition('exercise_event.tree_event_witnesses', partyId);
446
+ const nonConsumingMatch = partyWitnessArrayMatchCondition('exercise_event.tree_event_witnesses', partyId);
447
+ return `(
448
+ exists (
449
+ select 1
450
+ from participant.lapi_events_create create_event
451
+ where create_event.update_id = update_meta.update_id
452
+ and ${createMatch}
453
+ )
454
+ or exists (
455
+ select 1
456
+ from participant.lapi_events_consuming_exercise exercise_event
457
+ where exercise_event.update_id = update_meta.update_id
458
+ and ${consumingMatch}
459
+ )
460
+ or exists (
461
+ select 1
462
+ from participant.lapi_events_non_consuming_exercise exercise_event
463
+ where exercise_event.update_id = update_meta.update_id
464
+ and ${nonConsumingMatch}
465
+ )
466
+ )`;
467
+ }
468
+ function buildNormalizedUpdatePartyExistsCondition(partyId) {
469
+ const partyMatch = partyScalarMatchCondition('party_string.external_string', partyId);
470
+ return `(
471
+ exists (
472
+ select 1
473
+ from participant.lapi_events_activate_contract activate_event
474
+ join participant.lapi_filter_activate_witness witness_filter
475
+ on witness_filter.event_sequential_id = activate_event.event_sequential_id
476
+ join participant.lapi_string_interning party_string
477
+ on party_string.internal_id = witness_filter.party_id
478
+ where activate_event.update_id = update_meta.update_id
479
+ and ${partyMatch}
480
+ )
481
+ or exists (
482
+ select 1
483
+ from participant.lapi_events_deactivate_contract deactivate_event
484
+ join participant.lapi_filter_deactivate_witness witness_filter
485
+ on witness_filter.event_sequential_id = deactivate_event.event_sequential_id
486
+ join participant.lapi_string_interning party_string
487
+ on party_string.internal_id = witness_filter.party_id
488
+ where deactivate_event.update_id = update_meta.update_id
489
+ and ${partyMatch}
490
+ )
491
+ or exists (
492
+ select 1
493
+ from participant.lapi_events_various_witnessed various_event
494
+ join participant.lapi_filter_various_witness witness_filter
495
+ on witness_filter.event_sequential_id = various_event.event_sequential_id
496
+ join participant.lapi_string_interning party_string
497
+ on party_string.internal_id = witness_filter.party_id
498
+ where various_event.update_id = update_meta.update_id
499
+ and ${partyMatch}
500
+ )
501
+ )`;
502
+ }
503
+ function buildUpdateTemplateExistsCondition(templateId) {
504
+ const normalizedTemplateId = normalizeTemplateFilterValue(templateId);
505
+ const quotedTemplateId = `'${escapeSqlLiteral(normalizedTemplateId)}'`;
506
+ return `(
507
+ exists (
508
+ select 1
509
+ from (
510
+ select regexp_replace(coalesce(create_event.template_id::text, ''), '^t\\\\|#[^:]+:', '') as template_id
511
+ from participant.lapi_events_create create_event
512
+ where create_event.update_id = update_meta.update_id
513
+
514
+ union all
515
+
516
+ select regexp_replace(coalesce(exercise_event.template_id::text, ''), '^t\\\\|#[^:]+:', '') as template_id
517
+ from participant.lapi_events_consuming_exercise exercise_event
518
+ where exercise_event.update_id = update_meta.update_id
519
+
520
+ union all
521
+
522
+ select regexp_replace(coalesce(exercise_event.template_id::text, ''), '^t\\\\|#[^:]+:', '') as template_id
523
+ from participant.lapi_events_non_consuming_exercise exercise_event
524
+ where exercise_event.update_id = update_meta.update_id
525
+ ) update_event_templates
526
+ where update_event_templates.template_id = ${quotedTemplateId}
527
+ )
528
+ )`;
529
+ }
530
+ function buildNormalizedUpdateTemplateExistsCondition(templateId) {
531
+ const normalizedTemplateId = normalizeTemplateFilterValue(templateId);
532
+ const quotedTemplateId = `'${escapeSqlLiteral(normalizedTemplateId)}'`;
533
+ return `(
534
+ exists (
535
+ select 1
536
+ from (
537
+ select regexp_replace(coalesce(contract.template_id::text, ''), '^t\\\\|#[^:]+:', '') as template_id
538
+ from participant.lapi_events_activate_contract activate_event
539
+ left join participant.par_contracts contract
540
+ on contract.internal_contract_id = activate_event.internal_contract_id
541
+ where activate_event.update_id = update_meta.update_id
542
+
543
+ union all
544
+
545
+ select regexp_replace(coalesce(contract.template_id::text, ''), '^t\\\\|#[^:]+:', '') as template_id
546
+ from participant.lapi_events_various_witnessed various_event
547
+ left join participant.par_contracts contract
548
+ on contract.internal_contract_id = various_event.internal_contract_id
549
+ where various_event.update_id = update_meta.update_id
550
+ and various_event.event_type = 6
551
+
552
+ union all
553
+
554
+ select regexp_replace(coalesce(template_string.external_string, ''), '^t\\\\|#[^:]+:', '') as template_id
555
+ from participant.lapi_events_deactivate_contract deactivate_event
556
+ left join participant.lapi_string_interning template_string
557
+ on template_string.internal_id = deactivate_event.template_id
558
+ where deactivate_event.update_id = update_meta.update_id
559
+
560
+ union all
561
+
562
+ select regexp_replace(coalesce(template_string.external_string, ''), '^t\\\\|#[^:]+:', '') as template_id
563
+ from participant.lapi_events_various_witnessed various_event
564
+ left join participant.lapi_string_interning template_string
565
+ on template_string.internal_id = various_event.template_id
566
+ where various_event.update_id = update_meta.update_id
567
+ and various_event.event_type in (5, 7)
568
+ ) update_event_templates
569
+ where update_event_templates.template_id = ${quotedTemplateId}
570
+ )
571
+ )`;
572
+ }
573
+ function buildUpdatesFilterClause(parties, templates, partyMode) {
574
+ const partyConditions = normalizePartyFilters(parties).map((party) => buildUpdatePartyExistsCondition(party));
575
+ const templateConditions = normalizeTemplateFilters(templates).map((templateId) => buildUpdateTemplateExistsCondition(templateId));
576
+ const groups = [];
577
+ if (partyConditions.length > 0) {
578
+ const partyJoiner = normalizePartyFilterMode(partyMode) === 'and' ? '\n and ' : '\n or ';
579
+ groups.push(partyConditions.length === 1
580
+ ? partyConditions[0]
581
+ : `(\n ${partyConditions.join(partyJoiner)}\n )`);
582
+ }
583
+ if (templateConditions.length > 0) {
584
+ groups.push(templateConditions.length === 1
585
+ ? templateConditions[0]
586
+ : `(\n ${templateConditions.join('\n or ')}\n )`);
587
+ }
588
+ if (groups.length === 0) {
589
+ return null;
590
+ }
591
+ return groups.length === 1 ? groups[0] : `(\n ${groups.join('\n and ')}\n )`;
592
+ }
593
+ function buildNormalizedUpdatesFilterClause(parties, templates, partyMode) {
594
+ const partyConditions = normalizePartyFilters(parties).map((party) => buildNormalizedUpdatePartyExistsCondition(party));
595
+ const templateConditions = normalizeTemplateFilters(templates).map((templateId) => buildNormalizedUpdateTemplateExistsCondition(templateId));
596
+ const groups = [];
597
+ if (partyConditions.length > 0) {
598
+ const partyJoiner = normalizePartyFilterMode(partyMode) === 'and' ? '\n and ' : '\n or ';
599
+ groups.push(partyConditions.length === 1
600
+ ? partyConditions[0]
601
+ : `(\n ${partyConditions.join(partyJoiner)}\n )`);
602
+ }
603
+ if (templateConditions.length > 0) {
604
+ groups.push(templateConditions.length === 1
605
+ ? templateConditions[0]
606
+ : `(\n ${templateConditions.join('\n or ')}\n )`);
607
+ }
608
+ if (groups.length === 0) {
609
+ return null;
610
+ }
611
+ return groups.length === 1 ? groups[0] : `(\n ${groups.join('\n and ')}\n )`;
612
+ }
613
+ function recentUpdatesQuery(limit, before, after, parties, templates, partyMode, hideSplice) {
614
+ const normalizedBefore = normalizeEventOffsetCursor(before);
615
+ const normalizedAfter = normalizeEventOffsetCursor(after);
616
+ const filterClause = buildUpdatesFilterClause(parties, templates, partyMode);
617
+ const hideSpliceClause = hideSplice ? buildHideSpliceOffsetsClause() : null;
618
+ const queryLimit = limit + 1;
619
+ const afterFilters = [
620
+ normalizedAfter ? `update_meta.event_offset::numeric > ${normalizedAfter}` : null,
621
+ filterClause,
622
+ hideSpliceClause,
623
+ ].filter((value) => Boolean(value));
624
+ const olderFilters = [
625
+ normalizedBefore ? `update_meta.event_offset::numeric < ${normalizedBefore}` : null,
626
+ filterClause,
627
+ hideSpliceClause,
628
+ ].filter((value) => Boolean(value));
629
+ if (normalizedAfter && !normalizedBefore) {
630
+ const whereClause = afterFilters.length > 0 ? `where ${afterFilters.join('\n and ')}` : '';
631
+ return `
632
+ select
633
+ update_meta.update_id::text as update_id,
634
+ update_meta.event_offset::text as event_offset,
635
+ to_char(
636
+ to_timestamp(update_meta.record_time / 1000000.0) at time zone 'UTC',
637
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
638
+ ) as record_time
639
+ from participant.lapi_update_meta update_meta
640
+ ${whereClause}
641
+ order by update_meta.event_offset::numeric asc
642
+ limit ${queryLimit}
643
+ `;
644
+ }
645
+ const whereClause = olderFilters.length > 0 ? `where ${olderFilters.join('\n and ')}` : '';
646
+ return `
647
+ select
648
+ update_meta.update_id::text as update_id,
649
+ update_meta.event_offset::text as event_offset,
650
+ to_char(
651
+ to_timestamp(update_meta.record_time / 1000000.0) at time zone 'UTC',
652
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
653
+ ) as record_time
654
+ from participant.lapi_update_meta update_meta
655
+ ${whereClause}
656
+ order by update_meta.event_offset::numeric desc
657
+ limit ${queryLimit}
658
+ `;
659
+ }
660
+ function searchUpdatesQuery(searchQuery, limit) {
661
+ const quotedQuery = escapeSqlLiteral(searchQuery);
662
+ return `
663
+ select
664
+ update_meta.update_id::text as update_id,
665
+ update_meta.event_offset::text as event_offset,
666
+ to_char(
667
+ to_timestamp(update_meta.record_time / 1000000.0) at time zone 'UTC',
668
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
669
+ ) as record_time
670
+ from participant.lapi_update_meta update_meta
671
+ where update_meta.event_offset::text like '${quotedQuery}%'
672
+ or regexp_replace(update_meta.update_id::text, '^\\\\x', '') like '${quotedQuery}%'
673
+ order by update_meta.record_time desc, update_meta.event_offset::numeric desc
674
+ limit ${limit}
675
+ `;
676
+ }
677
+ function normalizedRecentUpdatesQuery(limit, before, after, parties, templates, partyMode, hideSplice) {
678
+ const normalizedBefore = normalizeEventOffsetCursor(before);
679
+ const normalizedAfter = normalizeEventOffsetCursor(after);
680
+ const filterClause = buildNormalizedUpdatesFilterClause(parties, templates, partyMode);
681
+ const hideSpliceClause = hideSplice ? buildNormalizedHideSpliceOffsetsClause() : null;
682
+ const queryLimit = limit + 1;
683
+ const afterFilters = [
684
+ normalizedAfter ? `update_meta.event_offset::numeric > ${normalizedAfter}` : null,
685
+ filterClause,
686
+ hideSpliceClause,
687
+ ].filter((value) => Boolean(value));
688
+ const olderFilters = [
689
+ normalizedBefore ? `update_meta.event_offset::numeric < ${normalizedBefore}` : null,
690
+ filterClause,
691
+ hideSpliceClause,
692
+ ].filter((value) => Boolean(value));
693
+ if (normalizedAfter && !normalizedBefore) {
694
+ const whereClause = afterFilters.length > 0 ? `where ${afterFilters.join('\n and ')}` : '';
695
+ return `
696
+ select
697
+ encode(update_meta.update_id, 'hex') as update_id,
698
+ update_meta.event_offset::text as event_offset,
699
+ to_char(
700
+ to_timestamp(update_meta.record_time / 1000000.0) at time zone 'UTC',
701
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
702
+ ) as record_time
703
+ from participant.lapi_update_meta update_meta
704
+ ${whereClause}
705
+ order by update_meta.event_offset::numeric asc
706
+ limit ${queryLimit}
707
+ `;
708
+ }
709
+ const whereClause = olderFilters.length > 0 ? `where ${olderFilters.join('\n and ')}` : '';
710
+ return `
711
+ select
712
+ encode(update_meta.update_id, 'hex') as update_id,
713
+ update_meta.event_offset::text as event_offset,
714
+ to_char(
715
+ to_timestamp(update_meta.record_time / 1000000.0) at time zone 'UTC',
716
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
717
+ ) as record_time
718
+ from participant.lapi_update_meta update_meta
719
+ ${whereClause}
720
+ order by update_meta.event_offset::numeric desc
721
+ limit ${queryLimit}
722
+ `;
723
+ }
724
+ function normalizedSearchUpdatesQuery(searchQuery, limit) {
725
+ const quotedQuery = escapeSqlLiteral(searchQuery);
726
+ return `
727
+ select
728
+ encode(update_meta.update_id, 'hex') as update_id,
729
+ update_meta.event_offset::text as event_offset,
730
+ to_char(
731
+ to_timestamp(update_meta.record_time / 1000000.0) at time zone 'UTC',
732
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
733
+ ) as record_time
734
+ from participant.lapi_update_meta update_meta
735
+ where update_meta.event_offset::text like '${quotedQuery}%'
736
+ or encode(update_meta.update_id, 'hex') like '${quotedQuery}%'
737
+ order by update_meta.record_time desc, update_meta.event_offset::numeric desc
738
+ limit ${limit}
739
+ `;
740
+ }
741
+ function buildHideSpliceOffsetsClause() {
742
+ return `
743
+ exists (
744
+ select 1
745
+ from (
746
+ select create_event.template_id::text as template_id
747
+ from participant.lapi_events_create create_event
748
+ where create_event.update_id::text = update_meta.update_id::text
749
+
750
+ union all
751
+
752
+ select exercise_event.template_id::text as template_id
753
+ from participant.lapi_events_consuming_exercise exercise_event
754
+ where exercise_event.update_id::text = update_meta.update_id::text
755
+
756
+ union all
757
+
758
+ select exercise_event.template_id::text as template_id
759
+ from participant.lapi_events_non_consuming_exercise exercise_event
760
+ where exercise_event.update_id::text = update_meta.update_id::text
761
+ ) update_event_templates
762
+ where update_event_templates.template_id is null
763
+ or update_event_templates.template_id not like 'Splice.%'
764
+ )
765
+ `;
766
+ }
767
+ function buildNormalizedHideSpliceOffsetsClause() {
768
+ return `
769
+ exists (
770
+ select 1
771
+ from (
772
+ select regexp_replace(coalesce(contract.template_id::text, ''), '^t\\\\|#[^:]+:', '') as template_id
773
+ from participant.lapi_events_activate_contract activate_event
774
+ left join participant.par_contracts contract
775
+ on contract.internal_contract_id = activate_event.internal_contract_id
776
+ where encode(activate_event.update_id, 'hex') = encode(update_meta.update_id, 'hex')
777
+
778
+ union all
779
+
780
+ select regexp_replace(coalesce(contract.template_id::text, ''), '^t\\\\|#[^:]+:', '') as template_id
781
+ from participant.lapi_events_various_witnessed various_event
782
+ left join participant.par_contracts contract
783
+ on contract.internal_contract_id = various_event.internal_contract_id
784
+ where encode(various_event.update_id, 'hex') = encode(update_meta.update_id, 'hex')
785
+ and various_event.event_type = 6
786
+
787
+ union all
788
+
789
+ select regexp_replace(coalesce(template_string.external_string, ''), '^t\\\\|#[^:]+:', '') as template_id
790
+ from participant.lapi_events_deactivate_contract deactivate_event
791
+ left join participant.lapi_string_interning template_string
792
+ on template_string.internal_id = deactivate_event.template_id
793
+ where encode(deactivate_event.update_id, 'hex') = encode(update_meta.update_id, 'hex')
794
+
795
+ union all
796
+
797
+ select regexp_replace(coalesce(template_string.external_string, ''), '^t\\\\|#[^:]+:', '') as template_id
798
+ from participant.lapi_events_various_witnessed various_event
799
+ left join participant.lapi_string_interning template_string
800
+ on template_string.internal_id = various_event.template_id
801
+ where encode(various_event.update_id, 'hex') = encode(update_meta.update_id, 'hex')
802
+ and various_event.event_type in (5, 7)
803
+ ) update_event_templates
804
+ where update_event_templates.template_id = ''
805
+ or update_event_templates.template_id not like 'Splice.%'
806
+ )
807
+ `;
808
+ }
809
+ function buildQuotedPartyIdentifiers(partyId) {
810
+ const trimmed = partyId.trim();
811
+ if (!trimmed) {
812
+ return [];
813
+ }
814
+ const normalized = trimmed.replace(/^p\|/, '');
815
+ const identifiers = new Set([trimmed, normalized]);
816
+ if (normalized) {
817
+ identifiers.add(`p|${normalized}`);
818
+ }
819
+ return Array.from(identifiers).map((identifier) => `'${escapeSqlLiteral(identifier)}'`);
820
+ }
821
+ function partyWitnessArrayMatchCondition(arrayExpression, partyId) {
822
+ const quotedIdentifiers = buildQuotedPartyIdentifiers(partyId);
823
+ if (quotedIdentifiers.length === 0) {
824
+ return 'false';
825
+ }
826
+ return `array[${quotedIdentifiers.join(', ')}]::text[] && ${arrayExpression}::text[]`;
827
+ }
828
+ function partyScalarMatchCondition(columnExpression, partyId) {
829
+ const quotedIdentifiers = buildQuotedPartyIdentifiers(partyId);
830
+ if (quotedIdentifiers.length === 0) {
831
+ return 'false';
832
+ }
833
+ return `${columnExpression} in (${quotedIdentifiers.join(', ')})`;
834
+ }
835
+ function partyRecentUpdatesQuery(partyId, limit) {
836
+ const witnessMatch = partyWitnessArrayMatchCondition('create_event.tree_event_witnesses', partyId);
837
+ const consumingWitnessMatch = partyWitnessArrayMatchCondition('exercise_event.tree_event_witnesses', partyId);
838
+ const nonConsumingWitnessMatch = partyWitnessArrayMatchCondition('exercise_event.tree_event_witnesses', partyId);
839
+ return `
840
+ select
841
+ update_meta.update_id::text as update_id,
842
+ update_meta.event_offset::text as event_offset,
843
+ to_char(
844
+ to_timestamp(update_meta.record_time / 1000000.0) at time zone 'UTC',
845
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
846
+ ) as record_time
847
+ from participant.lapi_update_meta update_meta
848
+ where update_meta.update_id::text in (
849
+ select create_event.update_id::text
850
+ from participant.lapi_events_create create_event
851
+ where ${witnessMatch}
852
+
853
+ union
854
+
855
+ select exercise_event.update_id::text
856
+ from participant.lapi_events_consuming_exercise exercise_event
857
+ where ${consumingWitnessMatch}
858
+
859
+ union
860
+
861
+ select exercise_event.update_id::text
862
+ from participant.lapi_events_non_consuming_exercise exercise_event
863
+ where ${nonConsumingWitnessMatch}
864
+ )
865
+ order by update_meta.record_time desc
866
+ limit ${limit}
867
+ `;
868
+ }
869
+ function normalizedPartyRecentUpdatesQuery(partyId, limit) {
870
+ const partyMatch = partyScalarMatchCondition('party_string.external_string', partyId);
871
+ return `
872
+ with matching_updates as (
873
+ select distinct activate_event.update_id
874
+ from participant.lapi_events_activate_contract activate_event
875
+ join participant.lapi_filter_activate_witness witness_filter
876
+ on witness_filter.event_sequential_id = activate_event.event_sequential_id
877
+ join participant.lapi_string_interning party_string
878
+ on party_string.internal_id = witness_filter.party_id
879
+ where ${partyMatch}
880
+
881
+ union
882
+
883
+ select distinct deactivate_event.update_id
884
+ from participant.lapi_events_deactivate_contract deactivate_event
885
+ join participant.lapi_filter_deactivate_witness witness_filter
886
+ on witness_filter.event_sequential_id = deactivate_event.event_sequential_id
887
+ join participant.lapi_string_interning party_string
888
+ on party_string.internal_id = witness_filter.party_id
889
+ where ${partyMatch}
890
+
891
+ union
892
+
893
+ select distinct various_event.update_id
894
+ from participant.lapi_events_various_witnessed various_event
895
+ join participant.lapi_filter_various_witness witness_filter
896
+ on witness_filter.event_sequential_id = various_event.event_sequential_id
897
+ join participant.lapi_string_interning party_string
898
+ on party_string.internal_id = witness_filter.party_id
899
+ where ${partyMatch}
900
+ )
901
+ select
902
+ encode(update_meta.update_id, 'hex') as update_id,
903
+ update_meta.event_offset::text as event_offset,
904
+ to_char(
905
+ to_timestamp(update_meta.record_time / 1000000.0) at time zone 'UTC',
906
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
907
+ ) as record_time
908
+ from participant.lapi_update_meta update_meta
909
+ join matching_updates
910
+ on matching_updates.update_id = update_meta.update_id
911
+ order by update_meta.record_time desc
912
+ limit ${limit}
913
+ `;
914
+ }
915
+ function recentUpdatePartiesQuery(updateIds) {
916
+ const quotedIds = updateIds.map((updateId) => `'${escapeSqlLiteral(updateId)}'`).join(', ');
917
+ return `
918
+ select
919
+ update_id::text as update_id,
920
+ array_agg(distinct party order by party) as parties
921
+ from (
922
+ select
923
+ update_id,
924
+ unnest(tree_event_witnesses) as party
925
+ from participant.lapi_events_create
926
+ where update_id in (${quotedIds})
927
+
928
+ union
929
+
930
+ select
931
+ update_id,
932
+ unnest(tree_event_witnesses) as party
933
+ from participant.lapi_events_consuming_exercise
934
+ where update_id in (${quotedIds})
935
+
936
+ union
937
+
938
+ select
939
+ update_id,
940
+ unnest(tree_event_witnesses) as party
941
+ from participant.lapi_events_non_consuming_exercise
942
+ where update_id in (${quotedIds})
943
+ ) update_parties
944
+ group by update_id
945
+ `;
946
+ }
947
+ function normalizedRecentUpdatePartiesQuery(updateIds) {
948
+ const quotedIds = updateIds
949
+ .map((updateId) => `'${escapeSqlLiteral(normalizeByteaHex(updateId))}'`)
950
+ .join(', ');
951
+ return `
952
+ select
953
+ update_id,
954
+ array_agg(distinct party order by party) as parties
955
+ from (
956
+ select
957
+ encode(activate_event.update_id, 'hex') as update_id,
958
+ party_string.external_string as party
959
+ from participant.lapi_events_activate_contract activate_event
960
+ join participant.lapi_filter_activate_witness witness_filter
961
+ on witness_filter.event_sequential_id = activate_event.event_sequential_id
962
+ join participant.lapi_string_interning party_string
963
+ on party_string.internal_id = witness_filter.party_id
964
+ where encode(activate_event.update_id, 'hex') in (${quotedIds})
965
+
966
+ union
967
+
968
+ select
969
+ encode(deactivate_event.update_id, 'hex') as update_id,
970
+ party_string.external_string as party
971
+ from participant.lapi_events_deactivate_contract deactivate_event
972
+ join participant.lapi_filter_deactivate_witness witness_filter
973
+ on witness_filter.event_sequential_id = deactivate_event.event_sequential_id
974
+ join participant.lapi_string_interning party_string
975
+ on party_string.internal_id = witness_filter.party_id
976
+ where encode(deactivate_event.update_id, 'hex') in (${quotedIds})
977
+
978
+ union
979
+
980
+ select
981
+ encode(various_event.update_id, 'hex') as update_id,
982
+ party_string.external_string as party
983
+ from participant.lapi_events_various_witnessed various_event
984
+ join participant.lapi_filter_various_witness witness_filter
985
+ on witness_filter.event_sequential_id = various_event.event_sequential_id
986
+ join participant.lapi_string_interning party_string
987
+ on party_string.internal_id = witness_filter.party_id
988
+ where encode(various_event.update_id, 'hex') in (${quotedIds})
989
+ ) update_parties
990
+ group by update_id
991
+ `;
992
+ }
993
+ function activePartiesQuery() {
994
+ return `
995
+ select
996
+ array_agg(distinct party order by party) as parties
997
+ from (
998
+ select unnest(tree_event_witnesses) as party
999
+ from participant.lapi_events_create
1000
+
1001
+ union
1002
+
1003
+ select unnest(tree_event_witnesses) as party
1004
+ from participant.lapi_events_consuming_exercise
1005
+
1006
+ union
1007
+
1008
+ select unnest(tree_event_witnesses) as party
1009
+ from participant.lapi_events_non_consuming_exercise
1010
+ ) observed_parties
1011
+ `;
1012
+ }
1013
+ function normalizedActivePartiesQuery() {
1014
+ return `
1015
+ select
1016
+ array_agg(distinct party order by party) as parties
1017
+ from (
1018
+ select party_string.external_string as party
1019
+ from participant.lapi_filter_activate_witness witness_filter
1020
+ join participant.lapi_string_interning party_string
1021
+ on party_string.internal_id = witness_filter.party_id
1022
+
1023
+ union
1024
+
1025
+ select party_string.external_string as party
1026
+ from participant.lapi_filter_deactivate_witness witness_filter
1027
+ join participant.lapi_string_interning party_string
1028
+ on party_string.internal_id = witness_filter.party_id
1029
+
1030
+ union
1031
+
1032
+ select party_string.external_string as party
1033
+ from participant.lapi_filter_various_witness witness_filter
1034
+ join participant.lapi_string_interning party_string
1035
+ on party_string.internal_id = witness_filter.party_id
1036
+ ) observed_parties
1037
+ `;
1038
+ }
1039
+ function partyRecentContractsQuery(partyId, limit, before, after) {
1040
+ const witnessMatch = partyWitnessArrayMatchCondition('create_event.tree_event_witnesses', partyId);
1041
+ const normalizedBefore = normalizeEventOffsetCursor(before);
1042
+ const normalizedAfter = normalizeEventOffsetCursor(after);
1043
+ const queryLimit = limit + 1;
1044
+ const afterClause = normalizedAfter ? `and create_event.event_offset::numeric > ${normalizedAfter}` : '';
1045
+ const beforeClause = normalizedBefore ? `and create_event.event_offset::numeric < ${normalizedBefore}` : '';
1046
+ if (normalizedAfter && !normalizedBefore) {
1047
+ return `
1048
+ select
1049
+ create_event.contract_id::text as contract_id,
1050
+ create_event.template_id::text as template_id,
1051
+ null::text as package_id,
1052
+ to_char(
1053
+ to_timestamp(update_meta.record_time / 1000000.0) at time zone 'UTC',
1054
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1055
+ ) as record_time,
1056
+ create_event.event_offset::text as created_event_offset
1057
+ from participant.lapi_events_create create_event
1058
+ join participant.lapi_update_meta update_meta
1059
+ on update_meta.update_id = create_event.update_id
1060
+ where ${witnessMatch}
1061
+ ${afterClause}
1062
+ order by create_event.event_offset::numeric asc
1063
+ limit ${queryLimit}
1064
+ `;
1065
+ }
1066
+ return `
1067
+ select
1068
+ create_event.contract_id::text as contract_id,
1069
+ create_event.template_id::text as template_id,
1070
+ null::text as package_id,
1071
+ to_char(
1072
+ to_timestamp(update_meta.record_time / 1000000.0) at time zone 'UTC',
1073
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1074
+ ) as record_time
1075
+ ,
1076
+ create_event.event_offset::text as created_event_offset
1077
+ from participant.lapi_events_create create_event
1078
+ join participant.lapi_update_meta update_meta
1079
+ on update_meta.update_id = create_event.update_id
1080
+ where ${witnessMatch}
1081
+ ${beforeClause}
1082
+ order by create_event.event_offset::numeric desc
1083
+ limit ${queryLimit}
1084
+ `;
1085
+ }
1086
+ function normalizedPartyRecentContractsQuery(partyId, limit, before, after) {
1087
+ const partyMatch = partyScalarMatchCondition('party_string.external_string', partyId);
1088
+ const normalizedBefore = normalizeEventOffsetCursor(before);
1089
+ const normalizedAfter = normalizeEventOffsetCursor(after);
1090
+ const queryLimit = limit + 1;
1091
+ const afterFilter = normalizedAfter && !normalizedBefore
1092
+ ? `where created_event_offset::numeric > ${normalizedAfter}`
1093
+ : normalizedBefore
1094
+ ? `where created_event_offset::numeric < ${normalizedBefore}`
1095
+ : '';
1096
+ const sortDirection = normalizedAfter && !normalizedBefore ? 'asc' : 'desc';
1097
+ return `
1098
+ select
1099
+ contract_id,
1100
+ template_id,
1101
+ package_id,
1102
+ record_time,
1103
+ created_event_offset
1104
+ from (
1105
+ select
1106
+ encode(contract.contract_id, 'hex') as contract_id,
1107
+ contract.template_id::text as template_id,
1108
+ package_string.external_string as package_id,
1109
+ to_char(
1110
+ to_timestamp(activate_event.record_time / 1000000.0) at time zone 'UTC',
1111
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1112
+ ) as record_time,
1113
+ activate_event.event_sequential_id as sort_event_sequential_id,
1114
+ activate_event.event_offset::text as created_event_offset
1115
+ from participant.lapi_events_activate_contract activate_event
1116
+ join participant.lapi_filter_activate_witness witness_filter
1117
+ on witness_filter.event_sequential_id = activate_event.event_sequential_id
1118
+ join participant.lapi_string_interning party_string
1119
+ on party_string.internal_id = witness_filter.party_id
1120
+ left join participant.par_contracts contract
1121
+ on contract.internal_contract_id = activate_event.internal_contract_id
1122
+ left join participant.lapi_string_interning package_string
1123
+ on package_string.internal_id = activate_event.representative_package_id
1124
+ where ${partyMatch}
1125
+
1126
+ union all
1127
+
1128
+ select
1129
+ encode(contract.contract_id, 'hex') as contract_id,
1130
+ contract.template_id::text as template_id,
1131
+ package_string.external_string as package_id,
1132
+ to_char(
1133
+ to_timestamp(various_event.record_time / 1000000.0) at time zone 'UTC',
1134
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1135
+ ) as record_time,
1136
+ various_event.event_sequential_id as sort_event_sequential_id,
1137
+ various_event.event_offset::text as created_event_offset
1138
+ from participant.lapi_events_various_witnessed various_event
1139
+ join participant.lapi_filter_various_witness witness_filter
1140
+ on witness_filter.event_sequential_id = various_event.event_sequential_id
1141
+ join participant.lapi_string_interning party_string
1142
+ on party_string.internal_id = witness_filter.party_id
1143
+ left join participant.par_contracts contract
1144
+ on contract.internal_contract_id = various_event.internal_contract_id
1145
+ left join participant.lapi_string_interning package_string
1146
+ on package_string.internal_id = various_event.package_id
1147
+ where ${partyMatch}
1148
+ and various_event.event_type = 6
1149
+ ) recent_contracts
1150
+ ${afterFilter}
1151
+ order by created_event_offset::numeric ${sortDirection}
1152
+ limit ${queryLimit}
1153
+ `;
1154
+ }
1155
+ function singleUpdateQuery(eventOffset) {
1156
+ const quotedOffset = `'${escapeSqlLiteral(eventOffset)}'`;
1157
+ return `
1158
+ select
1159
+ update_meta.update_id::text as update_id,
1160
+ update_meta.event_offset::text as event_offset,
1161
+ to_char(
1162
+ to_timestamp(update_meta.record_time / 1000000.0) at time zone 'UTC',
1163
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1164
+ ) as record_time_iso,
1165
+ to_jsonb(update_meta) as meta
1166
+ from participant.lapi_update_meta update_meta
1167
+ where update_meta.event_offset::text = ${quotedOffset}
1168
+ order by update_meta.record_time desc
1169
+ limit 1
1170
+ `;
1171
+ }
1172
+ function updateEventsQuery(updateId) {
1173
+ const quotedId = `'${escapeSqlLiteral(updateId)}'`;
1174
+ return `
1175
+ select
1176
+ event_kind,
1177
+ event_id,
1178
+ contract_id,
1179
+ template_id,
1180
+ package_id,
1181
+ choice,
1182
+ witnesses,
1183
+ contract_instance,
1184
+ exercise_argument,
1185
+ exercise_result,
1186
+ raw
1187
+ from (
1188
+ select
1189
+ 'create'::text as event_kind,
1190
+ create_event.event_id::text as event_id,
1191
+ create_event.contract_id::text as contract_id,
1192
+ create_event.template_id::text as template_id,
1193
+ null::text as package_id,
1194
+ null::text as choice,
1195
+ create_event.tree_event_witnesses as witnesses,
1196
+ null::bytea as contract_instance,
1197
+ null::bytea as exercise_argument,
1198
+ null::bytea as exercise_result,
1199
+ to_jsonb(create_event) as raw
1200
+ from participant.lapi_events_create create_event
1201
+ where create_event.update_id::text = ${quotedId}
1202
+
1203
+ union all
1204
+
1205
+ select
1206
+ 'consuming_exercise'::text as event_kind,
1207
+ exercise_event.event_id::text as event_id,
1208
+ exercise_event.contract_id::text as contract_id,
1209
+ exercise_event.template_id::text as template_id,
1210
+ null::text as package_id,
1211
+ exercise_event.choice::text as choice,
1212
+ exercise_event.tree_event_witnesses as witnesses,
1213
+ null::bytea as contract_instance,
1214
+ null::bytea as exercise_argument,
1215
+ null::bytea as exercise_result,
1216
+ to_jsonb(exercise_event) as raw
1217
+ from participant.lapi_events_consuming_exercise exercise_event
1218
+ where exercise_event.update_id::text = ${quotedId}
1219
+
1220
+ union all
1221
+
1222
+ select
1223
+ 'non_consuming_exercise'::text as event_kind,
1224
+ exercise_event.event_id::text as event_id,
1225
+ exercise_event.contract_id::text as contract_id,
1226
+ exercise_event.template_id::text as template_id,
1227
+ null::text as package_id,
1228
+ exercise_event.choice::text as choice,
1229
+ exercise_event.tree_event_witnesses as witnesses,
1230
+ null::bytea as contract_instance,
1231
+ null::bytea as exercise_argument,
1232
+ null::bytea as exercise_result,
1233
+ to_jsonb(exercise_event) as raw
1234
+ from participant.lapi_events_non_consuming_exercise exercise_event
1235
+ where exercise_event.update_id::text = ${quotedId}
1236
+ ) update_events
1237
+ order by event_id asc, event_kind asc, contract_id asc, template_id asc
1238
+ `;
1239
+ }
1240
+ function normalizedUpdateEventsQuery(updateId) {
1241
+ const quotedId = `'${escapeSqlLiteral(normalizeByteaHex(updateId))}'`;
1242
+ return `
1243
+ with activate_witnesses as (
1244
+ select
1245
+ witness_filter.event_sequential_id,
1246
+ array_agg(distinct party_string.external_string order by party_string.external_string) as witnesses
1247
+ from participant.lapi_filter_activate_witness witness_filter
1248
+ join participant.lapi_string_interning party_string
1249
+ on party_string.internal_id = witness_filter.party_id
1250
+ group by witness_filter.event_sequential_id
1251
+ ),
1252
+ deactivate_witnesses as (
1253
+ select
1254
+ witness_filter.event_sequential_id,
1255
+ array_agg(distinct party_string.external_string order by party_string.external_string) as witnesses
1256
+ from participant.lapi_filter_deactivate_witness witness_filter
1257
+ join participant.lapi_string_interning party_string
1258
+ on party_string.internal_id = witness_filter.party_id
1259
+ group by witness_filter.event_sequential_id
1260
+ ),
1261
+ various_witnesses as (
1262
+ select
1263
+ witness_filter.event_sequential_id,
1264
+ array_agg(distinct party_string.external_string order by party_string.external_string) as witnesses
1265
+ from participant.lapi_filter_various_witness witness_filter
1266
+ join participant.lapi_string_interning party_string
1267
+ on party_string.internal_id = witness_filter.party_id
1268
+ group by witness_filter.event_sequential_id
1269
+ )
1270
+ select
1271
+ event_kind,
1272
+ event_id,
1273
+ contract_id,
1274
+ template_id,
1275
+ package_id,
1276
+ choice,
1277
+ witnesses,
1278
+ contract_instance,
1279
+ exercise_argument,
1280
+ exercise_result,
1281
+ raw
1282
+ from (
1283
+ select
1284
+ 'create'::text as event_kind,
1285
+ '#0:' || activate_event.node_id::text as event_id,
1286
+ encode(contract.contract_id, 'hex') as contract_id,
1287
+ contract.template_id::text as template_id,
1288
+ package_string.external_string as package_id,
1289
+ null::text as choice,
1290
+ coalesce(activate_witnesses.witnesses, array[]::text[]) as witnesses,
1291
+ contract.instance as contract_instance,
1292
+ null::bytea as exercise_argument,
1293
+ null::bytea as exercise_result,
1294
+ jsonb_build_object(
1295
+ 'source_table', 'participant.lapi_events_activate_contract',
1296
+ 'update_id', encode(activate_event.update_id, 'hex'),
1297
+ 'event_offset', activate_event.event_offset,
1298
+ 'event_type', activate_event.event_type,
1299
+ 'event_sequential_id', activate_event.event_sequential_id,
1300
+ 'node_id', activate_event.node_id,
1301
+ 'contract_id', encode(contract.contract_id, 'hex'),
1302
+ 'template_id', contract.template_id,
1303
+ 'package_id', package_string.external_string
1304
+ ) as raw,
1305
+ activate_event.event_sequential_id as sort_event_sequential_id
1306
+ from participant.lapi_events_activate_contract activate_event
1307
+ left join participant.par_contracts contract
1308
+ on contract.internal_contract_id = activate_event.internal_contract_id
1309
+ left join participant.lapi_string_interning package_string
1310
+ on package_string.internal_id = activate_event.representative_package_id
1311
+ left join activate_witnesses
1312
+ on activate_witnesses.event_sequential_id = activate_event.event_sequential_id
1313
+ where encode(activate_event.update_id, 'hex') = ${quotedId}
1314
+
1315
+ union all
1316
+
1317
+ select
1318
+ 'create'::text as event_kind,
1319
+ '#0:' || various_event.node_id::text as event_id,
1320
+ encode(contract.contract_id, 'hex') as contract_id,
1321
+ contract.template_id::text as template_id,
1322
+ package_string.external_string as package_id,
1323
+ null::text as choice,
1324
+ coalesce(various_witnesses.witnesses, array[]::text[]) as witnesses,
1325
+ contract.instance as contract_instance,
1326
+ null::bytea as exercise_argument,
1327
+ null::bytea as exercise_result,
1328
+ jsonb_build_object(
1329
+ 'source_table', 'participant.lapi_events_various_witnessed',
1330
+ 'update_id', encode(various_event.update_id, 'hex'),
1331
+ 'event_offset', various_event.event_offset,
1332
+ 'event_type', various_event.event_type,
1333
+ 'event_sequential_id', various_event.event_sequential_id,
1334
+ 'node_id', various_event.node_id,
1335
+ 'contract_id', encode(contract.contract_id, 'hex'),
1336
+ 'template_id', contract.template_id,
1337
+ 'package_id', package_string.external_string
1338
+ ) as raw,
1339
+ various_event.event_sequential_id as sort_event_sequential_id
1340
+ from participant.lapi_events_various_witnessed various_event
1341
+ left join participant.par_contracts contract
1342
+ on contract.internal_contract_id = various_event.internal_contract_id
1343
+ left join participant.lapi_string_interning package_string
1344
+ on package_string.internal_id = various_event.package_id
1345
+ left join various_witnesses
1346
+ on various_witnesses.event_sequential_id = various_event.event_sequential_id
1347
+ where encode(various_event.update_id, 'hex') = ${quotedId}
1348
+ and various_event.event_type = 6
1349
+
1350
+ union all
1351
+
1352
+ select
1353
+ 'consuming_exercise'::text as event_kind,
1354
+ '#0:' || deactivate_event.node_id::text as event_id,
1355
+ encode(deactivate_event.contract_id, 'hex') as contract_id,
1356
+ template_string.external_string as template_id,
1357
+ package_string.external_string as package_id,
1358
+ choice_string.external_string as choice,
1359
+ coalesce(deactivate_witnesses.witnesses, array[]::text[]) as witnesses,
1360
+ null::bytea as contract_instance,
1361
+ deactivate_event.exercise_argument as exercise_argument,
1362
+ deactivate_event.exercise_result as exercise_result,
1363
+ jsonb_build_object(
1364
+ 'source_table', 'participant.lapi_events_deactivate_contract',
1365
+ 'update_id', encode(deactivate_event.update_id, 'hex'),
1366
+ 'event_offset', deactivate_event.event_offset,
1367
+ 'event_type', deactivate_event.event_type,
1368
+ 'event_sequential_id', deactivate_event.event_sequential_id,
1369
+ 'node_id', deactivate_event.node_id,
1370
+ 'contract_id', encode(deactivate_event.contract_id, 'hex'),
1371
+ 'template_id', template_string.external_string,
1372
+ 'package_id', package_string.external_string,
1373
+ 'choice', choice_string.external_string
1374
+ ) as raw,
1375
+ deactivate_event.event_sequential_id as sort_event_sequential_id
1376
+ from participant.lapi_events_deactivate_contract deactivate_event
1377
+ left join participant.lapi_string_interning template_string
1378
+ on template_string.internal_id = deactivate_event.template_id
1379
+ left join participant.lapi_string_interning package_string
1380
+ on package_string.internal_id = deactivate_event.package_id
1381
+ left join participant.lapi_string_interning choice_string
1382
+ on choice_string.internal_id = deactivate_event.exercise_choice
1383
+ left join deactivate_witnesses
1384
+ on deactivate_witnesses.event_sequential_id = deactivate_event.event_sequential_id
1385
+ where encode(deactivate_event.update_id, 'hex') = ${quotedId}
1386
+
1387
+ union all
1388
+
1389
+ select
1390
+ case
1391
+ when various_event.consuming then 'consuming_exercise'::text
1392
+ else 'non_consuming_exercise'::text
1393
+ end as event_kind,
1394
+ '#0:' || various_event.node_id::text as event_id,
1395
+ encode(various_event.contract_id, 'hex') as contract_id,
1396
+ template_string.external_string as template_id,
1397
+ package_string.external_string as package_id,
1398
+ choice_string.external_string as choice,
1399
+ coalesce(various_witnesses.witnesses, array[]::text[]) as witnesses,
1400
+ null::bytea as contract_instance,
1401
+ various_event.exercise_argument as exercise_argument,
1402
+ various_event.exercise_result as exercise_result,
1403
+ jsonb_build_object(
1404
+ 'source_table', 'participant.lapi_events_various_witnessed',
1405
+ 'update_id', encode(various_event.update_id, 'hex'),
1406
+ 'event_offset', various_event.event_offset,
1407
+ 'event_type', various_event.event_type,
1408
+ 'event_sequential_id', various_event.event_sequential_id,
1409
+ 'node_id', various_event.node_id,
1410
+ 'consuming', various_event.consuming,
1411
+ 'contract_id', encode(various_event.contract_id, 'hex'),
1412
+ 'template_id', template_string.external_string,
1413
+ 'package_id', package_string.external_string,
1414
+ 'choice', choice_string.external_string
1415
+ ) as raw,
1416
+ various_event.event_sequential_id as sort_event_sequential_id
1417
+ from participant.lapi_events_various_witnessed various_event
1418
+ left join participant.lapi_string_interning template_string
1419
+ on template_string.internal_id = various_event.template_id
1420
+ left join participant.lapi_string_interning package_string
1421
+ on package_string.internal_id = various_event.package_id
1422
+ left join participant.lapi_string_interning choice_string
1423
+ on choice_string.internal_id = various_event.exercise_choice
1424
+ left join various_witnesses
1425
+ on various_witnesses.event_sequential_id = various_event.event_sequential_id
1426
+ where encode(various_event.update_id, 'hex') = ${quotedId}
1427
+ and various_event.event_type in (5, 7)
1428
+ ) update_events
1429
+ order by sort_event_sequential_id asc, event_kind asc
1430
+ `;
1431
+ }
1432
+ function rewardCouponInstanceQuery(updateId) {
1433
+ const quotedId = `'${escapeSqlLiteral(normalizeByteaHex(updateId))}'`;
1434
+ return `
1435
+ select
1436
+ coupon_contract_id,
1437
+ contract_instance
1438
+ from (
1439
+ select
1440
+ encode(contract.contract_id, 'hex') as coupon_contract_id,
1441
+ contract.instance as contract_instance,
1442
+ activate_event.event_sequential_id as sort_event_sequential_id
1443
+ from participant.lapi_events_activate_contract activate_event
1444
+ join participant.par_contracts contract
1445
+ on contract.internal_contract_id = activate_event.internal_contract_id
1446
+ where encode(activate_event.update_id, 'hex') = ${quotedId}
1447
+ and contract.template_id = 'Splice.Amulet:SvRewardCoupon'
1448
+
1449
+ union all
1450
+
1451
+ select
1452
+ encode(contract.contract_id, 'hex') as coupon_contract_id,
1453
+ contract.instance as contract_instance,
1454
+ various_event.event_sequential_id as sort_event_sequential_id
1455
+ from participant.lapi_events_various_witnessed various_event
1456
+ join participant.par_contracts contract
1457
+ on contract.internal_contract_id = various_event.internal_contract_id
1458
+ where encode(various_event.update_id, 'hex') = ${quotedId}
1459
+ and various_event.event_type = 6
1460
+ and contract.template_id = 'Splice.Amulet:SvRewardCoupon'
1461
+ ) reward_coupon_events
1462
+ order by sort_event_sequential_id desc
1463
+ limit 1
1464
+ `;
1465
+ }
1466
+ function contractDetailQuery(contractId) {
1467
+ const quotedId = `'${escapeSqlLiteral(normalizeByteaHex(contractId))}'`;
1468
+ return `
1469
+ with contract_row as (
1470
+ select
1471
+ internal_contract_id,
1472
+ encode(contract_id, 'hex') as contract_id,
1473
+ package_id,
1474
+ template_id,
1475
+ instance
1476
+ from participant.par_contracts
1477
+ where encode(contract_id, 'hex') = ${quotedId}
1478
+ limit 1
1479
+ ),
1480
+ create_events as (
1481
+ select
1482
+ encode(activate_event.update_id, 'hex') as update_id,
1483
+ activate_event.event_offset::text as event_offset,
1484
+ to_char(
1485
+ to_timestamp(activate_event.record_time / 1000000.0) at time zone 'UTC',
1486
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1487
+ ) as record_time,
1488
+ activate_event.event_sequential_id
1489
+ from participant.lapi_events_activate_contract activate_event
1490
+ join contract_row contract_row
1491
+ on contract_row.internal_contract_id = activate_event.internal_contract_id
1492
+
1493
+ union all
1494
+
1495
+ select
1496
+ encode(various_event.update_id, 'hex') as update_id,
1497
+ various_event.event_offset::text as event_offset,
1498
+ to_char(
1499
+ to_timestamp(various_event.record_time / 1000000.0) at time zone 'UTC',
1500
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1501
+ ) as record_time,
1502
+ various_event.event_sequential_id
1503
+ from participant.lapi_events_various_witnessed various_event
1504
+ join contract_row contract_row
1505
+ on contract_row.internal_contract_id = various_event.internal_contract_id
1506
+ where various_event.event_type = 6
1507
+ ),
1508
+ archive_events as (
1509
+ select
1510
+ encode(deactivate_event.update_id, 'hex') as update_id,
1511
+ deactivate_event.event_offset::text as event_offset,
1512
+ to_char(
1513
+ to_timestamp(deactivate_event.record_time / 1000000.0) at time zone 'UTC',
1514
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1515
+ ) as record_time,
1516
+ deactivate_event.event_sequential_id
1517
+ from participant.lapi_events_deactivate_contract deactivate_event
1518
+ join contract_row contract_row
1519
+ on contract_row.internal_contract_id = deactivate_event.internal_contract_id
1520
+
1521
+ union all
1522
+
1523
+ select
1524
+ encode(various_event.update_id, 'hex') as update_id,
1525
+ various_event.event_offset::text as event_offset,
1526
+ to_char(
1527
+ to_timestamp(various_event.record_time / 1000000.0) at time zone 'UTC',
1528
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1529
+ ) as record_time,
1530
+ various_event.event_sequential_id
1531
+ from participant.lapi_events_various_witnessed various_event
1532
+ join contract_row contract_row
1533
+ on contract_row.internal_contract_id = various_event.internal_contract_id
1534
+ where various_event.event_type in (5, 7)
1535
+ and various_event.consuming
1536
+ )
1537
+ select
1538
+ contract_row.contract_id,
1539
+ contract_row.template_id,
1540
+ contract_row.package_id,
1541
+ contract_row.instance as contract_instance,
1542
+ created_event.update_id as created_update_id,
1543
+ created_event.event_offset as created_event_offset,
1544
+ created_event.record_time as created_record_time,
1545
+ archived_event.update_id as archived_update_id,
1546
+ archived_event.event_offset as archived_event_offset,
1547
+ archived_event.record_time as archived_record_time
1548
+ from contract_row
1549
+ left join lateral (
1550
+ select
1551
+ update_id,
1552
+ event_offset,
1553
+ record_time
1554
+ from create_events
1555
+ order by event_sequential_id asc
1556
+ limit 1
1557
+ ) created_event on true
1558
+ left join lateral (
1559
+ select
1560
+ update_id,
1561
+ event_offset,
1562
+ record_time
1563
+ from archive_events
1564
+ order by event_sequential_id asc
1565
+ limit 1
1566
+ ) archived_event on true
1567
+ `;
1568
+ }
1569
+ function tokenRowsQuery(limit, templateIds, templatePatterns = []) {
1570
+ const normalizedLimit = Number.isFinite(limit) && Number(limit) > 0 ? Math.trunc(limit) : TOKEN_TRANSFER_CACHE_LIMIT;
1571
+ const quotedTemplateIds = templateIds
1572
+ .map((templateId) => `'${escapeSqlLiteral(templateId)}'`)
1573
+ .join(',\n ');
1574
+ const patternClauses = templatePatterns.map((pattern) => `contract.template_id::text like '${escapeSqlLiteral(pattern)}'`);
1575
+ const templateFilterClause = [
1576
+ quotedTemplateIds ? `contract.template_id in (\n ${quotedTemplateIds}\n )` : null,
1577
+ ...patternClauses,
1578
+ ]
1579
+ .filter((clause) => clause !== null)
1580
+ .join('\n or ');
1581
+ return `
1582
+ select
1583
+ contract_id,
1584
+ update_id,
1585
+ event_offset,
1586
+ record_time,
1587
+ template_id,
1588
+ package_id,
1589
+ contract_instance
1590
+ from (
1591
+ select
1592
+ encode(contract.contract_id, 'hex') as contract_id,
1593
+ encode(activate_event.update_id, 'hex') as update_id,
1594
+ activate_event.event_offset::text as event_offset,
1595
+ to_char(
1596
+ to_timestamp(activate_event.record_time / 1000000.0) at time zone 'UTC',
1597
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1598
+ ) as record_time,
1599
+ contract.template_id::text as template_id,
1600
+ package_string.external_string as package_id,
1601
+ contract.instance as contract_instance
1602
+ from participant.lapi_events_activate_contract activate_event
1603
+ join participant.par_contracts contract
1604
+ on contract.internal_contract_id = activate_event.internal_contract_id
1605
+ left join participant.lapi_string_interning package_string
1606
+ on package_string.internal_id = activate_event.representative_package_id
1607
+ where ${templateFilterClause}
1608
+
1609
+ union all
1610
+
1611
+ select
1612
+ encode(contract.contract_id, 'hex') as contract_id,
1613
+ encode(various_event.update_id, 'hex') as update_id,
1614
+ various_event.event_offset::text as event_offset,
1615
+ to_char(
1616
+ to_timestamp(various_event.record_time / 1000000.0) at time zone 'UTC',
1617
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1618
+ ) as record_time,
1619
+ contract.template_id::text as template_id,
1620
+ package_string.external_string as package_id,
1621
+ contract.instance as contract_instance
1622
+ from participant.lapi_events_various_witnessed various_event
1623
+ join participant.par_contracts contract
1624
+ on contract.internal_contract_id = various_event.internal_contract_id
1625
+ left join participant.lapi_string_interning package_string
1626
+ on package_string.internal_id = various_event.package_id
1627
+ where various_event.event_type = 6
1628
+ and (${templateFilterClause})
1629
+ ) token_transfer_rows
1630
+ order by event_offset::numeric desc
1631
+ limit ${normalizedLimit}
1632
+ `;
1633
+ }
1634
+ function recentCip112MovementUpdateIdsQuery(limit) {
1635
+ const normalizedLimit = Number.isFinite(limit) && Number(limit) > 0 ? Math.trunc(limit) : TOKEN_TRANSFER_CACHE_LIMIT;
1636
+ return `
1637
+ /* cip112_movement_update_ids */
1638
+ with relevant_updates as (
1639
+ select
1640
+ encode(activate_event.update_id, 'hex') as update_id,
1641
+ activate_event.event_offset::text as event_offset,
1642
+ to_char(
1643
+ to_timestamp(activate_event.record_time / 1000000.0) at time zone 'UTC',
1644
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1645
+ ) as record_time
1646
+ from participant.lapi_events_activate_contract activate_event
1647
+ join participant.par_contracts contract
1648
+ on contract.internal_contract_id = activate_event.internal_contract_id
1649
+ where contract.template_id::text like '${escapeSqlLiteral(CIP112_TEMPLATE_ID_LIKE_PATTERN)}'
1650
+
1651
+ union all
1652
+
1653
+ select
1654
+ encode(deactivate_event.update_id, 'hex') as update_id,
1655
+ deactivate_event.event_offset::text as event_offset,
1656
+ to_char(
1657
+ to_timestamp(deactivate_event.record_time / 1000000.0) at time zone 'UTC',
1658
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1659
+ ) as record_time
1660
+ from participant.lapi_events_deactivate_contract deactivate_event
1661
+ left join participant.lapi_string_interning template_string
1662
+ on template_string.internal_id = deactivate_event.template_id
1663
+ where template_string.external_string like '${escapeSqlLiteral(CIP112_TEMPLATE_ID_LIKE_PATTERN)}'
1664
+
1665
+ union all
1666
+
1667
+ select
1668
+ encode(various_event.update_id, 'hex') as update_id,
1669
+ various_event.event_offset::text as event_offset,
1670
+ to_char(
1671
+ to_timestamp(various_event.record_time / 1000000.0) at time zone 'UTC',
1672
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1673
+ ) as record_time
1674
+ from participant.lapi_events_various_witnessed various_event
1675
+ left join participant.par_contracts contract
1676
+ on contract.internal_contract_id = various_event.internal_contract_id
1677
+ left join participant.lapi_string_interning template_string
1678
+ on template_string.internal_id = various_event.template_id
1679
+ where (
1680
+ various_event.event_type = 6
1681
+ and contract.template_id::text like '${escapeSqlLiteral(CIP112_TEMPLATE_ID_LIKE_PATTERN)}'
1682
+ ) or (
1683
+ various_event.event_type in (5, 7)
1684
+ and template_string.external_string like '${escapeSqlLiteral(CIP112_TEMPLATE_ID_LIKE_PATTERN)}'
1685
+ )
1686
+ )
1687
+ select
1688
+ update_id,
1689
+ max(event_offset::numeric)::text as event_offset,
1690
+ max(record_time) as record_time
1691
+ from relevant_updates
1692
+ group by update_id
1693
+ order by max(event_offset::numeric) desc
1694
+ limit ${normalizedLimit}
1695
+ `;
1696
+ }
1697
+ function buildActiveContractsFilterClause(parties, templates, partyMode, hideSplice) {
1698
+ const partyConditions = normalizePartyFilters(parties).map((party) => partyWitnessArrayMatchCondition('create_events.witnesses', party));
1699
+ const templateExpression = "regexp_replace(coalesce(contract.template_id::text, ''), '^t\\\\|#[^:]+:', '')";
1700
+ const templateConditions = normalizeTemplateFilters(templates).map((templateId) => {
1701
+ const quotedTemplateId = `'${escapeSqlLiteral(templateId)}'`;
1702
+ return `${templateExpression} = ${quotedTemplateId}`;
1703
+ });
1704
+ const groups = [];
1705
+ if (partyConditions.length > 0) {
1706
+ const partyJoiner = normalizePartyFilterMode(partyMode) === 'and' ? '\n and ' : '\n or ';
1707
+ groups.push(partyConditions.length === 1
1708
+ ? partyConditions[0]
1709
+ : `(\n ${partyConditions.join(partyJoiner)}\n )`);
1710
+ }
1711
+ if (templateConditions.length > 0) {
1712
+ groups.push(templateConditions.length === 1
1713
+ ? templateConditions[0]
1714
+ : `(\n ${templateConditions.join('\n or ')}\n )`);
1715
+ }
1716
+ if (hideSplice) {
1717
+ groups.push(`${templateExpression} not like 'Splice.%'`);
1718
+ }
1719
+ if (groups.length === 0) {
1720
+ return null;
1721
+ }
1722
+ return groups.length === 1 ? groups[0] : `(\n ${groups.join('\n and ')}\n )`;
1723
+ }
1724
+ function activeContractsQuery(limit, before, after, parties, templates, partyMode, hideSplice) {
1725
+ const normalizedBefore = normalizeEventOffsetCursor(before);
1726
+ const normalizedAfter = normalizeEventOffsetCursor(after);
1727
+ const queryLimit = limit + 1;
1728
+ const useAfterCursor = Boolean(normalizedAfter && !normalizedBefore);
1729
+ const cursorFilter = useAfterCursor
1730
+ ? `create_event_offset::numeric > ${normalizedAfter}`
1731
+ : normalizedBefore
1732
+ ? `create_event_offset::numeric < ${normalizedBefore}`
1733
+ : null;
1734
+ const orderDirection = useAfterCursor ? 'asc' : 'desc';
1735
+ const filterClause = buildActiveContractsFilterClause(parties, templates, partyMode, hideSplice);
1736
+ const whereConditions = [
1737
+ `not exists (
1738
+ select 1
1739
+ from archived_contracts archived_contract
1740
+ where archived_contract.internal_contract_id = create_events.internal_contract_id
1741
+ )`,
1742
+ cursorFilter,
1743
+ filterClause,
1744
+ ].filter((value) => Boolean(value));
1745
+ const whereClause = whereConditions.length > 0 ? `where ${whereConditions.join('\n and ')}` : '';
1746
+ return `
1747
+ with activate_witnesses as (
1748
+ select
1749
+ witness_filter.event_sequential_id,
1750
+ array_agg(distinct party_string.external_string order by party_string.external_string) as witnesses
1751
+ from participant.lapi_filter_activate_witness witness_filter
1752
+ join participant.lapi_string_interning party_string
1753
+ on party_string.internal_id = witness_filter.party_id
1754
+ group by witness_filter.event_sequential_id
1755
+ ),
1756
+ various_witnesses as (
1757
+ select
1758
+ witness_filter.event_sequential_id,
1759
+ array_agg(distinct party_string.external_string order by party_string.external_string) as witnesses
1760
+ from participant.lapi_filter_various_witness witness_filter
1761
+ join participant.lapi_string_interning party_string
1762
+ on party_string.internal_id = witness_filter.party_id
1763
+ group by witness_filter.event_sequential_id
1764
+ ),
1765
+ create_events as (
1766
+ select
1767
+ activate_event.internal_contract_id,
1768
+ activate_event.event_offset::text as create_event_offset,
1769
+ to_char(
1770
+ to_timestamp(activate_event.record_time / 1000000.0) at time zone 'UTC',
1771
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1772
+ ) as created_record_time,
1773
+ coalesce(activate_witnesses.witnesses, array[]::text[]) as witnesses
1774
+ from participant.lapi_events_activate_contract activate_event
1775
+ left join activate_witnesses
1776
+ on activate_witnesses.event_sequential_id = activate_event.event_sequential_id
1777
+
1778
+ union all
1779
+
1780
+ select
1781
+ various_event.internal_contract_id,
1782
+ various_event.event_offset::text as create_event_offset,
1783
+ to_char(
1784
+ to_timestamp(various_event.record_time / 1000000.0) at time zone 'UTC',
1785
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1786
+ ) as created_record_time,
1787
+ coalesce(various_witnesses.witnesses, array[]::text[]) as witnesses
1788
+ from participant.lapi_events_various_witnessed various_event
1789
+ left join various_witnesses
1790
+ on various_witnesses.event_sequential_id = various_event.event_sequential_id
1791
+ where various_event.event_type = 6
1792
+ ),
1793
+ archived_contracts as (
1794
+ select distinct deactivate_event.internal_contract_id
1795
+ from participant.lapi_events_deactivate_contract deactivate_event
1796
+
1797
+ union
1798
+
1799
+ select distinct various_event.internal_contract_id
1800
+ from participant.lapi_events_various_witnessed various_event
1801
+ where various_event.event_type in (5, 7)
1802
+ and various_event.consuming
1803
+ )
1804
+ select
1805
+ encode(contract.contract_id, 'hex') as contract_id,
1806
+ regexp_replace(coalesce(contract.template_id::text, ''), '^t\\\\|#[^:]+:', '') as template_id,
1807
+ create_events.created_record_time,
1808
+ create_events.create_event_offset as created_event_offset
1809
+ from create_events
1810
+ join participant.par_contracts contract
1811
+ on contract.internal_contract_id = create_events.internal_contract_id
1812
+ ${whereClause}
1813
+ order by create_event_offset::numeric ${orderDirection}
1814
+ limit ${queryLimit}
1815
+ `;
1816
+ }
1817
+ function searchContractsQuery(searchQuery, limit) {
1818
+ const quotedQuery = escapeSqlLiteral(searchQuery);
1819
+ return `
1820
+ with activate_witnesses as (
1821
+ select
1822
+ witness_filter.event_sequential_id,
1823
+ array_agg(distinct party_string.external_string order by party_string.external_string) as witnesses
1824
+ from participant.lapi_filter_activate_witness witness_filter
1825
+ join participant.lapi_string_interning party_string
1826
+ on party_string.internal_id = witness_filter.party_id
1827
+ group by witness_filter.event_sequential_id
1828
+ ),
1829
+ various_witnesses as (
1830
+ select
1831
+ witness_filter.event_sequential_id,
1832
+ array_agg(distinct party_string.external_string order by party_string.external_string) as witnesses
1833
+ from participant.lapi_filter_various_witness witness_filter
1834
+ join participant.lapi_string_interning party_string
1835
+ on party_string.internal_id = witness_filter.party_id
1836
+ group by witness_filter.event_sequential_id
1837
+ ),
1838
+ create_events as (
1839
+ select
1840
+ activate_event.internal_contract_id,
1841
+ activate_event.event_offset::text as create_event_offset,
1842
+ to_char(
1843
+ to_timestamp(activate_event.record_time / 1000000.0) at time zone 'UTC',
1844
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1845
+ ) as created_record_time,
1846
+ coalesce(activate_witnesses.witnesses, array[]::text[]) as witnesses
1847
+ from participant.lapi_events_activate_contract activate_event
1848
+ left join activate_witnesses
1849
+ on activate_witnesses.event_sequential_id = activate_event.event_sequential_id
1850
+
1851
+ union all
1852
+
1853
+ select
1854
+ various_event.internal_contract_id,
1855
+ various_event.event_offset::text as create_event_offset,
1856
+ to_char(
1857
+ to_timestamp(various_event.record_time / 1000000.0) at time zone 'UTC',
1858
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1859
+ ) as created_record_time,
1860
+ coalesce(various_witnesses.witnesses, array[]::text[]) as witnesses
1861
+ from participant.lapi_events_various_witnessed various_event
1862
+ left join various_witnesses
1863
+ on various_witnesses.event_sequential_id = various_event.event_sequential_id
1864
+ where various_event.event_type = 6
1865
+ ),
1866
+ archived_contracts as (
1867
+ select distinct deactivate_event.internal_contract_id
1868
+ from participant.lapi_events_deactivate_contract deactivate_event
1869
+
1870
+ union
1871
+
1872
+ select distinct various_event.internal_contract_id
1873
+ from participant.lapi_events_various_witnessed various_event
1874
+ where various_event.event_type in (5, 7)
1875
+ and various_event.consuming
1876
+ )
1877
+ select
1878
+ encode(contract.contract_id, 'hex') as contract_id,
1879
+ regexp_replace(coalesce(contract.template_id::text, ''), '^t\\\\|#[^:]+:', '') as template_id,
1880
+ create_events.created_record_time
1881
+ from create_events
1882
+ join participant.par_contracts contract
1883
+ on contract.internal_contract_id = create_events.internal_contract_id
1884
+ where encode(contract.contract_id, 'hex') like '${quotedQuery}%'
1885
+ and not exists (
1886
+ select 1
1887
+ from archived_contracts archived_contract
1888
+ where archived_contract.internal_contract_id = create_events.internal_contract_id
1889
+ )
1890
+ order by create_events.created_record_time desc nulls last, contract_id asc
1891
+ limit ${limit}
1892
+ `;
1893
+ }
1894
+ function escapeSqlLiteral(value) {
1895
+ return value.replaceAll("'", "''");
1896
+ }
1897
+ function normalizeEventOffsetCursor(value) {
1898
+ if (!value) {
1899
+ return null;
1900
+ }
1901
+ const trimmed = value.trim();
1902
+ if (!/^\d+$/.test(trimmed)) {
1903
+ return null;
1904
+ }
1905
+ return trimmed.replace(/^0+(?=\d)/, '');
1906
+ }
1907
+ function normalizeByteaHex(value) {
1908
+ return value.startsWith('\\x') ? value.slice(2) : value;
1909
+ }
1910
+ let PqsSummaryService = class PqsSummaryService {
1911
+ clientFactory;
1912
+ damlValueDecoder;
1913
+ packageCacheService;
1914
+ packageRegistryService;
1915
+ nodeConfigService;
1916
+ grpcOperationsService;
1917
+ packageSyncService;
1918
+ observedTokensByNode = new Map();
1919
+ tokenHoldersByNode = new Map();
1920
+ tokenTransfersByNode = new Map();
1921
+ constructor(clientFactory, damlValueDecoder, packageCacheService, packageRegistryService, nodeConfigService, grpcOperationsService, packageSyncService) {
1922
+ this.clientFactory = clientFactory;
1923
+ this.damlValueDecoder = damlValueDecoder;
1924
+ this.packageCacheService = packageCacheService;
1925
+ this.packageRegistryService = packageRegistryService;
1926
+ this.nodeConfigService = nodeConfigService;
1927
+ this.grpcOperationsService = grpcOperationsService;
1928
+ this.packageSyncService = packageSyncService;
1929
+ }
1930
+ async fetchSummary(node) {
1931
+ const client = this.clientFactory.getClient(node);
1932
+ const result = await this.querySummary(client.query.bind(client));
1933
+ const row = result.rows[0] ?? this.defaultRow();
1934
+ return {
1935
+ ledgerLabel: node.ledgerLabel ?? node.label,
1936
+ pqsDatabase: row.pqs_database,
1937
+ activeContractCount: Number(row.active_contract_count ?? 0),
1938
+ latestOffset: row.latest_offset ?? null,
1939
+ latestEventAt: row.latest_event_at ?? null,
1940
+ totalUpdateCount: Number(row.total_update_count ?? 0),
1941
+ };
1942
+ }
1943
+ async search(query) {
1944
+ const trimmedQuery = query.trim();
1945
+ if (!trimmedQuery) {
1946
+ return {
1947
+ query: '',
1948
+ updates: this.emptySearchGroup(),
1949
+ contracts: this.emptySearchGroup(),
1950
+ parties: this.emptySearchGroup(),
1951
+ packages: {
1952
+ packageIds: this.emptySearchGroup(),
1953
+ packageNames: this.emptySearchGroup(),
1954
+ },
1955
+ };
1956
+ }
1957
+ const nodes = this.nodeConfigService?.list() ?? [];
1958
+ const updates = await this.searchUpdates(nodes, trimmedQuery);
1959
+ const contracts = await this.searchContracts(nodes, trimmedQuery);
1960
+ const parties = await this.searchParties(nodes, trimmedQuery);
1961
+ const packages = await this.searchPackages(trimmedQuery);
1962
+ return {
1963
+ query: trimmedQuery,
1964
+ updates,
1965
+ contracts,
1966
+ parties,
1967
+ packages,
1968
+ };
1969
+ }
1970
+ async fetchRecentUpdates(node, options = 25) {
1971
+ const client = this.clientFactory.getClient(node);
1972
+ const normalizedLimit = typeof options === 'number'
1973
+ ? Number.isFinite(options) && options > 0
1974
+ ? Math.trunc(options)
1975
+ : 25
1976
+ : Number.isFinite(options.limit) && Number(options.limit) > 0
1977
+ ? Math.trunc(Number(options.limit))
1978
+ : 25;
1979
+ const before = typeof options === 'object' ? options.before : undefined;
1980
+ const after = typeof options === 'object' ? options.after : undefined;
1981
+ const parties = typeof options === 'object' ? options.parties : undefined;
1982
+ const templates = typeof options === 'object' ? options.templates : undefined;
1983
+ const partyMode = typeof options === 'object' ? (options.partyMode ?? options.mode) : undefined;
1984
+ const hideSplice = typeof options === 'object' ? options.hideSplice === true : false;
1985
+ const useAfterCursor = Boolean(after && !before);
1986
+ const query = client.query.bind(client);
1987
+ const rawMetaRows = await this.queryRecentUpdateMetaRows(query, normalizedLimit, before, after, parties, templates, partyMode, hideSplice);
1988
+ const hasMoreInQuery = rawMetaRows.length > normalizedLimit;
1989
+ const trimmedMetaRows = rawMetaRows.slice(0, normalizedLimit);
1990
+ const orderedUpdates = (useAfterCursor ? [...trimmedMetaRows].reverse() : trimmedMetaRows).map((row) => ({
1991
+ eventOffset: this.extractEventOffset(row),
1992
+ rawUpdateId: row.update_id,
1993
+ updateId: this.normalizeUpdateId(row.update_id),
1994
+ recordTime: row.record_time ?? null,
1995
+ }));
1996
+ if (orderedUpdates.length === 0) {
1997
+ return {
1998
+ nodeId: node.id,
1999
+ label: node.label,
2000
+ limit: normalizedLimit,
2001
+ nextBefore: null,
2002
+ nextAfter: null,
2003
+ updates: [],
2004
+ };
2005
+ }
2006
+ const partiesByUpdateId = await this.fetchPartiesByUpdateId(query, orderedUpdates.map((update) => update.rawUpdateId));
2007
+ return {
2008
+ nodeId: node.id,
2009
+ label: node.label,
2010
+ limit: normalizedLimit,
2011
+ nextBefore: useAfterCursor || hasMoreInQuery
2012
+ ? orderedUpdates[orderedUpdates.length - 1]?.eventOffset ?? null
2013
+ : null,
2014
+ nextAfter: useAfterCursor
2015
+ ? hasMoreInQuery
2016
+ ? orderedUpdates[0]?.eventOffset ?? null
2017
+ : null
2018
+ : before
2019
+ ? orderedUpdates[0]?.eventOffset ?? null
2020
+ : null,
2021
+ updates: orderedUpdates.map((update) => ({
2022
+ eventOffset: update.eventOffset,
2023
+ updateId: update.updateId,
2024
+ recordTime: update.recordTime,
2025
+ parties: partiesByUpdateId.get(update.updateId) ?? [],
2026
+ })),
2027
+ };
2028
+ }
2029
+ async fetchGlobalRecentUpdates(nodes, limit = 25, options) {
2030
+ const normalizedLimit = Number.isFinite(limit) && Number(limit) > 0 ? Math.trunc(Number(limit)) : 25;
2031
+ const beforeCursor = decodeGlobalUpdateCursor(options?.before);
2032
+ const afterCursor = beforeCursor === null ? decodeGlobalUpdateCursor(options?.after) : null;
2033
+ const parties = options?.parties;
2034
+ const templates = options?.templates;
2035
+ const partyMode = options?.partyMode ?? options?.mode;
2036
+ const hideSplice = options?.hideSplice === true;
2037
+ const useAfterCursor = Boolean(afterCursor && !beforeCursor);
2038
+ const pageSize = Math.max(normalizedLimit * 2, normalizedLimit + 1);
2039
+ const nodeStates = nodes.map((node) => ({
2040
+ node,
2041
+ nextBefore: undefined,
2042
+ exhausted: false,
2043
+ }));
2044
+ const mergedUpdatesByKey = new Map();
2045
+ const filterUpdates = (updates) => {
2046
+ if (beforeCursor !== null) {
2047
+ return updates.filter((update) => compareGlobalMergedUpdates(update, beforeCursor) > 0);
2048
+ }
2049
+ if (afterCursor !== null) {
2050
+ return updates.filter((update) => compareGlobalMergedUpdates(update, afterCursor) < 0);
2051
+ }
2052
+ return updates;
2053
+ };
2054
+ let filteredUpdates = [];
2055
+ let hasMoreInDirection = false;
2056
+ while (true) {
2057
+ const nodesToFetch = nodeStates.filter((state) => !state.exhausted);
2058
+ if (nodesToFetch.length === 0) {
2059
+ break;
2060
+ }
2061
+ const responses = await Promise.all(nodesToFetch.map((state) => this.fetchRecentUpdates(state.node, {
2062
+ limit: pageSize,
2063
+ parties,
2064
+ templates,
2065
+ partyMode,
2066
+ hideSplice,
2067
+ before: state.nextBefore,
2068
+ })));
2069
+ responses.forEach((response, index) => {
2070
+ const state = nodesToFetch[index];
2071
+ for (const update of response.updates) {
2072
+ mergedUpdatesByKey.set(`${state.node.id}:${update.eventOffset}:${update.updateId}`, {
2073
+ nodeId: state.node.id,
2074
+ label: state.node.label,
2075
+ eventOffset: update.eventOffset,
2076
+ updateId: update.updateId,
2077
+ recordTime: update.recordTime,
2078
+ parties: update.parties,
2079
+ });
2080
+ }
2081
+ state.nextBefore = response.nextBefore ?? undefined;
2082
+ state.exhausted = response.nextBefore === null;
2083
+ });
2084
+ const sortedUpdates = Array.from(mergedUpdatesByKey.values()).sort(compareGlobalMergedUpdates);
2085
+ filteredUpdates = filterUpdates(sortedUpdates);
2086
+ hasMoreInDirection = filteredUpdates.length > normalizedLimit;
2087
+ if (hasMoreInDirection || nodeStates.every((state) => state.exhausted)) {
2088
+ break;
2089
+ }
2090
+ }
2091
+ const updates = filteredUpdates.slice(0, normalizedLimit);
2092
+ return {
2093
+ limit: normalizedLimit,
2094
+ nextBefore: updates.length > 0 && (useAfterCursor || hasMoreInDirection)
2095
+ ? encodeGlobalUpdateCursor(updates[updates.length - 1])
2096
+ : null,
2097
+ nextAfter: updates.length === 0
2098
+ ? null
2099
+ : useAfterCursor
2100
+ ? hasMoreInDirection
2101
+ ? encodeGlobalUpdateCursor(updates[0])
2102
+ : null
2103
+ : beforeCursor
2104
+ ? encodeGlobalUpdateCursor(updates[0])
2105
+ : null,
2106
+ updates,
2107
+ };
2108
+ }
2109
+ emptySearchGroup() {
2110
+ return {
2111
+ items: [],
2112
+ displayedCount: 0,
2113
+ truncated: false,
2114
+ status: 'ok',
2115
+ warnings: [],
2116
+ };
2117
+ }
2118
+ finalizeSearchGroup(options) {
2119
+ const totalCount = options.totalCount ?? options.items.length;
2120
+ return {
2121
+ items: options.items,
2122
+ displayedCount: options.items.length,
2123
+ truncated: totalCount > options.items.length,
2124
+ status: options.status ?? 'ok',
2125
+ warnings: options.warnings ?? [],
2126
+ };
2127
+ }
2128
+ resolveNodeSearchStatus(settled, subject, nodes) {
2129
+ const warnings = settled.flatMap((result, index) => result.status === 'rejected'
2130
+ ? [`Failed to search ${subject} on ${nodes[index]?.label ?? nodes[index]?.id ?? 'unknown node'}`]
2131
+ : []);
2132
+ const hasSuccess = settled.some((result) => result.status === 'fulfilled');
2133
+ return {
2134
+ status: warnings.length === 0 ? 'ok' : hasSuccess ? 'partial' : 'failed',
2135
+ warnings,
2136
+ hasSuccess,
2137
+ };
2138
+ }
2139
+ isExactMatch(query, values) {
2140
+ return values.some((value) => typeof value === 'string' && value === query);
2141
+ }
2142
+ hasPrefixMatch(query, values) {
2143
+ return values.some((value) => typeof value === 'string' && value.startsWith(query));
2144
+ }
2145
+ async searchUpdates(nodes, query) {
2146
+ if (nodes.length === 0) {
2147
+ return this.emptySearchGroup();
2148
+ }
2149
+ const settled = await Promise.allSettled(nodes.map((node) => this.searchUpdatesForNode(node, query, SEARCH_GROUP_LIMIT + 1)));
2150
+ const deduped = new Map();
2151
+ for (const result of settled) {
2152
+ if (result.status !== 'fulfilled') {
2153
+ continue;
2154
+ }
2155
+ for (const item of result.value) {
2156
+ const key = `${item.nodeId}:${item.eventOffset}`;
2157
+ const existing = deduped.get(key);
2158
+ if (!existing || (item.exact && !existing.exact)) {
2159
+ deduped.set(key, item);
2160
+ }
2161
+ }
2162
+ }
2163
+ const ordered = Array.from(deduped.values()).sort(compareSearchUpdates);
2164
+ const { status, warnings } = this.resolveNodeSearchStatus(settled, 'updates', nodes);
2165
+ return this.finalizeSearchGroup({
2166
+ items: ordered.slice(0, SEARCH_GROUP_LIMIT).map(({ exact: _exact, ...item }) => item),
2167
+ totalCount: ordered.length,
2168
+ status,
2169
+ warnings,
2170
+ });
2171
+ }
2172
+ async searchUpdatesForNode(node, query, limit) {
2173
+ const client = this.clientFactory.getClient(node);
2174
+ const rows = await this.querySearchUpdateMetaRows(client.query.bind(client), query, limit);
2175
+ const dedupedRows = new Map();
2176
+ for (const row of rows) {
2177
+ const eventOffset = this.extractEventOffset(row);
2178
+ if (!dedupedRows.has(eventOffset)) {
2179
+ dedupedRows.set(eventOffset, row);
2180
+ }
2181
+ }
2182
+ const rawRows = Array.from(dedupedRows.values());
2183
+ const rawUpdateIds = rawRows.map((row) => row.update_id);
2184
+ const partiesByUpdateId = rawUpdateIds.length > 0
2185
+ ? await this.fetchPartiesByUpdateId(client.query.bind(client), rawUpdateIds)
2186
+ : new Map();
2187
+ return rawRows
2188
+ .map((row) => {
2189
+ const normalizedUpdateId = this.normalizeUpdateId(row.update_id);
2190
+ const eventOffset = this.extractEventOffset(row);
2191
+ const exact = this.isExactMatch(query, [eventOffset, normalizedUpdateId]);
2192
+ const matches = exact || this.hasPrefixMatch(query, [eventOffset, normalizedUpdateId]);
2193
+ if (!matches) {
2194
+ return null;
2195
+ }
2196
+ return {
2197
+ nodeId: node.id,
2198
+ label: node.label,
2199
+ eventOffset,
2200
+ updateId: normalizedUpdateId,
2201
+ recordTime: row.record_time ?? null,
2202
+ parties: partiesByUpdateId.get(normalizedUpdateId) ?? [],
2203
+ exact,
2204
+ };
2205
+ })
2206
+ .filter((item) => item !== null);
2207
+ }
2208
+ async searchContracts(nodes, query) {
2209
+ if (nodes.length === 0) {
2210
+ return this.emptySearchGroup();
2211
+ }
2212
+ const settled = await Promise.allSettled(nodes.map((node) => this.searchContractsForNode(node, query, SEARCH_GROUP_LIMIT + 1)));
2213
+ const deduped = new Map();
2214
+ for (const result of settled) {
2215
+ if (result.status !== 'fulfilled') {
2216
+ continue;
2217
+ }
2218
+ for (const item of result.value) {
2219
+ deduped.set(`${item.nodeId}:${item.contractId}`, item);
2220
+ }
2221
+ }
2222
+ const ordered = Array.from(deduped.values()).sort(compareSearchContracts);
2223
+ const { status, warnings } = this.resolveNodeSearchStatus(settled, 'contracts', nodes);
2224
+ return this.finalizeSearchGroup({
2225
+ items: ordered.slice(0, SEARCH_GROUP_LIMIT).map(({ exact: _exact, ...item }) => item),
2226
+ totalCount: ordered.length,
2227
+ status,
2228
+ warnings,
2229
+ });
2230
+ }
2231
+ async searchContractsForNode(node, query, limit) {
2232
+ const client = this.clientFactory.getClient(node);
2233
+ const result = await client.query(searchContractsQuery(query, limit));
2234
+ const rows = result.rows ?? [];
2235
+ return rows
2236
+ .filter((row) => this.hasPrefixMatch(query, [row.contract_id]))
2237
+ .map((row) => ({
2238
+ nodeId: node.id,
2239
+ label: node.label,
2240
+ contractId: row.contract_id,
2241
+ templateId: this.normalizeTemplateIdentifier(row.template_id),
2242
+ createdRecordTime: row.created_record_time ?? null,
2243
+ exact: this.isExactMatch(query, [row.contract_id]),
2244
+ }));
2245
+ }
2246
+ async searchParties(nodes, query) {
2247
+ if (nodes.length === 0) {
2248
+ return this.emptySearchGroup();
2249
+ }
2250
+ const normalizedQuery = this.normalizePartyIdentifier(query);
2251
+ const settled = await Promise.allSettled(nodes.map(async (node) => ({
2252
+ node,
2253
+ parties: await this.fetchActivePartiesForNode(node),
2254
+ })));
2255
+ const merged = new Map();
2256
+ for (const result of settled) {
2257
+ if (result.status !== 'fulfilled') {
2258
+ continue;
2259
+ }
2260
+ for (const partyId of result.value.parties) {
2261
+ if (!partyId.startsWith(normalizedQuery)) {
2262
+ continue;
2263
+ }
2264
+ const existing = merged.get(partyId);
2265
+ if (existing) {
2266
+ existing.nodeIds = Array.from(new Set([...existing.nodeIds, result.value.node.id])).sort();
2267
+ continue;
2268
+ }
2269
+ merged.set(partyId, {
2270
+ partyId,
2271
+ nodeIds: [result.value.node.id],
2272
+ exact: partyId === normalizedQuery,
2273
+ });
2274
+ }
2275
+ }
2276
+ const ordered = Array.from(merged.values()).sort(compareSearchParties);
2277
+ const { status, warnings } = this.resolveNodeSearchStatus(settled, 'parties', nodes);
2278
+ return this.finalizeSearchGroup({
2279
+ items: ordered.slice(0, SEARCH_GROUP_LIMIT).map(({ exact: _exact, ...item }) => item),
2280
+ totalCount: ordered.length,
2281
+ status,
2282
+ warnings,
2283
+ });
2284
+ }
2285
+ async searchPackages(query) {
2286
+ const packages = this.packageCacheService?.listPackages() ?? [];
2287
+ const packageIdMatches = packages
2288
+ .filter((pkg) => pkg.packageId.startsWith(query))
2289
+ .map((pkg) => ({
2290
+ packageId: pkg.packageId,
2291
+ name: pkg.name,
2292
+ version: pkg.version,
2293
+ exact: pkg.packageId === query,
2294
+ }))
2295
+ .sort(compareSearchPackageIds);
2296
+ const groupedNames = new Map();
2297
+ for (const pkg of packages) {
2298
+ if (!pkg.name || !pkg.name.startsWith(query)) {
2299
+ continue;
2300
+ }
2301
+ const existing = groupedNames.get(pkg.name);
2302
+ if (existing) {
2303
+ existing.packages.push({ packageId: pkg.packageId, version: pkg.version });
2304
+ continue;
2305
+ }
2306
+ groupedNames.set(pkg.name, {
2307
+ name: pkg.name,
2308
+ packages: [{ packageId: pkg.packageId, version: pkg.version }],
2309
+ exact: pkg.name === query,
2310
+ });
2311
+ }
2312
+ const packageNameMatches = Array.from(groupedNames.values())
2313
+ .map((entry) => ({
2314
+ ...entry,
2315
+ packages: [...entry.packages].sort((left, right) => (right.version ?? '').localeCompare(left.version ?? '') ||
2316
+ left.packageId.localeCompare(right.packageId)),
2317
+ }))
2318
+ .sort(compareSearchPackageNames);
2319
+ return {
2320
+ packageIds: this.finalizeSearchGroup({
2321
+ items: packageIdMatches
2322
+ .slice(0, SEARCH_GROUP_LIMIT)
2323
+ .map(({ exact: _exact, ...item }) => item),
2324
+ totalCount: packageIdMatches.length,
2325
+ }),
2326
+ packageNames: this.finalizeSearchGroup({
2327
+ items: packageNameMatches
2328
+ .slice(0, SEARCH_GROUP_LIMIT)
2329
+ .map(({ exact: _exact, ...item }) => item),
2330
+ totalCount: packageNameMatches.length,
2331
+ }),
2332
+ };
2333
+ }
2334
+ async querySearchUpdateMetaRows(queryFn, searchQuery, limit) {
2335
+ try {
2336
+ const result = await queryFn(searchUpdatesQuery(searchQuery, limit));
2337
+ return result.rows ?? [];
2338
+ }
2339
+ catch (error) {
2340
+ if (!this.shouldFallbackToNormalizedEventTables(error)) {
2341
+ throw error;
2342
+ }
2343
+ }
2344
+ const fallbackResult = await queryFn(normalizedSearchUpdatesQuery(searchQuery, limit));
2345
+ return fallbackResult.rows ?? [];
2346
+ }
2347
+ async queryRecentUpdateMetaRows(query, limit, before, after, parties, templates, mode, hideSplice) {
2348
+ try {
2349
+ const result = await query(recentUpdatesQuery(limit, before, after, parties, templates, mode, hideSplice));
2350
+ return result.rows ?? [];
2351
+ }
2352
+ catch (error) {
2353
+ if (!this.shouldFallbackToNormalizedEventTables(error)) {
2354
+ throw error;
2355
+ }
2356
+ }
2357
+ const fallbackResult = await query(normalizedRecentUpdatesQuery(limit, before, after, parties, templates, mode, hideSplice));
2358
+ return fallbackResult.rows ?? [];
2359
+ }
2360
+ async fetchNodeContracts(node, options) {
2361
+ const client = this.clientFactory.getClient(node);
2362
+ const normalizedLimit = typeof options?.limit === 'number' && Number.isFinite(options.limit) && options.limit > 0
2363
+ ? Math.trunc(options.limit)
2364
+ : 25;
2365
+ const before = options?.before;
2366
+ const after = options?.after;
2367
+ const parties = options?.parties;
2368
+ const templates = options?.templates;
2369
+ const partyMode = options?.partyMode;
2370
+ const hideSplice = options?.hideSplice === true;
2371
+ const useAfterCursor = Boolean(after && !before);
2372
+ const result = await client.query(activeContractsQuery(normalizedLimit, before, after, parties, templates, partyMode, hideSplice));
2373
+ const rawRows = result.rows ?? [];
2374
+ const hasMoreInQuery = rawRows.length > normalizedLimit;
2375
+ const trimmedRows = rawRows.slice(0, normalizedLimit);
2376
+ const orderedContracts = (useAfterCursor ? [...trimmedRows].reverse() : trimmedRows).map((row) => ({
2377
+ contractId: row.contract_id,
2378
+ templateId: this.normalizeTemplateIdentifier(row.template_id),
2379
+ createdRecordTime: row.created_record_time ?? null,
2380
+ createdEventOffset: this.normalizeOptionalScalar(row.created_event_offset),
2381
+ }));
2382
+ return {
2383
+ nodeId: node.id,
2384
+ label: node.label,
2385
+ limit: normalizedLimit,
2386
+ nextBefore: orderedContracts.length > 0 && (useAfterCursor || hasMoreInQuery)
2387
+ ? orderedContracts[orderedContracts.length - 1]?.createdEventOffset ?? null
2388
+ : null,
2389
+ nextAfter: orderedContracts.length === 0
2390
+ ? null
2391
+ : useAfterCursor
2392
+ ? hasMoreInQuery
2393
+ ? orderedContracts[0]?.createdEventOffset ?? null
2394
+ : null
2395
+ : before
2396
+ ? orderedContracts[0]?.createdEventOffset ?? null
2397
+ : null,
2398
+ contracts: orderedContracts.map((contract) => ({
2399
+ contractId: contract.contractId,
2400
+ templateId: contract.templateId,
2401
+ createdRecordTime: contract.createdRecordTime,
2402
+ })),
2403
+ };
2404
+ }
2405
+ async fetchActivityBuckets(node, days = 30, bucketMinutes = 15) {
2406
+ const client = this.clientFactory.getClient(node);
2407
+ const result = await client.query(activityBucketsQuery(days, bucketMinutes));
2408
+ const rows = result.rows ?? [];
2409
+ return rows
2410
+ .filter((row) => typeof row.bucket_timestamp === 'string')
2411
+ .map((row) => ({
2412
+ timestamp: row.bucket_timestamp,
2413
+ activityValue: Number(row.activity_value ?? 0),
2414
+ latestOffset: row.latest_offset ?? null,
2415
+ }));
2416
+ }
2417
+ async fetchPackageDetail(packageId) {
2418
+ const metadata = this.packageCacheService?.getPackageMetadata(packageId) ?? null;
2419
+ if (!metadata) {
2420
+ throw new Error('Package not found');
2421
+ }
2422
+ const seenOnNodes = (this.packageCacheService?.listNodesForPackage(packageId) ?? []).map((row) => ({
2423
+ nodeId: row.nodeId,
2424
+ packageName: row.packageName,
2425
+ packageVersion: row.packageVersion,
2426
+ seenAt: row.seenAt,
2427
+ }));
2428
+ const inspection = this.packageRegistryService
2429
+ ? await this.packageRegistryService.inspectPackage(packageId)
2430
+ : { ok: false, reason: 'missing_package' };
2431
+ if (!inspection.ok) {
2432
+ return {
2433
+ packageId: metadata.packageId,
2434
+ name: metadata.name,
2435
+ version: metadata.version,
2436
+ uploadedAt: metadata.uploadedAt,
2437
+ packageSize: metadata.packageSize,
2438
+ status: inspection.reason === 'missing_package' ? 'missing_package' : 'invalid_package',
2439
+ seenOnNodes,
2440
+ moduleCount: 0,
2441
+ templateCount: 0,
2442
+ dataTypeCount: 0,
2443
+ modules: [],
2444
+ templates: [],
2445
+ dataTypes: [],
2446
+ };
2447
+ }
2448
+ return {
2449
+ packageId: metadata.packageId,
2450
+ name: metadata.name,
2451
+ version: metadata.version,
2452
+ uploadedAt: metadata.uploadedAt,
2453
+ packageSize: metadata.packageSize,
2454
+ status: 'decoded',
2455
+ seenOnNodes,
2456
+ moduleCount: inspection.definition.moduleCount,
2457
+ templateCount: inspection.definition.templateCount,
2458
+ dataTypeCount: inspection.definition.dataTypeCount,
2459
+ modules: [...inspection.definition.modules].sort((left, right) => left.localeCompare(right)),
2460
+ templates: [...inspection.definition.templates].sort((left, right) => left.templateId.localeCompare(right.templateId)),
2461
+ dataTypes: [...inspection.definition.dataTypes].sort((left, right) => left.typeId.localeCompare(right.typeId)),
2462
+ };
2463
+ }
2464
+ async fetchPackagesByName(packageName) {
2465
+ const packages = this.packageCacheService?.listPackagesByName(packageName) ?? [];
2466
+ if (packages.length === 0) {
2467
+ throw new Error('Package family not found');
2468
+ }
2469
+ return {
2470
+ name: packageName,
2471
+ packages,
2472
+ };
2473
+ }
2474
+ async fetchNodePackages(node) {
2475
+ const rows = this.packageCacheService?.listPackagesForNode(node.id) ?? [];
2476
+ const groupedPackages = new Map();
2477
+ for (const row of rows) {
2478
+ const packageName = row.packageName ?? row.packageId;
2479
+ const group = groupedPackages.get(packageName) ?? [];
2480
+ group.push({
2481
+ packageId: row.packageId,
2482
+ version: row.packageVersion,
2483
+ uploadedAt: row.uploadedAt,
2484
+ seenAt: row.seenAt,
2485
+ });
2486
+ groupedPackages.set(packageName, group);
2487
+ }
2488
+ return {
2489
+ nodeId: node.id,
2490
+ label: node.label,
2491
+ packagesByName: Array.from(groupedPackages.entries()).map(([packageName, packages]) => ({
2492
+ packageName,
2493
+ packages,
2494
+ })),
2495
+ };
2496
+ }
2497
+ async fetchTemplates() {
2498
+ const packageIds = (this.packageCacheService?.listPackages() ?? []).map((pkg) => pkg.packageId);
2499
+ return this.buildTemplateFilterResponse(packageIds);
2500
+ }
2501
+ async fetchNodeTemplates(node) {
2502
+ const packageIds = Array.from(new Set((this.packageCacheService?.listPackagesForNode(node.id) ?? []).map((pkg) => pkg.packageId)));
2503
+ return this.buildTemplateFilterResponse(packageIds);
2504
+ }
2505
+ async fetchActiveParties(nodes) {
2506
+ return {
2507
+ nodes: await Promise.all(nodes.map(async (node) => ({
2508
+ nodeId: node.id,
2509
+ label: node.label,
2510
+ mode: node.mode,
2511
+ parties: await this.fetchActivePartiesForNode(node),
2512
+ }))),
2513
+ };
2514
+ }
2515
+ async fetchPartyDetail(nodes, partyId) {
2516
+ const normalizedPartyId = this.normalizePartyIdentifier(partyId);
2517
+ const grpcOperationsService = this.grpcOperationsService;
2518
+ const activePartiesByNode = await Promise.all(nodes.map(async (node) => ({
2519
+ node,
2520
+ parties: await this.fetchActivePartiesForNode(node),
2521
+ })));
2522
+ const recentUpdatesByNode = await Promise.all(nodes.map(async (node) => ({
2523
+ node,
2524
+ updates: await this.fetchPartyRecentUpdatesForNode(node, normalizedPartyId, 10),
2525
+ })));
2526
+ const recentContractsByNode = await Promise.all(nodes.map(async (node) => ({
2527
+ node,
2528
+ contracts: await this.fetchPartyRecentContractsForNode(node, normalizedPartyId, 10),
2529
+ })));
2530
+ const localPartiesByNode = grpcOperationsService
2531
+ ? await Promise.all(nodes.map(async (node) => {
2532
+ try {
2533
+ return {
2534
+ node,
2535
+ parties: await grpcOperationsService.listLocalParties(node),
2536
+ };
2537
+ }
2538
+ catch {
2539
+ return {
2540
+ node,
2541
+ parties: [],
2542
+ };
2543
+ }
2544
+ }))
2545
+ : [];
2546
+ const localPartyPresenceByNodeId = new Map(localPartiesByNode.map(({ node, parties }) => [node.id, parties.includes(normalizedPartyId)]));
2547
+ const nodesById = new Map();
2548
+ for (const { node, updates } of recentUpdatesByNode) {
2549
+ if (updates.length === 0) {
2550
+ continue;
2551
+ }
2552
+ const existing = nodesById.get(node.id);
2553
+ nodesById.set(node.id, {
2554
+ nodeId: node.id,
2555
+ label: node.label,
2556
+ recentUpdateCount: updates.length,
2557
+ recentContractCount: existing?.recentContractCount ?? 0,
2558
+ });
2559
+ }
2560
+ for (const { node, contracts } of recentContractsByNode) {
2561
+ if (contracts.length === 0) {
2562
+ continue;
2563
+ }
2564
+ const existing = nodesById.get(node.id);
2565
+ nodesById.set(node.id, {
2566
+ nodeId: node.id,
2567
+ label: node.label,
2568
+ recentUpdateCount: existing?.recentUpdateCount ?? 0,
2569
+ recentContractCount: contracts.length,
2570
+ });
2571
+ }
2572
+ for (const { node, parties } of activePartiesByNode) {
2573
+ if (!parties.includes(normalizedPartyId)) {
2574
+ continue;
2575
+ }
2576
+ const existing = nodesById.get(node.id);
2577
+ nodesById.set(node.id, {
2578
+ nodeId: node.id,
2579
+ label: node.label,
2580
+ recentUpdateCount: existing?.recentUpdateCount ?? 0,
2581
+ recentContractCount: existing?.recentContractCount ?? 0,
2582
+ });
2583
+ }
2584
+ for (const { node, parties } of localPartiesByNode) {
2585
+ if (!parties.includes(normalizedPartyId)) {
2586
+ continue;
2587
+ }
2588
+ const existing = nodesById.get(node.id);
2589
+ nodesById.set(node.id, {
2590
+ nodeId: node.id,
2591
+ label: node.label,
2592
+ recentUpdateCount: existing?.recentUpdateCount ?? 0,
2593
+ recentContractCount: existing?.recentContractCount ?? 0,
2594
+ });
2595
+ }
2596
+ const recentUpdates = recentUpdatesByNode
2597
+ .flatMap(({ updates }) => updates)
2598
+ .sort((left, right) => Date.parse(right.recordTime ?? '') - Date.parse(left.recordTime ?? ''))
2599
+ .slice(0, 10);
2600
+ const recentContracts = recentContractsByNode
2601
+ .flatMap(({ contracts }) => contracts)
2602
+ .sort((left, right) => Date.parse(right.recordTime ?? '') - Date.parse(left.recordTime ?? ''))
2603
+ .slice(0, 10);
2604
+ const observedNodes = Array.from(nodesById.values()).sort((left, right) => left.label.localeCompare(right.label));
2605
+ if (observedNodes.length === 0 && recentUpdates.length === 0 && recentContracts.length === 0) {
2606
+ throw new Error('Party not found');
2607
+ }
2608
+ const partyTopologyByNode = grpcOperationsService
2609
+ ? (await Promise.all(observedNodes.map(async (observedNode) => {
2610
+ const node = nodes.find((candidate) => candidate.id === observedNode.nodeId);
2611
+ if (!node) {
2612
+ return null;
2613
+ }
2614
+ const topology = await grpcOperationsService.fetchPartyTopology(node, normalizedPartyId);
2615
+ return {
2616
+ ...topology,
2617
+ isLocalParty: localPartyPresenceByNodeId.get(node.id) ?? topology.isLocalParty ?? null,
2618
+ };
2619
+ })))
2620
+ .filter((entry) => entry !== null)
2621
+ .sort((left, right) => left.label.localeCompare(right.label))
2622
+ : [];
2623
+ return {
2624
+ partyId: normalizedPartyId,
2625
+ nodeCount: observedNodes.length,
2626
+ recentUpdateCount: recentUpdates.length,
2627
+ recentContractCount: recentContracts.length,
2628
+ nodes: observedNodes,
2629
+ recentUpdates,
2630
+ recentContracts,
2631
+ partyTopologyByNode,
2632
+ };
2633
+ }
2634
+ async fetchNamespaceDetail(nodes, namespaceId) {
2635
+ const normalizedNamespaceId = namespaceId.trim();
2636
+ const grpcOperationsService = this.grpcOperationsService;
2637
+ const activePartiesByNode = await Promise.all(nodes.map(async (node) => ({
2638
+ node,
2639
+ parties: (await this.fetchActivePartiesForNode(node)).filter((partyId) => this.extractNamespaceIdentifier(partyId) === normalizedNamespaceId),
2640
+ })));
2641
+ const localPartiesByNode = grpcOperationsService
2642
+ ? await Promise.all(nodes.map(async (node) => {
2643
+ try {
2644
+ return {
2645
+ node,
2646
+ parties: (await grpcOperationsService.listLocalParties(node)).filter((partyId) => this.extractNamespaceIdentifier(partyId) === normalizedNamespaceId),
2647
+ };
2648
+ }
2649
+ catch {
2650
+ return {
2651
+ node,
2652
+ parties: [],
2653
+ };
2654
+ }
2655
+ }))
2656
+ : [];
2657
+ const partiesById = new Map();
2658
+ const nodesById = new Map();
2659
+ for (const { node, parties } of [...activePartiesByNode, ...localPartiesByNode]) {
2660
+ for (const partyId of parties) {
2661
+ const nodeIds = partiesById.get(partyId) ?? new Set();
2662
+ nodeIds.add(node.id);
2663
+ partiesById.set(partyId, nodeIds);
2664
+ if (!nodesById.has(node.id)) {
2665
+ nodesById.set(node.id, {
2666
+ nodeId: node.id,
2667
+ label: node.label,
2668
+ recentUpdateCount: 0,
2669
+ recentContractCount: 0,
2670
+ });
2671
+ }
2672
+ }
2673
+ }
2674
+ const matchingParties = Array.from(partiesById.keys()).sort((left, right) => left.localeCompare(right));
2675
+ if (matchingParties.length === 0) {
2676
+ throw new Error('Namespace not found');
2677
+ }
2678
+ const recentUpdatesResponse = await this.fetchGlobalRecentUpdates(nodes, 10, {
2679
+ parties: matchingParties,
2680
+ partyMode: 'or',
2681
+ });
2682
+ const recentContractsResponse = await this.fetchGlobalContracts(nodes, 10, {
2683
+ parties: matchingParties,
2684
+ partyMode: 'or',
2685
+ });
2686
+ for (const update of recentUpdatesResponse.updates) {
2687
+ const existing = nodesById.get(update.nodeId);
2688
+ nodesById.set(update.nodeId, {
2689
+ nodeId: update.nodeId,
2690
+ label: update.label,
2691
+ recentUpdateCount: (existing?.recentUpdateCount ?? 0) + 1,
2692
+ recentContractCount: existing?.recentContractCount ?? 0,
2693
+ });
2694
+ for (const partyId of update.parties.filter((partyId) => this.extractNamespaceIdentifier(partyId) === normalizedNamespaceId)) {
2695
+ const nodeIds = partiesById.get(partyId) ?? new Set();
2696
+ nodeIds.add(update.nodeId);
2697
+ partiesById.set(partyId, nodeIds);
2698
+ }
2699
+ }
2700
+ for (const contract of recentContractsResponse.contracts) {
2701
+ const existing = nodesById.get(contract.nodeId);
2702
+ nodesById.set(contract.nodeId, {
2703
+ nodeId: contract.nodeId,
2704
+ label: contract.label,
2705
+ recentUpdateCount: existing?.recentUpdateCount ?? 0,
2706
+ recentContractCount: (existing?.recentContractCount ?? 0) + 1,
2707
+ });
2708
+ }
2709
+ const observedNodes = Array.from(nodesById.values()).sort((left, right) => left.label.localeCompare(right.label));
2710
+ const topologyByNode = grpcOperationsService
2711
+ ? await this.fetchNamespaceTopologyByNode(nodes, observedNodes.map((node) => node.nodeId), partiesById)
2712
+ : [];
2713
+ return {
2714
+ namespaceId: normalizedNamespaceId,
2715
+ partyCount: matchingParties.length,
2716
+ nodeCount: observedNodes.length,
2717
+ recentUpdateCount: recentUpdatesResponse.updates.length,
2718
+ recentContractCount: recentContractsResponse.contracts.length,
2719
+ nodes: observedNodes,
2720
+ recentUpdates: recentUpdatesResponse.updates,
2721
+ recentContracts: recentContractsResponse.contracts.map((contract) => ({
2722
+ ...contract,
2723
+ packageId: null,
2724
+ packageName: null,
2725
+ packageVersion: null,
2726
+ })),
2727
+ topologyByNode,
2728
+ };
2729
+ }
2730
+ async fetchNamespaceParties(nodes, namespaceId, options) {
2731
+ const normalizedNamespaceId = namespaceId.trim();
2732
+ const matchingParties = new Set();
2733
+ const activePartiesByNode = await Promise.all(nodes.map(async (node) => (await this.fetchActivePartiesForNode(node)).filter((partyId) => this.extractNamespaceIdentifier(partyId) === normalizedNamespaceId)));
2734
+ for (const parties of activePartiesByNode) {
2735
+ for (const partyId of parties) {
2736
+ matchingParties.add(partyId);
2737
+ }
2738
+ }
2739
+ if (this.grpcOperationsService) {
2740
+ const localPartiesByNode = await Promise.all(nodes.map(async (node) => {
2741
+ try {
2742
+ return (await this.grpcOperationsService?.listLocalParties(node))?.filter((partyId) => this.extractNamespaceIdentifier(partyId) === normalizedNamespaceId) ?? [];
2743
+ }
2744
+ catch {
2745
+ return [];
2746
+ }
2747
+ }));
2748
+ for (const parties of localPartiesByNode) {
2749
+ for (const partyId of parties) {
2750
+ matchingParties.add(partyId);
2751
+ }
2752
+ }
2753
+ }
2754
+ const orderedParties = Array.from(matchingParties).sort((left, right) => left.localeCompare(right));
2755
+ if (orderedParties.length === 0) {
2756
+ throw new Error('Namespace not found');
2757
+ }
2758
+ const normalizedLimit = typeof options?.limit === 'number' && Number.isFinite(options.limit) && options.limit > 0
2759
+ ? Math.trunc(options.limit)
2760
+ : 25;
2761
+ const pagedParties = this.paginateNamespacePartyIds(orderedParties, {
2762
+ limit: normalizedLimit,
2763
+ before: options?.before,
2764
+ after: options?.after,
2765
+ });
2766
+ return {
2767
+ namespaceId: normalizedNamespaceId,
2768
+ partyCount: orderedParties.length,
2769
+ limit: normalizedLimit,
2770
+ nextBefore: pagedParties.nextBefore,
2771
+ nextAfter: pagedParties.nextAfter,
2772
+ parties: pagedParties.items.map((partyId) => ({
2773
+ partyId,
2774
+ })),
2775
+ };
2776
+ }
2777
+ async fetchPartyUpdates(nodes, partyId, options) {
2778
+ return this.fetchGlobalRecentUpdates(nodes, options?.limit ?? 25, {
2779
+ before: options?.before,
2780
+ after: options?.after,
2781
+ parties: [this.normalizePartyIdentifier(partyId)],
2782
+ templates: options?.templates,
2783
+ partyMode: options?.partyMode,
2784
+ hideSplice: options?.hideSplice,
2785
+ });
2786
+ }
2787
+ async fetchGlobalContracts(nodes, limit = 25, options) {
2788
+ const normalizedLimit = typeof limit === 'number' && Number.isFinite(limit) && limit > 0
2789
+ ? Math.trunc(limit)
2790
+ : 25;
2791
+ const beforeCursor = decodeGlobalContractCursor(options?.before);
2792
+ const afterCursor = beforeCursor === null ? decodeGlobalContractCursor(options?.after) : null;
2793
+ const useAfterCursor = Boolean(afterCursor && !beforeCursor);
2794
+ const pageSize = Math.max(normalizedLimit * 2, normalizedLimit + 1);
2795
+ const nodeStates = nodes.map((node) => ({
2796
+ node,
2797
+ nextBefore: undefined,
2798
+ exhausted: false,
2799
+ }));
2800
+ const mergedContractsByKey = new Map();
2801
+ const activePartyFilters = normalizePartyFilters(options?.parties);
2802
+ const activeTemplateFilters = normalizeTemplateFilters(options?.templates);
2803
+ const partyMode = normalizePartyFilterMode(options?.partyMode);
2804
+ const hideSplice = options?.hideSplice === true;
2805
+ const filterContracts = (contracts) => contracts.filter((contract) => {
2806
+ if (beforeCursor !== null && compareGlobalMergedContracts(contract, beforeCursor) <= 0) {
2807
+ return false;
2808
+ }
2809
+ if (afterCursor !== null && compareGlobalMergedContracts(contract, afterCursor) >= 0) {
2810
+ return false;
2811
+ }
2812
+ const normalizedTemplateId = this.normalizeTemplateIdentifier(contract.templateId);
2813
+ if (activeTemplateFilters.length > 0 &&
2814
+ (!normalizedTemplateId || !activeTemplateFilters.includes(normalizedTemplateId))) {
2815
+ return false;
2816
+ }
2817
+ if (hideSplice && normalizedTemplateId?.startsWith('Splice.')) {
2818
+ return false;
2819
+ }
2820
+ return true;
2821
+ });
2822
+ let filteredContracts = [];
2823
+ let hasMoreInDirection = false;
2824
+ while (true) {
2825
+ const nodesToFetch = nodeStates.filter((state) => !state.exhausted);
2826
+ if (nodesToFetch.length === 0) {
2827
+ break;
2828
+ }
2829
+ const responses = await Promise.all(nodesToFetch.map((state) => this.fetchNodeContracts(state.node, {
2830
+ limit: pageSize,
2831
+ before: state.nextBefore,
2832
+ parties: activePartyFilters.length > 0 ? activePartyFilters : undefined,
2833
+ templates: activeTemplateFilters.length > 0 ? activeTemplateFilters : undefined,
2834
+ partyMode,
2835
+ hideSplice,
2836
+ })));
2837
+ responses.forEach((response, index) => {
2838
+ const state = nodesToFetch[index];
2839
+ for (const contract of response.contracts) {
2840
+ mergedContractsByKey.set(`${state.node.id}:${contract.contractId}`, {
2841
+ nodeId: state.node.id,
2842
+ label: state.node.label,
2843
+ contractId: contract.contractId,
2844
+ templateId: contract.templateId,
2845
+ recordTime: contract.createdRecordTime,
2846
+ });
2847
+ }
2848
+ state.nextBefore = response.nextBefore ?? undefined;
2849
+ state.exhausted = response.nextBefore === null;
2850
+ });
2851
+ const sortedContracts = Array.from(mergedContractsByKey.values()).sort(compareGlobalMergedContracts);
2852
+ filteredContracts = filterContracts(sortedContracts);
2853
+ hasMoreInDirection = filteredContracts.length > normalizedLimit;
2854
+ if (hasMoreInDirection || nodeStates.every((state) => state.exhausted)) {
2855
+ break;
2856
+ }
2857
+ }
2858
+ const contracts = filteredContracts.slice(0, normalizedLimit);
2859
+ return {
2860
+ limit: normalizedLimit,
2861
+ nextBefore: contracts.length > 0 && (useAfterCursor || hasMoreInDirection)
2862
+ ? encodeGlobalContractCursor(contracts[contracts.length - 1])
2863
+ : null,
2864
+ nextAfter: contracts.length === 0
2865
+ ? null
2866
+ : useAfterCursor
2867
+ ? hasMoreInDirection
2868
+ ? encodeGlobalContractCursor(contracts[0])
2869
+ : null
2870
+ : beforeCursor
2871
+ ? encodeGlobalContractCursor(contracts[0])
2872
+ : null,
2873
+ contracts,
2874
+ };
2875
+ }
2876
+ async fetchPartyContracts(nodes, partyId, options) {
2877
+ const normalizedPartyId = this.normalizePartyIdentifier(partyId);
2878
+ const normalizedLimit = typeof options?.limit === 'number' && Number.isFinite(options.limit) && options.limit > 0
2879
+ ? Math.trunc(options.limit)
2880
+ : 25;
2881
+ const beforeCursor = decodeGlobalContractCursor(options?.before);
2882
+ const afterCursor = beforeCursor === null ? decodeGlobalContractCursor(options?.after) : null;
2883
+ const useAfterCursor = Boolean(afterCursor && !beforeCursor);
2884
+ const pageSize = Math.max(normalizedLimit * 2, normalizedLimit + 1);
2885
+ const nodeStates = nodes.map((node) => ({
2886
+ node,
2887
+ nextBefore: undefined,
2888
+ exhausted: false,
2889
+ }));
2890
+ const mergedContractsByKey = new Map();
2891
+ const activeTemplateFilters = normalizeTemplateFilters(options?.templates);
2892
+ const hideSplice = options?.hideSplice === true;
2893
+ const filterContracts = (contracts) => {
2894
+ return contracts.filter((contract) => {
2895
+ if (beforeCursor !== null && compareGlobalMergedContracts(contract, beforeCursor) <= 0) {
2896
+ return false;
2897
+ }
2898
+ if (afterCursor !== null && compareGlobalMergedContracts(contract, afterCursor) >= 0) {
2899
+ return false;
2900
+ }
2901
+ const normalizedTemplateId = this.normalizeTemplateIdentifier(contract.templateId);
2902
+ if (activeTemplateFilters.length > 0 &&
2903
+ (!normalizedTemplateId || !activeTemplateFilters.includes(normalizedTemplateId))) {
2904
+ return false;
2905
+ }
2906
+ if (hideSplice && normalizedTemplateId?.startsWith('Splice.')) {
2907
+ return false;
2908
+ }
2909
+ return true;
2910
+ });
2911
+ };
2912
+ let filteredContracts = [];
2913
+ let hasMoreInDirection = false;
2914
+ while (true) {
2915
+ const nodesToFetch = nodeStates.filter((state) => !state.exhausted);
2916
+ if (nodesToFetch.length === 0) {
2917
+ break;
2918
+ }
2919
+ const responses = await Promise.all(nodesToFetch.map((state) => this.fetchPartyContractsForNode(state.node, normalizedPartyId, {
2920
+ limit: pageSize,
2921
+ before: state.nextBefore,
2922
+ })));
2923
+ responses.forEach((response, index) => {
2924
+ const state = nodesToFetch[index];
2925
+ for (const contract of response.contracts) {
2926
+ mergedContractsByKey.set(`${contract.nodeId}:${contract.contractId}`, contract);
2927
+ }
2928
+ state.nextBefore = response.nextBefore ?? undefined;
2929
+ state.exhausted = response.nextBefore === null;
2930
+ });
2931
+ const sortedContracts = Array.from(mergedContractsByKey.values()).sort(compareGlobalMergedContracts);
2932
+ filteredContracts = filterContracts(sortedContracts);
2933
+ hasMoreInDirection = filteredContracts.length > normalizedLimit;
2934
+ if (hasMoreInDirection || nodeStates.every((state) => state.exhausted)) {
2935
+ break;
2936
+ }
2937
+ }
2938
+ const contracts = filteredContracts.slice(0, normalizedLimit);
2939
+ return {
2940
+ limit: normalizedLimit,
2941
+ nextBefore: contracts.length > 0 && (useAfterCursor || hasMoreInDirection)
2942
+ ? encodeGlobalContractCursor(contracts[contracts.length - 1])
2943
+ : null,
2944
+ nextAfter: contracts.length === 0
2945
+ ? null
2946
+ : useAfterCursor
2947
+ ? hasMoreInDirection
2948
+ ? encodeGlobalContractCursor(contracts[0])
2949
+ : null
2950
+ : beforeCursor
2951
+ ? encodeGlobalContractCursor(contracts[0])
2952
+ : null,
2953
+ contracts,
2954
+ };
2955
+ }
2956
+ async fetchTokens(nodes, limit = 25, options) {
2957
+ const refreshResults = await Promise.allSettled(nodes.map(async (node) => ({
2958
+ node,
2959
+ tokens: await this.loadCachedObservedTokens(node),
2960
+ })));
2961
+ const successfulTokens = refreshResults
2962
+ .filter((result) => result.status === 'fulfilled')
2963
+ .flatMap((result) => result.value.tokens);
2964
+ if (successfulTokens.length === 0) {
2965
+ const firstFailure = refreshResults.find((result) => result.status === 'rejected');
2966
+ if (firstFailure) {
2967
+ throw firstFailure.reason;
2968
+ }
2969
+ }
2970
+ const dedupedTokens = new Map();
2971
+ for (const token of successfulTokens) {
2972
+ if (!dedupedTokens.has(token.tokenId)) {
2973
+ dedupedTokens.set(token.tokenId, token);
2974
+ }
2975
+ }
2976
+ const filteredTokens = this.filterTokens(Array.from(dedupedTokens.values()), options);
2977
+ return this.paginateTokens(filteredTokens.sort(compareGlobalTokens), limit, options);
2978
+ }
2979
+ async fetchLatestTokenTransfers(nodes, limit = 25, options) {
2980
+ const mergedTransfers = this.filterTokenTransfersByParties(await this.loadMergedTokenTransfers(nodes), options);
2981
+ return this.paginateTokenTransfers(mergedTransfers, limit, options);
2982
+ }
2983
+ async fetchTokenTransfers(nodes, tokenId, limit = 25, options) {
2984
+ const normalizedTokenId = this.normalizeTokenId(tokenId);
2985
+ await this.findObservedToken(nodes, normalizedTokenId);
2986
+ const mergedTransfers = this.filterTokenTransfersByParties(await this.loadMergedTokenTransfers(nodes), options);
2987
+ const directTransfers = mergedTransfers.filter((transfer) => transfer.tokenId === normalizedTokenId);
2988
+ if (directTransfers.length > 0) {
2989
+ return this.paginateTokenTransfers(directTransfers, limit, options);
2990
+ }
2991
+ const recoveredTransfers = this.filterTokenTransfersByParties(await this.recoverTokenTransfersFromActiveHolders(nodes, normalizedTokenId), options);
2992
+ return this.paginateTokenTransfers(recoveredTransfers, limit, options);
2993
+ }
2994
+ async fetchTokenTransferDetail(nodes, transferId) {
2995
+ const normalizedTransferId = this.normalizeUpdateId(transferId);
2996
+ const transfer = (await this.loadMergedTokenTransfers(nodes))
2997
+ .sort(compareGlobalTokenTransfers)
2998
+ .find((entry) => entry.rowId === normalizedTransferId || entry.updateId === normalizedTransferId);
2999
+ if (!transfer) {
3000
+ throw new Error('Token transfer not found');
3001
+ }
3002
+ return transfer;
3003
+ }
3004
+ async fetchTokenDetail(nodes, tokenId) {
3005
+ const normalizedTokenId = this.normalizeTokenId(tokenId);
3006
+ const token = await this.findObservedToken(nodes, normalizedTokenId);
3007
+ const transfersResponse = await this.fetchTokenTransfers(nodes, normalizedTokenId, 25);
3008
+ return {
3009
+ token,
3010
+ transfers: transfersResponse.transfers,
3011
+ };
3012
+ }
3013
+ async fetchTokenHolders(nodes, tokenId, limit = 25, options) {
3014
+ const normalizedTokenId = this.normalizeTokenId(tokenId);
3015
+ await this.findObservedToken(nodes, normalizedTokenId);
3016
+ const refreshResults = await Promise.allSettled(nodes.map(async (node) => ({
3017
+ node,
3018
+ holders: await this.loadCachedTokenHolders(node),
3019
+ })));
3020
+ const successfulHolders = refreshResults
3021
+ .filter((result) => result.status === 'fulfilled')
3022
+ .flatMap((result) => result.value.holders)
3023
+ .filter((holder) => holder.tokenId === normalizedTokenId);
3024
+ if (successfulHolders.length === 0) {
3025
+ const firstFailure = refreshResults.find((result) => result.status === 'rejected');
3026
+ if (firstFailure) {
3027
+ throw firstFailure.reason;
3028
+ }
3029
+ }
3030
+ return this.paginateTokenHolders(normalizedTokenId, this.mergeTokenHolders(successfulHolders), limit, options);
3031
+ }
3032
+ paginateTokenHolders(tokenId, holders, limit = 25, options) {
3033
+ const normalizedLimit = Number.isFinite(limit) && Number(limit) > 0 ? Math.trunc(Number(limit)) : 25;
3034
+ const beforeCursor = decodeGlobalTokenHolderCursor(options?.before);
3035
+ const afterCursor = beforeCursor === null ? decodeGlobalTokenHolderCursor(options?.after) : null;
3036
+ const useAfterCursor = Boolean(afterCursor && !beforeCursor);
3037
+ const filteredHolders = holders.filter((holder) => {
3038
+ if (beforeCursor !== null) {
3039
+ return compareGlobalTokenHolders(holder, beforeCursor) > 0;
3040
+ }
3041
+ if (afterCursor !== null) {
3042
+ return compareGlobalTokenHolders(holder, afterCursor) < 0;
3043
+ }
3044
+ return true;
3045
+ });
3046
+ const hasMoreInDirection = filteredHolders.length > normalizedLimit;
3047
+ const pagedHolders = filteredHolders.slice(0, normalizedLimit);
3048
+ return {
3049
+ tokenId,
3050
+ limit: normalizedLimit,
3051
+ nextBefore: pagedHolders.length > 0 && (useAfterCursor || hasMoreInDirection)
3052
+ ? encodeGlobalTokenHolderCursor(pagedHolders[pagedHolders.length - 1])
3053
+ : null,
3054
+ nextAfter: pagedHolders.length === 0
3055
+ ? null
3056
+ : useAfterCursor
3057
+ ? hasMoreInDirection
3058
+ ? encodeGlobalTokenHolderCursor(pagedHolders[0])
3059
+ : null
3060
+ : beforeCursor
3061
+ ? encodeGlobalTokenHolderCursor(pagedHolders[0])
3062
+ : null,
3063
+ holders: pagedHolders,
3064
+ };
3065
+ }
3066
+ paginateTokens(tokens, limit = 25, options) {
3067
+ const normalizedLimit = Number.isFinite(limit) && Number(limit) > 0 ? Math.trunc(Number(limit)) : 25;
3068
+ const beforeCursor = decodeGlobalTokenCursor(options?.before);
3069
+ const afterCursor = beforeCursor === null ? decodeGlobalTokenCursor(options?.after) : null;
3070
+ const useAfterCursor = Boolean(afterCursor && !beforeCursor);
3071
+ const filteredTokens = tokens.filter((token) => {
3072
+ if (beforeCursor !== null) {
3073
+ return compareGlobalTokens(token, beforeCursor) > 0;
3074
+ }
3075
+ if (afterCursor !== null) {
3076
+ return compareGlobalTokens(token, afterCursor) < 0;
3077
+ }
3078
+ return true;
3079
+ });
3080
+ const hasMoreInDirection = filteredTokens.length > normalizedLimit;
3081
+ const pagedTokens = filteredTokens.slice(0, normalizedLimit);
3082
+ return {
3083
+ limit: normalizedLimit,
3084
+ nextBefore: pagedTokens.length > 0 && (useAfterCursor || hasMoreInDirection)
3085
+ ? encodeGlobalTokenCursor(pagedTokens[pagedTokens.length - 1])
3086
+ : null,
3087
+ nextAfter: pagedTokens.length === 0
3088
+ ? null
3089
+ : useAfterCursor
3090
+ ? hasMoreInDirection
3091
+ ? encodeGlobalTokenCursor(pagedTokens[0])
3092
+ : null
3093
+ : beforeCursor
3094
+ ? encodeGlobalTokenCursor(pagedTokens[0])
3095
+ : null,
3096
+ tokens: pagedTokens,
3097
+ };
3098
+ }
3099
+ filterTokens(tokens, options) {
3100
+ const activeNameFilters = normalizeTokenTextFilters(options?.names);
3101
+ const activeExcludedNameFilters = normalizeTokenTextFilters(options?.excludeNames);
3102
+ const activeIssuerFilters = normalizeTokenIssuerFilters(options?.issuers);
3103
+ if (activeNameFilters.length === 0
3104
+ && activeExcludedNameFilters.length === 0
3105
+ && activeIssuerFilters.length === 0) {
3106
+ return tokens;
3107
+ }
3108
+ return tokens.filter((token) => {
3109
+ const tokenName = token.name.trim().toLowerCase();
3110
+ const tokenSymbol = token.symbol?.trim().toLowerCase() ?? '';
3111
+ const matchesNameFilter = activeNameFilters.length === 0
3112
+ || activeNameFilters.some((filterValue) => tokenName.includes(filterValue) || tokenSymbol.includes(filterValue));
3113
+ if (!matchesNameFilter) {
3114
+ return false;
3115
+ }
3116
+ const matchesExcludedNameFilter = activeExcludedNameFilters.some((filterValue) => tokenName.includes(filterValue) || tokenSymbol.includes(filterValue));
3117
+ if (matchesExcludedNameFilter) {
3118
+ return false;
3119
+ }
3120
+ if (activeIssuerFilters.length === 0) {
3121
+ return true;
3122
+ }
3123
+ return token.issuer !== null && activeIssuerFilters.includes(token.issuer);
3124
+ });
3125
+ }
3126
+ async loadMergedTokenTransfers(nodes) {
3127
+ const refreshResults = await Promise.allSettled(nodes.map(async (node) => ({
3128
+ node,
3129
+ transfers: await this.loadCachedTokenTransfers(node),
3130
+ })));
3131
+ const successfulTransfers = refreshResults
3132
+ .filter((result) => result.status === 'fulfilled')
3133
+ .flatMap((result) => result.value.transfers);
3134
+ if (successfulTransfers.length === 0) {
3135
+ const firstFailure = refreshResults.find((result) => result.status === 'rejected');
3136
+ if (firstFailure) {
3137
+ throw firstFailure.reason;
3138
+ }
3139
+ }
3140
+ return this.mergeTokenTransfers(successfulTransfers);
3141
+ }
3142
+ async recoverTokenTransfersFromActiveHolders(nodes, tokenId) {
3143
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
3144
+ const refreshResults = await Promise.allSettled(nodes.map(async (node) => ({
3145
+ node,
3146
+ holders: await this.loadCachedTokenHolders(node),
3147
+ })));
3148
+ const candidateHolders = refreshResults
3149
+ .filter((result) => result.status === 'fulfilled')
3150
+ .flatMap((result) => result.value.holders)
3151
+ .filter((holder) => holder.tokenId === tokenId && holder.contractId !== null);
3152
+ const recoveredTransfers = [];
3153
+ const seenContracts = new Set();
3154
+ for (const holder of candidateHolders) {
3155
+ const node = nodeById.get(holder.nodeId);
3156
+ if (!node || !holder.contractId) {
3157
+ continue;
3158
+ }
3159
+ const dedupeKey = `${holder.nodeId}\u0000${holder.contractId}`;
3160
+ if (seenContracts.has(dedupeKey)) {
3161
+ continue;
3162
+ }
3163
+ seenContracts.add(dedupeKey);
3164
+ const client = this.clientFactory.getClient(node);
3165
+ const contractResult = await client.query(contractDetailQuery(holder.contractId));
3166
+ const contractRow = contractResult.rows[0];
3167
+ if (!contractRow || typeof contractRow.created_update_id !== 'string') {
3168
+ continue;
3169
+ }
3170
+ const events = await this.fetchEventsByUpdateId(node, client.query.bind(client), contractRow.created_update_id);
3171
+ const inferredTransfers = this.inferCip112TokenMovements(node, {
3172
+ update_id: contractRow.created_update_id,
3173
+ event_offset: contractRow.created_event_offset,
3174
+ record_time: contractRow.created_record_time,
3175
+ }, events).filter((transfer) => transfer.tokenId === tokenId);
3176
+ recoveredTransfers.push(...inferredTransfers);
3177
+ }
3178
+ return this.mergeTokenTransfers(recoveredTransfers);
3179
+ }
3180
+ paginateTokenTransfers(transfers, limit = 25, options) {
3181
+ const normalizedLimit = Number.isFinite(limit) && Number(limit) > 0 ? Math.trunc(Number(limit)) : 25;
3182
+ const beforeCursor = decodeGlobalTokenTransferCursor(options?.before);
3183
+ const afterCursor = beforeCursor === null ? decodeGlobalTokenTransferCursor(options?.after) : null;
3184
+ const useAfterCursor = Boolean(afterCursor && !beforeCursor);
3185
+ const filteredTransfers = transfers
3186
+ .sort(compareGlobalTokenTransfers)
3187
+ .filter((transfer) => {
3188
+ if (beforeCursor !== null) {
3189
+ return compareGlobalTokenTransfers(transfer, beforeCursor) > 0;
3190
+ }
3191
+ if (afterCursor !== null) {
3192
+ return compareGlobalTokenTransfers(transfer, afterCursor) < 0;
3193
+ }
3194
+ return true;
3195
+ });
3196
+ const hasMoreInDirection = filteredTransfers.length > normalizedLimit;
3197
+ const pagedTransfers = filteredTransfers.slice(0, normalizedLimit);
3198
+ return {
3199
+ limit: normalizedLimit,
3200
+ nextBefore: pagedTransfers.length > 0 && (useAfterCursor || hasMoreInDirection)
3201
+ ? encodeGlobalTokenTransferCursor(pagedTransfers[pagedTransfers.length - 1])
3202
+ : null,
3203
+ nextAfter: pagedTransfers.length === 0
3204
+ ? null
3205
+ : useAfterCursor
3206
+ ? hasMoreInDirection
3207
+ ? encodeGlobalTokenTransferCursor(pagedTransfers[0])
3208
+ : null
3209
+ : beforeCursor
3210
+ ? encodeGlobalTokenTransferCursor(pagedTransfers[0])
3211
+ : null,
3212
+ transfers: pagedTransfers,
3213
+ };
3214
+ }
3215
+ filterTokenTransfersByParties(transfers, options) {
3216
+ const activeFromParties = normalizePartyFilters(options?.fromParties);
3217
+ const activeToParties = normalizePartyFilters(options?.toParties);
3218
+ const amountGt = options?.amountGt?.trim() || null;
3219
+ const amountLt = options?.amountLt?.trim() || null;
3220
+ if (activeFromParties.length === 0 &&
3221
+ activeToParties.length === 0 &&
3222
+ amountGt === null &&
3223
+ amountLt === null) {
3224
+ return transfers;
3225
+ }
3226
+ return transfers.filter((transfer) => {
3227
+ const matchesFrom = activeFromParties.length === 0 ||
3228
+ (transfer.sender !== null && activeFromParties.includes(transfer.sender));
3229
+ const matchesTo = activeToParties.length === 0 ||
3230
+ (transfer.receiver !== null && activeToParties.includes(transfer.receiver));
3231
+ const matchesAmountGt = amountGt === null ||
3232
+ (transfer.amount !== null && this.compareNumericStrings(transfer.amount, amountGt) > 0);
3233
+ const matchesAmountLt = amountLt === null ||
3234
+ (transfer.amount !== null && this.compareNumericStrings(transfer.amount, amountLt) < 0);
3235
+ return matchesFrom && matchesTo && matchesAmountGt && matchesAmountLt;
3236
+ });
3237
+ }
3238
+ async fetchUpdateDetail(node, eventOffset) {
3239
+ const client = this.clientFactory.getClient(node);
3240
+ const detailResult = await client.query(singleUpdateQuery(eventOffset));
3241
+ const detailRows = detailResult.rows ?? [];
3242
+ const detailRow = detailRows[0];
3243
+ if (!detailRow) {
3244
+ throw new Error('Update not found');
3245
+ }
3246
+ const rawUpdateId = this.extractRawUpdateId(detailRow);
3247
+ const matchedEventOffset = this.extractEventOffset(detailRow);
3248
+ const canonicalUpdateId = this.normalizeUpdateId(rawUpdateId);
3249
+ const partiesByUpdateId = await this.fetchPartiesByUpdateId(client.query.bind(client), [rawUpdateId]);
3250
+ const events = await this.fetchEventsByUpdateId(node, client.query.bind(client), rawUpdateId);
3251
+ const exerciseData = this.shouldResolveRewardCoupon(events)
3252
+ ? await this.fetchRewardCouponDetails(client.query.bind(client), rawUpdateId)
3253
+ : null;
3254
+ return {
3255
+ nodeId: node.id,
3256
+ label: node.label,
3257
+ eventOffset: matchedEventOffset,
3258
+ updateId: canonicalUpdateId,
3259
+ recordTime: this.extractIsoRecordTime(detailRow),
3260
+ parties: partiesByUpdateId.get(canonicalUpdateId) ?? [],
3261
+ meta: this.extractMeta(detailRow),
3262
+ events: this.attachRewardCouponDetails(events, exerciseData),
3263
+ };
3264
+ }
3265
+ async fetchContractDetail(node, contractId) {
3266
+ const client = this.clientFactory.getClient(node);
3267
+ const result = await client.query(contractDetailQuery(contractId));
3268
+ const row = result.rows[0];
3269
+ if (!row) {
3270
+ throw new Error('Contract not found');
3271
+ }
3272
+ const templateId = this.normalizeTemplateIdentifier(row.template_id);
3273
+ const packageId = typeof row.package_id === 'string' ? row.package_id : null;
3274
+ const packageMetadata = packageId && this.packageCacheService ? this.packageCacheService.getPackage(packageId) : null;
3275
+ return {
3276
+ nodeId: node.id,
3277
+ label: node.label,
3278
+ contractId: row.contract_id,
3279
+ templateId,
3280
+ packageId,
3281
+ packageName: packageMetadata?.name ?? null,
3282
+ packageVersion: packageMetadata?.version ?? null,
3283
+ createdUpdateId: typeof row.created_update_id === 'string' ? row.created_update_id : null,
3284
+ createdEventOffset: this.normalizeOptionalScalar(row.created_event_offset),
3285
+ createdRecordTime: typeof row.created_record_time === 'string' ? row.created_record_time : null,
3286
+ archivedUpdateId: typeof row.archived_update_id === 'string' ? row.archived_update_id : null,
3287
+ archivedEventOffset: this.normalizeOptionalScalar(row.archived_event_offset),
3288
+ archivedRecordTime: typeof row.archived_record_time === 'string' ? row.archived_record_time : null,
3289
+ contractData: await this.decodeContractData(node, packageId, templateId, row.contract_instance),
3290
+ };
3291
+ }
3292
+ async querySummary(query) {
3293
+ try {
3294
+ return await query(ACTIVE_QUERY);
3295
+ }
3296
+ catch (error) {
3297
+ if (!this.shouldFallbackToParticipantTables(error)) {
3298
+ throw error;
3299
+ }
3300
+ }
3301
+ return query(PARTICIPANT_FALLBACK_QUERY);
3302
+ }
3303
+ async fetchPartyRecentUpdatesForNode(node, partyId, limit) {
3304
+ const client = this.clientFactory.getClient(node);
3305
+ let rows = [];
3306
+ try {
3307
+ const result = await client.query(partyRecentUpdatesQuery(partyId, limit));
3308
+ rows = result.rows ?? [];
3309
+ }
3310
+ catch (error) {
3311
+ if (!this.shouldFallbackToNormalizedEventTables(error)) {
3312
+ throw error;
3313
+ }
3314
+ const result = await client.query(normalizedPartyRecentUpdatesQuery(partyId, limit));
3315
+ rows = result.rows ?? [];
3316
+ }
3317
+ const rawUpdateIds = rows.map((row) => row.update_id);
3318
+ const partiesByUpdateId = rawUpdateIds.length > 0
3319
+ ? await this.fetchPartiesByUpdateId(client.query.bind(client), rawUpdateIds)
3320
+ : new Map();
3321
+ return rows.map((row) => {
3322
+ const updateId = this.normalizeUpdateId(row.update_id);
3323
+ return {
3324
+ nodeId: node.id,
3325
+ label: node.label,
3326
+ eventOffset: this.extractEventOffset(row),
3327
+ updateId,
3328
+ recordTime: typeof row.record_time === 'string' ? row.record_time : null,
3329
+ parties: partiesByUpdateId.get(updateId) ?? [],
3330
+ };
3331
+ });
3332
+ }
3333
+ async fetchPartyRecentContractsForNode(node, partyId, limit) {
3334
+ const response = await this.fetchPartyContractsForNode(node, partyId, { limit });
3335
+ return response.contracts;
3336
+ }
3337
+ async fetchPartyContractsForNode(node, partyId, options) {
3338
+ const client = this.clientFactory.getClient(node);
3339
+ const normalizedLimit = typeof options?.limit === 'number' && Number.isFinite(options.limit) && options.limit > 0
3340
+ ? Math.trunc(options.limit)
3341
+ : 25;
3342
+ const before = options?.before;
3343
+ const after = options?.after;
3344
+ const useAfterCursor = Boolean(after && !before);
3345
+ let rows = [];
3346
+ try {
3347
+ const result = await client.query(partyRecentContractsQuery(partyId, normalizedLimit, before, after));
3348
+ rows = result.rows ?? [];
3349
+ }
3350
+ catch (error) {
3351
+ if (!this.shouldFallbackToNormalizedEventTables(error)) {
3352
+ throw error;
3353
+ }
3354
+ const result = await client.query(normalizedPartyRecentContractsQuery(partyId, normalizedLimit, before, after));
3355
+ rows = result.rows ?? [];
3356
+ }
3357
+ const hasMoreInQuery = rows.length > normalizedLimit;
3358
+ const trimmedRows = rows.slice(0, normalizedLimit);
3359
+ const orderedContracts = (useAfterCursor ? [...trimmedRows].reverse() : trimmedRows)
3360
+ .filter((row) => typeof row.contract_id === 'string')
3361
+ .map((row) => {
3362
+ const packageId = this.normalizePackageIdentifier(row.package_id ?? null);
3363
+ const packageMetadata = packageId && this.packageCacheService ? this.packageCacheService.getPackage(packageId) : null;
3364
+ return {
3365
+ nodeId: node.id,
3366
+ label: node.label,
3367
+ contractId: row.contract_id,
3368
+ templateId: this.normalizeTemplateIdentifier(row.template_id),
3369
+ packageId,
3370
+ packageName: packageMetadata?.name ?? null,
3371
+ packageVersion: packageMetadata?.version ?? null,
3372
+ recordTime: typeof row.record_time === 'string' ? row.record_time : null,
3373
+ createdEventOffset: this.normalizeOptionalScalar(row.created_event_offset),
3374
+ };
3375
+ });
3376
+ return {
3377
+ nextBefore: orderedContracts.length > 0 && (useAfterCursor || hasMoreInQuery)
3378
+ ? orderedContracts[orderedContracts.length - 1]?.createdEventOffset ?? null
3379
+ : null,
3380
+ nextAfter: orderedContracts.length === 0
3381
+ ? null
3382
+ : useAfterCursor
3383
+ ? hasMoreInQuery
3384
+ ? orderedContracts[0]?.createdEventOffset ?? null
3385
+ : null
3386
+ : before
3387
+ ? orderedContracts[0]?.createdEventOffset ?? null
3388
+ : null,
3389
+ contracts: orderedContracts.map(({ createdEventOffset: _, ...contract }) => contract),
3390
+ };
3391
+ }
3392
+ shouldFallbackToParticipantTables(error) {
3393
+ if (!(error instanceof Error)) {
3394
+ return false;
3395
+ }
3396
+ const code = error.code;
3397
+ return code === '42883' || error.message.includes('function active() does not exist');
3398
+ }
3399
+ shouldFallbackToNormalizedEventTables(error) {
3400
+ if (!(error instanceof Error)) {
3401
+ return false;
3402
+ }
3403
+ const code = error.code;
3404
+ return code === '42P01' || error.message.includes('does not exist');
3405
+ }
3406
+ async fetchActivePartiesForNode(node) {
3407
+ const client = this.clientFactory.getClient(node);
3408
+ try {
3409
+ const result = await client.query(activePartiesQuery());
3410
+ const row = result.rows[0];
3411
+ return this.normalizeParties(row?.parties ?? null);
3412
+ }
3413
+ catch (error) {
3414
+ if (!this.shouldFallbackToNormalizedEventTables(error)) {
3415
+ throw error;
3416
+ }
3417
+ }
3418
+ const fallbackResult = await client.query(normalizedActivePartiesQuery());
3419
+ const fallbackRow = fallbackResult.rows[0];
3420
+ return this.normalizeParties(fallbackRow?.parties ?? null);
3421
+ }
3422
+ async fetchPartiesByUpdateId(query, updateIds) {
3423
+ try {
3424
+ const result = await query(recentUpdatePartiesQuery(updateIds));
3425
+ const rows = result.rows ?? [];
3426
+ return new Map(rows.map((row) => [
3427
+ this.normalizeUpdateId(row.update_id),
3428
+ this.normalizeParties(row.parties),
3429
+ ]));
3430
+ }
3431
+ catch (error) {
3432
+ if (!this.shouldFallbackToNormalizedEventTables(error)) {
3433
+ return new Map();
3434
+ }
3435
+ }
3436
+ try {
3437
+ const fallbackResult = await query(normalizedRecentUpdatePartiesQuery(updateIds));
3438
+ const fallbackRows = fallbackResult.rows ?? [];
3439
+ return new Map(fallbackRows.map((row) => [
3440
+ this.normalizeUpdateId(row.update_id),
3441
+ this.normalizeParties(row.parties),
3442
+ ]));
3443
+ }
3444
+ catch {
3445
+ return new Map();
3446
+ }
3447
+ }
3448
+ async loadCachedObservedTokens(node) {
3449
+ const cached = this.observedTokensByNode.get(node.id);
3450
+ if (cached && Date.now() - cached.cachedAt < TOKEN_TRANSFER_CACHE_TTL_MS) {
3451
+ return cached.tokens;
3452
+ }
3453
+ const useGrpcHoldingViews = node.mode === 'pqs_with_grpc' && this.grpcOperationsService !== undefined;
3454
+ const [pqsTokens, grpcTokens] = await Promise.all([
3455
+ this.fetchObservedTokensForNode(node, TOKEN_TRANSFER_CACHE_LIMIT, {
3456
+ includeCip112: !useGrpcHoldingViews,
3457
+ }),
3458
+ useGrpcHoldingViews
3459
+ ? this.grpcOperationsService.fetchHoldingV2Tokens(node)
3460
+ : Promise.resolve([]),
3461
+ ]);
3462
+ const dedupedTokens = new Map();
3463
+ for (const token of [...pqsTokens, ...grpcTokens]) {
3464
+ if (!dedupedTokens.has(token.tokenId)) {
3465
+ dedupedTokens.set(token.tokenId, token);
3466
+ }
3467
+ }
3468
+ const tokens = Array.from(dedupedTokens.values()).sort(compareGlobalTokens);
3469
+ this.observedTokensByNode.set(node.id, {
3470
+ cachedAt: Date.now(),
3471
+ tokens,
3472
+ });
3473
+ return tokens;
3474
+ }
3475
+ async fetchObservedTokensForNode(node, limit, options) {
3476
+ const client = this.clientFactory.getClient(node);
3477
+ const includeCip112 = options?.includeCip112 ?? true;
3478
+ const result = await client.query(tokenRowsQuery(limit, includeCip112 ? TOKEN_DISCOVERY_TEMPLATE_IDS : TOKEN_DISCOVERY_NON_CIP112_TEMPLATE_IDS, includeCip112 ? TOKEN_DISCOVERY_TEMPLATE_PATTERNS : TOKEN_DISCOVERY_NON_CIP112_TEMPLATE_PATTERNS));
3479
+ const rows = result.rows ?? [];
3480
+ const dedupedTokens = new Map();
3481
+ for (const row of rows) {
3482
+ const token = await this.normalizeObservedTokenRow(node, row);
3483
+ if (token && !dedupedTokens.has(token.tokenId)) {
3484
+ dedupedTokens.set(token.tokenId, token);
3485
+ }
3486
+ }
3487
+ return Array.from(dedupedTokens.values()).sort((left, right) => left.name.localeCompare(right.name) || left.tokenId.localeCompare(right.tokenId));
3488
+ }
3489
+ async findObservedToken(nodes, tokenId) {
3490
+ const refreshResults = await Promise.allSettled(nodes.map(async (node) => ({
3491
+ node,
3492
+ tokens: await this.loadCachedObservedTokens(node),
3493
+ })));
3494
+ const successfulTokens = refreshResults
3495
+ .filter((result) => result.status === 'fulfilled')
3496
+ .flatMap((result) => result.value.tokens);
3497
+ if (successfulTokens.length === 0) {
3498
+ const firstFailure = refreshResults.find((result) => result.status === 'rejected');
3499
+ if (firstFailure) {
3500
+ throw firstFailure.reason;
3501
+ }
3502
+ }
3503
+ const token = successfulTokens.find((candidate) => candidate.tokenId === tokenId);
3504
+ if (!token) {
3505
+ throw new Error('Token not found');
3506
+ }
3507
+ return token;
3508
+ }
3509
+ async normalizeObservedTokenRow(node, row) {
3510
+ const templateId = this.normalizeTemplateIdentifier(row.template_id);
3511
+ if (templateId === CANTON_COIN_TRANSFER_TEMPLATE_ID ||
3512
+ templateId === CANTON_COIN_AMULET_TEMPLATE_ID) {
3513
+ return {
3514
+ tokenId: CANTON_COIN_TOKEN_ID,
3515
+ name: CANTON_COIN_TOKEN_NAME,
3516
+ symbol: null,
3517
+ issuer: null,
3518
+ source: 'pqs',
3519
+ };
3520
+ }
3521
+ if (!this.isSupportedObservedTokenTemplate(templateId)) {
3522
+ return null;
3523
+ }
3524
+ const decoded = await this.decodeContractData(node, this.normalizePackageIdentifier(row.package_id), templateId, row.contract_instance);
3525
+ return this.extractObservedTokenSummary(templateId, decoded);
3526
+ }
3527
+ async loadCachedTokenHolders(node) {
3528
+ const cached = this.tokenHoldersByNode.get(node.id);
3529
+ if (cached && Date.now() - cached.cachedAt < TOKEN_TRANSFER_CACHE_TTL_MS) {
3530
+ return cached.holders;
3531
+ }
3532
+ const useGrpcHoldingViews = node.mode === 'pqs_with_grpc' && this.grpcOperationsService !== undefined;
3533
+ const [pqsHolders, grpcHolders] = await Promise.all([
3534
+ this.fetchTokenHoldersForNode(node, TOKEN_TRANSFER_CACHE_LIMIT, {
3535
+ includeCip112: !useGrpcHoldingViews,
3536
+ }),
3537
+ useGrpcHoldingViews
3538
+ ? this.grpcOperationsService.fetchHoldingV2TokenHolders(node)
3539
+ : Promise.resolve([]),
3540
+ ]);
3541
+ const holders = [
3542
+ ...pqsHolders,
3543
+ ...grpcHolders.map((holder) => ({
3544
+ contractId: holder.contractId,
3545
+ nodeId: holder.nodeId,
3546
+ label: holder.label,
3547
+ tokenId: holder.tokenId,
3548
+ partyId: holder.partyId,
3549
+ amount: holder.amount,
3550
+ })),
3551
+ ];
3552
+ this.tokenHoldersByNode.set(node.id, {
3553
+ cachedAt: Date.now(),
3554
+ holders,
3555
+ });
3556
+ return holders;
3557
+ }
3558
+ async fetchTokenHoldersForNode(node, limit, options) {
3559
+ const client = this.clientFactory.getClient(node);
3560
+ const includeCip112 = options?.includeCip112 ?? true;
3561
+ const result = await client.query(tokenRowsQuery(limit, includeCip112 ? TOKEN_HOLDER_TEMPLATE_IDS : TOKEN_HOLDER_NON_CIP112_TEMPLATE_IDS, includeCip112 ? TOKEN_HOLDER_TEMPLATE_PATTERNS : TOKEN_HOLDER_NON_CIP112_TEMPLATE_PATTERNS));
3562
+ const rows = result.rows ?? [];
3563
+ const holders = [];
3564
+ for (const row of rows) {
3565
+ const holder = await this.normalizeTokenHolderRow(node, row);
3566
+ if (holder) {
3567
+ holders.push(holder);
3568
+ }
3569
+ }
3570
+ return holders;
3571
+ }
3572
+ async normalizeTokenHolderRow(node, row) {
3573
+ const templateId = this.normalizeTemplateIdentifier(row.template_id);
3574
+ const decoded = await this.decodeContractData(node, this.normalizePackageIdentifier(row.package_id), templateId, row.contract_instance);
3575
+ if (decoded?.status !== 'decoded' || !this.isRecordValue(decoded.value)) {
3576
+ return null;
3577
+ }
3578
+ if (templateId === CIP56_HOLDING_TEMPLATE_ID || this.isCip112TemplateId(templateId)) {
3579
+ const tokenSummary = this.extractObservedTokenSummary(templateId, decoded);
3580
+ const partyId = this.readScalarField(decoded.value, 'owner');
3581
+ if (!tokenSummary || !partyId) {
3582
+ return null;
3583
+ }
3584
+ return {
3585
+ contractId: this.normalizeOptionalScalar(row.contract_id),
3586
+ nodeId: node.id,
3587
+ label: node.label,
3588
+ tokenId: tokenSummary.tokenId,
3589
+ partyId,
3590
+ amount: this.readScalarField(decoded.value, 'amount'),
3591
+ };
3592
+ }
3593
+ if (templateId === CANTON_COIN_AMULET_TEMPLATE_ID) {
3594
+ const partyId = this.readScalarField(decoded.value, 'owner');
3595
+ if (!partyId) {
3596
+ return null;
3597
+ }
3598
+ return {
3599
+ contractId: this.normalizeOptionalScalar(row.contract_id),
3600
+ nodeId: node.id,
3601
+ label: node.label,
3602
+ tokenId: CANTON_COIN_TOKEN_ID,
3603
+ partyId,
3604
+ amount: this.readNestedScalarField(decoded.value, ['amount', 'initialAmount']),
3605
+ };
3606
+ }
3607
+ return null;
3608
+ }
3609
+ async loadCachedTokenTransfers(node) {
3610
+ const cached = this.tokenTransfersByNode.get(node.id);
3611
+ if (cached && Date.now() - cached.cachedAt < TOKEN_TRANSFER_CACHE_TTL_MS) {
3612
+ return cached.transfers;
3613
+ }
3614
+ const transfers = await this.fetchTokenTransfersForNode(node, TOKEN_TRANSFER_CACHE_LIMIT);
3615
+ this.tokenTransfersByNode.set(node.id, {
3616
+ cachedAt: Date.now(),
3617
+ transfers,
3618
+ });
3619
+ return transfers;
3620
+ }
3621
+ async fetchTokenTransfersForNode(node, limit) {
3622
+ const client = this.clientFactory.getClient(node);
3623
+ const result = await client.query(tokenRowsQuery(limit, TOKEN_TRANSFER_TEMPLATE_IDS));
3624
+ const rows = result.rows ?? [];
3625
+ const transfers = [];
3626
+ for (const row of rows) {
3627
+ const transfer = await this.normalizeTokenTransferRow(node, row);
3628
+ if (transfer) {
3629
+ transfers.push(transfer);
3630
+ }
3631
+ }
3632
+ const inferredTransfers = await this.fetchInferredCip112TransfersForNode(node, limit);
3633
+ return [...transfers, ...inferredTransfers].sort(compareGlobalTokenTransfers);
3634
+ }
3635
+ async normalizeTokenTransferRow(node, row) {
3636
+ const templateId = this.normalizeTemplateIdentifier(row.template_id);
3637
+ const decoded = await this.decodeContractData(node, this.normalizePackageIdentifier(row.package_id), templateId, row.contract_instance);
3638
+ const decodedTransfer = this.extractTokenMovement(templateId, decoded);
3639
+ if (!decodedTransfer) {
3640
+ return null;
3641
+ }
3642
+ return {
3643
+ nodeId: node.id,
3644
+ label: node.label,
3645
+ tokenId: decodedTransfer.tokenId,
3646
+ tokenName: decodedTransfer.tokenName,
3647
+ amount: decodedTransfer.amount,
3648
+ sender: decodedTransfer.sender,
3649
+ receiver: decodedTransfer.receiver,
3650
+ eventOffset: this.normalizeOptionalScalar(row.event_offset) ?? '',
3651
+ updateId: this.normalizeUpdateId(row.update_id),
3652
+ recordTime: typeof row.record_time === 'string' ? row.record_time : null,
3653
+ };
3654
+ }
3655
+ async fetchInferredCip112TransfersForNode(node, limit) {
3656
+ const client = this.clientFactory.getClient(node);
3657
+ const result = await client.query(recentCip112MovementUpdateIdsQuery(limit));
3658
+ const rows = result.rows ?? [];
3659
+ const transfers = [];
3660
+ for (const row of rows) {
3661
+ const rawUpdateId = typeof row.update_id === 'string' ? row.update_id : null;
3662
+ if (!rawUpdateId || !this.isHexTokenMovementUpdateId(rawUpdateId)) {
3663
+ continue;
3664
+ }
3665
+ const events = await this.fetchEventsByUpdateId(node, client.query.bind(client), rawUpdateId);
3666
+ transfers.push(...this.inferCip112TokenMovements(node, row, events));
3667
+ }
3668
+ return transfers;
3669
+ }
3670
+ inferCip112TokenMovements(node, update, events) {
3671
+ const createEvents = events.filter((event) => event.eventKind === 'create' &&
3672
+ this.isCip112TemplateId(event.templateId) &&
3673
+ event.createData?.status === 'decoded' &&
3674
+ this.isRecordValue(event.createData.value));
3675
+ const transferExercises = events.filter((event) => event.eventKind === 'consuming_exercise' &&
3676
+ this.isCip112TemplateId(event.templateId) &&
3677
+ typeof event.choice === 'string' &&
3678
+ event.choice.startsWith('Transfer'));
3679
+ const mintExercises = events.filter((event) => event.eventKind === 'non_consuming_exercise' &&
3680
+ this.isCip112TemplateId(event.templateId) &&
3681
+ typeof event.choice === 'string' &&
3682
+ event.choice.startsWith('Mint'));
3683
+ const transfers = [];
3684
+ const shareOwnerFallback = createEvents
3685
+ .filter((event) => this.isShareLikeCip112Template(event.templateId))
3686
+ .map((event) => event.createData?.status === 'decoded' && this.isRecordValue(event.createData.value)
3687
+ ? this.readScalarField(event.createData.value, 'owner')
3688
+ : null)
3689
+ .find((owner) => Boolean(owner));
3690
+ if (transferExercises.length > 0) {
3691
+ for (const event of createEvents) {
3692
+ const movementType = 'Create';
3693
+ const transfer = this.buildInferredCip112Movement(node, update, event, movementType, !this.isShareLikeCip112Template(event.templateId) ? shareOwnerFallback : null);
3694
+ if (transfer) {
3695
+ transfers.push(transfer);
3696
+ }
3697
+ }
3698
+ }
3699
+ if (mintExercises.length > 0) {
3700
+ for (const event of createEvents) {
3701
+ if (this.isShareLikeCip112Template(event.templateId)) {
3702
+ continue;
3703
+ }
3704
+ const transfer = this.buildInferredCip112Movement(node, update, event, 'Create');
3705
+ if (transfer) {
3706
+ transfers.push(transfer);
3707
+ }
3708
+ }
3709
+ }
3710
+ return transfers;
3711
+ }
3712
+ buildInferredCip112Movement(node, update, event, movementType, receiverFallback = null) {
3713
+ if (event.createData?.status !== 'decoded') {
3714
+ return null;
3715
+ }
3716
+ const record = event.createData.value;
3717
+ if (!this.isRecordValue(record)) {
3718
+ return null;
3719
+ }
3720
+ const token = this.extractObservedTokenSummary(event.templateId, event.createData);
3721
+ if (!token) {
3722
+ return null;
3723
+ }
3724
+ return {
3725
+ rowId: this.buildTokenMovementRowId(typeof update.update_id === 'string' ? update.update_id : this.normalizeUpdateId(''), event.eventId, event.templateId, movementType),
3726
+ movementType,
3727
+ source: INFERRED_HOLDING_V2_SOURCE,
3728
+ nodeId: node.id,
3729
+ label: node.label,
3730
+ tokenId: token.tokenId,
3731
+ tokenName: token.name,
3732
+ amount: this.readScalarField(record, 'amount'),
3733
+ sender: this.readScalarField(record, 'issuer'),
3734
+ receiver: this.readScalarField(record, 'owner') ?? receiverFallback,
3735
+ eventOffset: this.normalizeOptionalScalar(update.event_offset) ?? '',
3736
+ updateId: typeof update.update_id === 'string' ? this.normalizeUpdateId(update.update_id) : '',
3737
+ recordTime: typeof update.record_time === 'string' ? update.record_time : null,
3738
+ };
3739
+ }
3740
+ mergeTokenTransfers(transfers) {
3741
+ const deduped = new Map();
3742
+ for (const transfer of transfers) {
3743
+ const key = this.buildTokenTransferDedupKey(transfer);
3744
+ const existing = deduped.get(key);
3745
+ const observedNode = {
3746
+ nodeId: transfer.nodeId,
3747
+ label: transfer.label,
3748
+ eventOffset: transfer.eventOffset,
3749
+ };
3750
+ if (!existing) {
3751
+ deduped.set(key, {
3752
+ rowId: transfer.rowId,
3753
+ movementType: transfer.movementType,
3754
+ source: transfer.source,
3755
+ tokenId: transfer.tokenId,
3756
+ tokenName: transfer.tokenName,
3757
+ amount: transfer.amount,
3758
+ sender: transfer.sender,
3759
+ receiver: transfer.receiver,
3760
+ updateId: transfer.updateId,
3761
+ recordTime: transfer.recordTime,
3762
+ nodes: [observedNode],
3763
+ });
3764
+ continue;
3765
+ }
3766
+ if (!existing.nodes.some((node) => node.nodeId === observedNode.nodeId)) {
3767
+ existing.nodes.push(observedNode);
3768
+ }
3769
+ }
3770
+ return Array.from(deduped.values()).map((transfer) => ({
3771
+ ...transfer,
3772
+ nodes: [...transfer.nodes].sort((left, right) => left.label.localeCompare(right.label)),
3773
+ }));
3774
+ }
3775
+ mergeTokenHolders(holders) {
3776
+ const merged = new Map();
3777
+ for (const holder of holders) {
3778
+ const holderKey = `${holder.tokenId}\u0000${holder.partyId}`;
3779
+ const existing = merged.get(holderKey);
3780
+ const observedNode = {
3781
+ nodeId: holder.nodeId,
3782
+ label: holder.label,
3783
+ };
3784
+ const dedupeContractId = holder.contractId ??
3785
+ JSON.stringify([holder.tokenId, holder.partyId, holder.amount ?? '', holder.nodeId]);
3786
+ if (!existing) {
3787
+ merged.set(holderKey, {
3788
+ partyId: holder.partyId,
3789
+ amount: holder.amount,
3790
+ nodes: [observedNode],
3791
+ observedContractIds: new Set([dedupeContractId]),
3792
+ });
3793
+ continue;
3794
+ }
3795
+ if (!existing.nodes.some((node) => node.nodeId === observedNode.nodeId)) {
3796
+ existing.nodes.push(observedNode);
3797
+ }
3798
+ if (!existing.observedContractIds.has(dedupeContractId)) {
3799
+ existing.amount = this.addNumericStrings(existing.amount, holder.amount);
3800
+ existing.observedContractIds.add(dedupeContractId);
3801
+ }
3802
+ }
3803
+ return Array.from(merged.values())
3804
+ .map(({ observedContractIds: _observedContractIds, ...holder }) => ({
3805
+ ...holder,
3806
+ nodes: [...holder.nodes].sort((left, right) => left.label.localeCompare(right.label)),
3807
+ }))
3808
+ .sort((left, right) => {
3809
+ const amountComparison = this.compareNumericStrings(left.amount, right.amount);
3810
+ if (amountComparison !== 0) {
3811
+ return -amountComparison;
3812
+ }
3813
+ return left.partyId.localeCompare(right.partyId);
3814
+ });
3815
+ }
3816
+ buildTokenTransferDedupKey(transfer) {
3817
+ if (transfer.rowId) {
3818
+ return transfer.rowId;
3819
+ }
3820
+ return JSON.stringify([
3821
+ transfer.updateId,
3822
+ transfer.tokenId,
3823
+ transfer.amount ?? '',
3824
+ transfer.sender ?? '',
3825
+ transfer.receiver ?? '',
3826
+ transfer.recordTime ?? '',
3827
+ ]);
3828
+ }
3829
+ normalizeTokenId(tokenId) {
3830
+ return tokenId.trim();
3831
+ }
3832
+ isCip112TemplateId(templateId) {
3833
+ return typeof templateId === 'string' && templateId.includes('.CIP112:');
3834
+ }
3835
+ isSupportedObservedTokenTemplate(templateId) {
3836
+ return (templateId === CIP56_HOLDING_TEMPLATE_ID ||
3837
+ templateId === CIP56_TRANSFER_TEMPLATE_ID ||
3838
+ this.isCip112TemplateId(templateId));
3839
+ }
3840
+ isShareLikeCip112Template(templateId) {
3841
+ return typeof templateId === 'string' && templateId.includes('.ShareToken.CIP112:');
3842
+ }
3843
+ isHexTokenMovementUpdateId(updateId) {
3844
+ return /^[0-9a-f]+$/i.test(updateId);
3845
+ }
3846
+ buildTokenMovementRowId(updateId, eventId, templateId, movementType) {
3847
+ return [updateId, eventId ?? '', templateId ?? '', movementType].join(':');
3848
+ }
3849
+ compareNumericStrings(left, right) {
3850
+ const leftValue = left === null ? Number.NEGATIVE_INFINITY : Number(left);
3851
+ const rightValue = right === null ? Number.NEGATIVE_INFINITY : Number(right);
3852
+ const leftValid = Number.isFinite(leftValue);
3853
+ const rightValid = Number.isFinite(rightValue);
3854
+ if (leftValid && rightValid && leftValue !== rightValue) {
3855
+ return leftValue - rightValue;
3856
+ }
3857
+ if (leftValid !== rightValid) {
3858
+ return leftValid ? 1 : -1;
3859
+ }
3860
+ return (left ?? '').localeCompare(right ?? '');
3861
+ }
3862
+ addNumericStrings(left, right) {
3863
+ if (left === null) {
3864
+ return right;
3865
+ }
3866
+ if (right === null) {
3867
+ return left;
3868
+ }
3869
+ const scale = Math.max(this.numericScale(left), this.numericScale(right));
3870
+ const factor = 10n ** BigInt(scale);
3871
+ const leftValue = this.parseScaledDecimal(left, scale);
3872
+ const rightValue = this.parseScaledDecimal(right, scale);
3873
+ const sum = leftValue + rightValue;
3874
+ const negative = sum < 0n;
3875
+ const absolute = negative ? -sum : sum;
3876
+ const whole = absolute / factor;
3877
+ const fractional = absolute % factor;
3878
+ if (scale === 0) {
3879
+ return `${negative ? '-' : ''}${whole.toString()}`;
3880
+ }
3881
+ return `${negative ? '-' : ''}${whole.toString()}.${fractional
3882
+ .toString()
3883
+ .padStart(scale, '0')}`;
3884
+ }
3885
+ numericScale(value) {
3886
+ const normalized = value.trim();
3887
+ const decimalIndex = normalized.indexOf('.');
3888
+ return decimalIndex === -1 ? 0 : normalized.length - decimalIndex - 1;
3889
+ }
3890
+ parseScaledDecimal(value, scale) {
3891
+ const normalized = value.trim();
3892
+ const negative = normalized.startsWith('-');
3893
+ const unsigned = negative ? normalized.slice(1) : normalized;
3894
+ const [wholePart, fractionalPart = ''] = unsigned.split('.');
3895
+ const paddedFractional = `${fractionalPart}${'0'.repeat(scale)}`.slice(0, scale);
3896
+ const digits = `${wholePart || '0'}${paddedFractional}`;
3897
+ const parsed = BigInt(digits || '0');
3898
+ return negative ? -parsed : parsed;
3899
+ }
3900
+ normalizeUpdateId(updateId) {
3901
+ return updateId.startsWith('\\x') ? updateId.slice(2) : updateId;
3902
+ }
3903
+ extractTokenMovement(templateId, decoded) {
3904
+ if (!decoded || decoded.status !== 'decoded' || !this.isRecordValue(decoded.value)) {
3905
+ return null;
3906
+ }
3907
+ if (templateId === CANTON_COIN_TRANSFER_TEMPLATE_ID) {
3908
+ const transferRecord = this.findRecordField(decoded.value, 'transfer');
3909
+ if (!transferRecord) {
3910
+ return null;
3911
+ }
3912
+ return {
3913
+ tokenId: CANTON_COIN_TOKEN_ID,
3914
+ tokenName: CANTON_COIN_TOKEN_NAME,
3915
+ sender: this.readScalarField(transferRecord, 'sender'),
3916
+ receiver: this.readScalarField(transferRecord, 'receiver'),
3917
+ amount: this.readScalarField(transferRecord, 'amount'),
3918
+ };
3919
+ }
3920
+ if (templateId === CANTON_COIN_AMULET_TEMPLATE_ID) {
3921
+ return {
3922
+ tokenId: CANTON_COIN_TOKEN_ID,
3923
+ tokenName: CANTON_COIN_TOKEN_NAME,
3924
+ sender: this.readScalarField(decoded.value, 'dso'),
3925
+ receiver: this.readScalarField(decoded.value, 'owner'),
3926
+ amount: this.readNestedScalarField(decoded.value, ['amount', 'initialAmount']),
3927
+ };
3928
+ }
3929
+ if (templateId === CIP56_TRANSFER_TEMPLATE_ID) {
3930
+ const tokenSummary = this.extractCip56TokenSummary(decoded);
3931
+ if (!tokenSummary) {
3932
+ return null;
3933
+ }
3934
+ return {
3935
+ tokenId: tokenSummary.tokenId,
3936
+ tokenName: tokenSummary.name,
3937
+ sender: this.readScalarField(decoded.value, 'sender'),
3938
+ receiver: this.readScalarField(decoded.value, 'receiver'),
3939
+ amount: this.readScalarField(decoded.value, 'amount'),
3940
+ };
3941
+ }
3942
+ return null;
3943
+ }
3944
+ extractCip56TokenSummary(decoded) {
3945
+ if (!decoded || decoded.status !== 'decoded' || !this.isRecordValue(decoded.value)) {
3946
+ return null;
3947
+ }
3948
+ const intrinsicId = this.readNestedScalarField(decoded.value, ['instrumentId', 'id']);
3949
+ if (!intrinsicId) {
3950
+ return null;
3951
+ }
3952
+ const issuer = this.readNestedScalarField(decoded.value, ['instrumentId', 'admin']);
3953
+ const tokenId = this.buildObservedTokenId(intrinsicId, issuer);
3954
+ return {
3955
+ tokenId,
3956
+ name: this.readConfiguredDecodedTokenMetadata(decoded.value, 'name') ?? intrinsicId,
3957
+ symbol: this.readConfiguredDecodedTokenMetadata(decoded.value, 'symbol'),
3958
+ issuer,
3959
+ source: 'pqs',
3960
+ };
3961
+ }
3962
+ extractObservedTokenSummary(templateId, decoded) {
3963
+ if (!templateId) {
3964
+ return null;
3965
+ }
3966
+ if (this.isCip112TemplateId(templateId)) {
3967
+ return this.extractCip112TokenSummary(templateId, decoded);
3968
+ }
3969
+ return this.extractCip56TokenSummary(decoded);
3970
+ }
3971
+ extractCip112TokenSummary(templateId, decoded) {
3972
+ if (!decoded || decoded.status !== 'decoded' || !this.isRecordValue(decoded.value)) {
3973
+ return null;
3974
+ }
3975
+ const configuredSymbol = this.readConfiguredDecodedTokenMetadata(decoded.value, 'symbol');
3976
+ const symbol = configuredSymbol ?? this.readScalarField(decoded.value, 'symbol');
3977
+ const instrumentIdText = this.readScalarField(decoded.value, 'instrumentIdText');
3978
+ const instrumentId = this.readNestedScalarField(decoded.value, ['instrumentId', 'id']);
3979
+ const vaultIdentityId = this.readNestedScalarField(decoded.value, ['vaultIdentity', 'id']);
3980
+ const shareLikeIntrinsicId = templateId !== null && this.isShareLikeCip112Template(templateId) && vaultIdentityId
3981
+ ? this.normalizeShareLikeIntrinsicTokenId(vaultIdentityId)
3982
+ : null;
3983
+ const intrinsicId = instrumentId ?? shareLikeIntrinsicId ?? symbol ?? instrumentIdText;
3984
+ if (!intrinsicId) {
3985
+ return null;
3986
+ }
3987
+ const issuer = this.readNestedScalarField(decoded.value, ['instrumentId', 'admin'])
3988
+ ?? this.readNestedScalarField(decoded.value, ['vaultIdentity', 'admin'])
3989
+ ?? this.readScalarField(decoded.value, 'vaultParty')
3990
+ ?? this.readScalarField(decoded.value, 'issuer');
3991
+ const tokenId = this.buildObservedTokenId(intrinsicId, issuer);
3992
+ return {
3993
+ tokenId,
3994
+ name: this.readConfiguredDecodedTokenMetadata(decoded.value, 'name')
3995
+ ?? this.readScalarField(decoded.value, 'name')
3996
+ ?? instrumentIdText
3997
+ ?? symbol
3998
+ ?? intrinsicId,
3999
+ symbol,
4000
+ issuer,
4001
+ source: 'pqs',
4002
+ };
4003
+ }
4004
+ buildObservedTokenId(intrinsicId, issuer) {
4005
+ const normalizedIntrinsicId = intrinsicId.trim();
4006
+ const normalizedIssuer = issuer?.trim() ?? '';
4007
+ if (normalizedIssuer.length === 0) {
4008
+ return normalizedIntrinsicId;
4009
+ }
4010
+ return `${normalizedIssuer}::${normalizedIntrinsicId}`;
4011
+ }
4012
+ normalizeShareLikeIntrinsicTokenId(intrinsicId) {
4013
+ const normalizedIntrinsicId = intrinsicId.trim();
4014
+ if (normalizedIntrinsicId.endsWith(':share')) {
4015
+ return normalizedIntrinsicId;
4016
+ }
4017
+ return `${normalizedIntrinsicId}:share`;
4018
+ }
4019
+ readConfiguredDecodedTokenMetadata(value, field) {
4020
+ const keys = this.getTokenMetadataConfig()[field === 'name' ? 'nameKeys' : 'symbolKeys'];
4021
+ for (const key of keys) {
4022
+ const metadataValue = this.readTextMapEntryField(value, ['meta', 'values'], key);
4023
+ if (metadataValue) {
4024
+ return metadataValue;
4025
+ }
4026
+ }
4027
+ return null;
4028
+ }
4029
+ getTokenMetadataConfig() {
4030
+ return this.nodeConfigService?.getTokenMetadataConfig() ?? {
4031
+ nameKeys: [...node_config_schema_1.DEFAULT_TOKEN_METADATA_CONFIG.nameKeys],
4032
+ symbolKeys: [...node_config_schema_1.DEFAULT_TOKEN_METADATA_CONFIG.symbolKeys],
4033
+ };
4034
+ }
4035
+ async fetchEventsByUpdateId(node, query, rawUpdateId) {
4036
+ try {
4037
+ const result = await query(updateEventsQuery(rawUpdateId));
4038
+ const rows = result.rows ?? [];
4039
+ return Promise.all(rows.map((row) => this.normalizeEventRow(node, row)));
4040
+ }
4041
+ catch (error) {
4042
+ if (!this.shouldFallbackToNormalizedEventTables(error)) {
4043
+ throw error;
4044
+ }
4045
+ }
4046
+ const fallbackResult = await query(normalizedUpdateEventsQuery(rawUpdateId));
4047
+ const rows = fallbackResult.rows ?? [];
4048
+ return Promise.all(rows.map((row) => this.normalizeEventRow(node, row)));
4049
+ }
4050
+ shouldResolveRewardCoupon(events) {
4051
+ return events.some((event) => event.choice === 'ReceiveSvRewardCoupon');
4052
+ }
4053
+ async fetchRewardCouponDetails(query, rawUpdateId) {
4054
+ try {
4055
+ const result = await query(rewardCouponInstanceQuery(rawUpdateId));
4056
+ const row = result.rows[0];
4057
+ if (!row || !Buffer.isBuffer(row.contract_instance)) {
4058
+ return null;
4059
+ }
4060
+ const decoded = this.decodeRewardCouponContractInstance(row.contract_instance);
4061
+ if (!decoded || decoded.status !== 'decoded' || !this.isRecordValue(decoded.value)) {
4062
+ return null;
4063
+ }
4064
+ return {
4065
+ argument: { status: 'not_available' },
4066
+ result: {
4067
+ status: 'decoded',
4068
+ value: {
4069
+ kind: 'record',
4070
+ fields: typeof row.coupon_contract_id === 'string'
4071
+ ? [
4072
+ ...decoded.value.fields,
4073
+ {
4074
+ label: 'couponContractId',
4075
+ value: { kind: 'contract_id', value: row.coupon_contract_id },
4076
+ },
4077
+ ]
4078
+ : decoded.value.fields,
4079
+ },
4080
+ },
4081
+ };
4082
+ }
4083
+ catch {
4084
+ return null;
4085
+ }
4086
+ }
4087
+ extractRawUpdateId(row) {
4088
+ if (typeof row.update_id === 'string') {
4089
+ return row.update_id;
4090
+ }
4091
+ const meta = row.meta;
4092
+ if (meta && typeof meta.update_id === 'string') {
4093
+ return meta.update_id;
4094
+ }
4095
+ throw new Error('Update metadata is missing update_id');
4096
+ }
4097
+ extractEventOffset(row) {
4098
+ if (typeof row.event_offset === 'string') {
4099
+ return row.event_offset;
4100
+ }
4101
+ if (typeof row.event_offset === 'number') {
4102
+ return String(row.event_offset);
4103
+ }
4104
+ const meta = 'meta' in row ? row.meta : null;
4105
+ if (meta && typeof meta.event_offset === 'string') {
4106
+ return meta.event_offset;
4107
+ }
4108
+ if (meta && typeof meta.event_offset === 'number') {
4109
+ return String(meta.event_offset);
4110
+ }
4111
+ throw new Error('Update metadata is missing event_offset');
4112
+ }
4113
+ extractIsoRecordTime(row) {
4114
+ if (typeof row.record_time_iso === 'string') {
4115
+ return row.record_time_iso;
4116
+ }
4117
+ return typeof row.record_time === 'string' ? row.record_time : null;
4118
+ }
4119
+ extractMeta(row) {
4120
+ if (row.meta && typeof row.meta === 'object' && !Array.isArray(row.meta)) {
4121
+ return row.meta;
4122
+ }
4123
+ const { record_time_iso: _recordTimeIso, ...meta } = row;
4124
+ return meta;
4125
+ }
4126
+ async normalizeEventRow(node, row) {
4127
+ const templateId = this.normalizeTemplateIdentifier(row.template_id);
4128
+ const packageId = this.normalizePackageIdentifier(row.package_id ?? null);
4129
+ const rawChoice = typeof row.choice === 'string' ? row.choice : null;
4130
+ return {
4131
+ eventKind: row.event_kind,
4132
+ eventId: typeof row.event_id === 'string' ? row.event_id : null,
4133
+ contractId: typeof row.contract_id === 'string' ? row.contract_id : null,
4134
+ packageId,
4135
+ templateId,
4136
+ choice: this.normalizeChoiceIdentifier(row.choice),
4137
+ witnesses: this.normalizeParties(row.witnesses),
4138
+ createData: row.event_kind === 'create'
4139
+ ? await this.decodeContractData(node, packageId, templateId, row.contract_instance ?? null)
4140
+ : null,
4141
+ exerciseData: row.event_kind === 'create'
4142
+ ? null
4143
+ : await this.decodeExerciseData(node, {
4144
+ packageId,
4145
+ templateId,
4146
+ rawChoice,
4147
+ exerciseArgument: row.exercise_argument ?? null,
4148
+ exerciseResult: row.exercise_result ?? null,
4149
+ }),
4150
+ raw: row.raw && typeof row.raw === 'object' && !Array.isArray(row.raw) ? row.raw : {},
4151
+ };
4152
+ }
4153
+ attachRewardCouponDetails(events, exerciseData) {
4154
+ if (!exerciseData) {
4155
+ return events;
4156
+ }
4157
+ return events.map((event) => event.choice === 'ReceiveSvRewardCoupon'
4158
+ ? {
4159
+ ...event,
4160
+ exerciseData: {
4161
+ argument: event.exerciseData?.argument ?? exerciseData.argument,
4162
+ result: exerciseData.result,
4163
+ },
4164
+ }
4165
+ : event);
4166
+ }
4167
+ decodeRewardCouponContractInstance(contractInstance) {
4168
+ const contractPayload = this.getFirstLengthDelimitedField(contractInstance, 2);
4169
+ const createArgument = contractPayload
4170
+ ? this.getFirstLengthDelimitedField(contractPayload, 4)
4171
+ : null;
4172
+ const rewardCouponRecord = createArgument
4173
+ ? this.getFirstLengthDelimitedField(createArgument, 13)
4174
+ : null;
4175
+ const fields = rewardCouponRecord
4176
+ ? this.getAllLengthDelimitedFields(rewardCouponRecord, 1)
4177
+ : [];
4178
+ if (fields.length < 5) {
4179
+ return null;
4180
+ }
4181
+ const rewardRound = this.unwrapScalarVarint(fields.at(-2) ?? null);
4182
+ const rewardAmount = this.unwrapScalarVarint(fields.at(-1) ?? null);
4183
+ if (rewardRound === null || rewardAmount === null) {
4184
+ return null;
4185
+ }
4186
+ return {
4187
+ status: 'decoded',
4188
+ value: {
4189
+ kind: 'record',
4190
+ fields: [
4191
+ { label: 'rewardRound', value: rewardRound },
4192
+ { label: 'rewardAmount', value: rewardAmount },
4193
+ ],
4194
+ },
4195
+ };
4196
+ }
4197
+ async decodeContractData(node, packageId, templateId, contractInstance) {
4198
+ if (!Buffer.isBuffer(contractInstance) || !templateId) {
4199
+ return null;
4200
+ }
4201
+ if (templateId === 'Splice.Amulet:SvRewardCoupon') {
4202
+ return this.decodeRewardCouponContractInstance(contractInstance);
4203
+ }
4204
+ if (!this.damlValueDecoder) {
4205
+ return null;
4206
+ }
4207
+ const initialDecode = await this.damlValueDecoder.decodeContractInstance({
4208
+ packageId,
4209
+ templateId,
4210
+ contractInstance,
4211
+ });
4212
+ return this.retryContractDecodeAfterPackageRefresh(node, packageId, templateId, contractInstance, initialDecode);
4213
+ }
4214
+ async decodeExerciseData(node, input) {
4215
+ if (!this.damlValueDecoder) {
4216
+ return null;
4217
+ }
4218
+ const initialDecode = await this.damlValueDecoder.decodeExerciseValue(input);
4219
+ return this.retryExerciseDecodeAfterPackageRefresh(node, input, initialDecode);
4220
+ }
4221
+ async retryContractDecodeAfterPackageRefresh(node, packageId, templateId, contractInstance, initialDecode) {
4222
+ const reason = initialDecode?.status === 'invalid_data' ? initialDecode.reason : null;
4223
+ if (!this.shouldRetryPackageRefresh(node, packageId, reason)) {
4224
+ return initialDecode;
4225
+ }
4226
+ await this.refreshPackageForNode(node, packageId);
4227
+ return this.damlValueDecoder?.decodeContractInstance({
4228
+ packageId,
4229
+ templateId,
4230
+ contractInstance,
4231
+ }) ?? initialDecode;
4232
+ }
4233
+ async retryExerciseDecodeAfterPackageRefresh(node, input, initialDecode) {
4234
+ const reason = this.extractPackageRefreshReason(initialDecode);
4235
+ if (!this.shouldRetryPackageRefresh(node, input.packageId, reason)) {
4236
+ return initialDecode;
4237
+ }
4238
+ await this.refreshPackageForNode(node, input.packageId);
4239
+ return this.damlValueDecoder?.decodeExerciseValue(input) ?? initialDecode;
4240
+ }
4241
+ extractPackageRefreshReason(decoded) {
4242
+ const reasons = [
4243
+ decoded?.argument.status === 'invalid_data' ? decoded.argument.reason : null,
4244
+ decoded?.result.status === 'invalid_data' ? decoded.result.reason : null,
4245
+ ];
4246
+ return reasons.find((reason) => reason !== null) ?? null;
4247
+ }
4248
+ shouldRetryPackageRefresh(node, packageId, reason) {
4249
+ return Boolean(node &&
4250
+ packageId &&
4251
+ this.packageSyncService &&
4252
+ (reason === 'missing_package' || reason === 'invalid_package'));
4253
+ }
4254
+ async refreshPackageForNode(node, packageId) {
4255
+ await this.packageSyncService?.syncPackagesById(node, [packageId]);
4256
+ this.packageRegistryService?.invalidatePackage(packageId);
4257
+ }
4258
+ normalizePackageIdentifier(packageId) {
4259
+ if (typeof packageId !== 'string') {
4260
+ return null;
4261
+ }
4262
+ return packageId.replace(/^i\|/, '');
4263
+ }
4264
+ decodeKnownContractData(templateId, contractInstance) {
4265
+ if (!Buffer.isBuffer(contractInstance) || !templateId) {
4266
+ return null;
4267
+ }
4268
+ if (templateId === 'Splice.Amulet:SvRewardCoupon') {
4269
+ return this.decodeRewardCouponContractInstance(contractInstance);
4270
+ }
4271
+ return null;
4272
+ }
4273
+ isRecordValue(value) {
4274
+ return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'record';
4275
+ }
4276
+ findRecordField(value, label) {
4277
+ const field = value.fields.find((candidate) => candidate.label === label)?.value;
4278
+ return field && this.isRecordValue(field) ? field : null;
4279
+ }
4280
+ readScalarField(value, label) {
4281
+ const field = value.fields.find((candidate) => candidate.label === label)?.value;
4282
+ if (field === null || field === undefined) {
4283
+ return null;
4284
+ }
4285
+ if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') {
4286
+ return String(field);
4287
+ }
4288
+ if (typeof field === 'object' && field !== null && 'kind' in field && field.kind === 'contract_id') {
4289
+ return field.value;
4290
+ }
4291
+ return null;
4292
+ }
4293
+ readNestedScalarField(value, path) {
4294
+ let current = value;
4295
+ for (let index = 0; index < path.length; index += 1) {
4296
+ if (!current || !this.isRecordValue(current)) {
4297
+ return null;
4298
+ }
4299
+ const field = current.fields.find((candidate) => candidate.label === path[index])?.value;
4300
+ if (field === undefined) {
4301
+ return null;
4302
+ }
4303
+ if (index === path.length - 1) {
4304
+ if (typeof field === 'string' ||
4305
+ typeof field === 'number' ||
4306
+ typeof field === 'boolean') {
4307
+ return String(field);
4308
+ }
4309
+ if (typeof field === 'object' && field !== null && 'kind' in field && field.kind === 'contract_id') {
4310
+ return field.value;
4311
+ }
4312
+ return null;
4313
+ }
4314
+ current = field;
4315
+ }
4316
+ return null;
4317
+ }
4318
+ readTextMapEntryField(value, path, key) {
4319
+ let current = value;
4320
+ for (const segment of path) {
4321
+ if (!current || !this.isRecordValue(current)) {
4322
+ return null;
4323
+ }
4324
+ const field = current.fields.find((candidate) => candidate.label === segment)?.value;
4325
+ if (field === undefined) {
4326
+ return null;
4327
+ }
4328
+ current = field;
4329
+ }
4330
+ if (!current ||
4331
+ typeof current !== 'object' ||
4332
+ !('kind' in current) ||
4333
+ current.kind !== 'text_map') {
4334
+ return null;
4335
+ }
4336
+ const entry = current.entries.find((candidate) => candidate.key === key)?.value;
4337
+ if (entry === null || entry === undefined) {
4338
+ return null;
4339
+ }
4340
+ if (typeof entry === 'string' || typeof entry === 'number' || typeof entry === 'boolean') {
4341
+ return String(entry);
4342
+ }
4343
+ if (typeof entry === 'object' &&
4344
+ entry !== null &&
4345
+ 'kind' in entry &&
4346
+ entry.kind === 'contract_id') {
4347
+ return entry.value;
4348
+ }
4349
+ return null;
4350
+ }
4351
+ getFirstLengthDelimitedField(message, fieldNumber) {
4352
+ return this.parseProtobufFields(message).find((field) => field.fieldNumber === fieldNumber && field.buffer)?.buffer ?? null;
4353
+ }
4354
+ getAllLengthDelimitedFields(message, fieldNumber) {
4355
+ return this.parseProtobufFields(message)
4356
+ .filter((field) => field.fieldNumber === fieldNumber && Boolean(field.buffer))
4357
+ .map((field) => field.buffer);
4358
+ }
4359
+ unwrapScalarVarint(message) {
4360
+ let current = message;
4361
+ while (current) {
4362
+ const fields = this.parseProtobufFields(current);
4363
+ if (fields.length !== 1) {
4364
+ return null;
4365
+ }
4366
+ const [field] = fields;
4367
+ if (typeof field.value === 'number') {
4368
+ return field.value;
4369
+ }
4370
+ current = field.buffer ?? null;
4371
+ }
4372
+ return null;
4373
+ }
4374
+ parseProtobufFields(message) {
4375
+ const fields = [];
4376
+ let offset = 0;
4377
+ while (offset < message.length) {
4378
+ const tag = this.readVarint(message, offset);
4379
+ offset = tag.next;
4380
+ const fieldNumber = Number(tag.value >> 3n);
4381
+ const wireType = Number(tag.value & 0x07n);
4382
+ if (wireType === 0) {
4383
+ const value = this.readVarint(message, offset);
4384
+ offset = value.next;
4385
+ fields.push({ fieldNumber, value: Number(value.value) });
4386
+ continue;
4387
+ }
4388
+ if (wireType === 2) {
4389
+ const length = this.readVarint(message, offset);
4390
+ offset = length.next;
4391
+ const end = offset + Number(length.value);
4392
+ if (end > message.length) {
4393
+ break;
4394
+ }
4395
+ fields.push({ fieldNumber, buffer: message.subarray(offset, end) });
4396
+ offset = end;
4397
+ continue;
4398
+ }
4399
+ if (wireType === 1) {
4400
+ offset += 8;
4401
+ continue;
4402
+ }
4403
+ if (wireType === 5) {
4404
+ offset += 4;
4405
+ continue;
4406
+ }
4407
+ break;
4408
+ }
4409
+ return fields;
4410
+ }
4411
+ readVarint(buffer, offset) {
4412
+ let value = 0n;
4413
+ let shift = 0n;
4414
+ let next = offset;
4415
+ while (next < buffer.length) {
4416
+ const byte = BigInt(buffer[next]);
4417
+ value |= (byte & 0x7fn) << shift;
4418
+ next += 1;
4419
+ if ((byte & 0x80n) === 0n) {
4420
+ return { value, next };
4421
+ }
4422
+ shift += 7n;
4423
+ }
4424
+ return { value, next };
4425
+ }
4426
+ async buildTemplateFilterResponse(packageIds) {
4427
+ const templates = new Set();
4428
+ for (const packageId of packageIds) {
4429
+ const inspection = this.packageRegistryService
4430
+ ? await this.packageRegistryService.inspectPackage(packageId)
4431
+ : { ok: false, reason: 'missing_package' };
4432
+ if (!inspection.ok) {
4433
+ continue;
4434
+ }
4435
+ for (const template of inspection.definition.templates) {
4436
+ if (template.templateId.trim()) {
4437
+ templates.add(template.templateId);
4438
+ }
4439
+ }
4440
+ }
4441
+ return {
4442
+ templates: Array.from(templates)
4443
+ .sort((left, right) => left.localeCompare(right))
4444
+ .map((templateId) => ({ templateId })),
4445
+ };
4446
+ }
4447
+ normalizeOptionalScalar(value) {
4448
+ if (typeof value === 'string') {
4449
+ return value;
4450
+ }
4451
+ if (typeof value === 'number') {
4452
+ return String(value);
4453
+ }
4454
+ return null;
4455
+ }
4456
+ async fetchNamespaceTopologyByNode(nodes, observedNodeIds, partiesById) {
4457
+ const grpcOperationsService = this.grpcOperationsService;
4458
+ if (!grpcOperationsService) {
4459
+ return [];
4460
+ }
4461
+ const observedNodeIdSet = new Set(observedNodeIds);
4462
+ const topologyEntries = await Promise.all(nodes
4463
+ .filter((node) => observedNodeIdSet.has(node.id))
4464
+ .map(async (node) => {
4465
+ if (node.mode !== 'pqs_with_grpc') {
4466
+ return {
4467
+ nodeId: node.id,
4468
+ label: node.label,
4469
+ status: 'grpc_not_configured',
4470
+ errorMessage: null,
4471
+ partyToParticipants: [],
4472
+ partyToKeyMappings: [],
4473
+ };
4474
+ }
4475
+ const nodeParties = Array.from(partiesById.entries())
4476
+ .filter(([, nodeIds]) => nodeIds.has(node.id))
4477
+ .map(([partyId]) => partyId);
4478
+ return this.mergeNamespaceNodeTopologies(node, await Promise.all(nodeParties.map(async (partyId) => grpcOperationsService.fetchPartyTopology(node, partyId))));
4479
+ }));
4480
+ return topologyEntries.sort((left, right) => left.label.localeCompare(right.label));
4481
+ }
4482
+ mergeNamespaceNodeTopologies(node, topologies) {
4483
+ const successfulTopologies = topologies.filter((entry) => entry.status === 'ok');
4484
+ if (successfulTopologies.length > 0) {
4485
+ return {
4486
+ nodeId: node.id,
4487
+ label: node.label,
4488
+ status: 'ok',
4489
+ errorMessage: null,
4490
+ partyToParticipants: this.dedupePartyTopologyParticipants(successfulTopologies.flatMap((entry) => entry.partyToParticipants)),
4491
+ partyToKeyMappings: this.dedupePartyTopologyKeys(successfulTopologies.flatMap((entry) => entry.partyToKeyMappings)),
4492
+ };
4493
+ }
4494
+ const grpcError = topologies.find((entry) => entry.status === 'grpc_error');
4495
+ if (grpcError) {
4496
+ return {
4497
+ nodeId: node.id,
4498
+ label: node.label,
4499
+ status: 'grpc_error',
4500
+ errorMessage: grpcError.errorMessage,
4501
+ partyToParticipants: [],
4502
+ partyToKeyMappings: [],
4503
+ };
4504
+ }
4505
+ return {
4506
+ nodeId: node.id,
4507
+ label: node.label,
4508
+ status: 'grpc_not_configured',
4509
+ errorMessage: null,
4510
+ partyToParticipants: [],
4511
+ partyToKeyMappings: [],
4512
+ };
4513
+ }
4514
+ dedupePartyTopologyParticipants(participants) {
4515
+ const deduped = new Map();
4516
+ for (const participant of participants) {
4517
+ const key = [
4518
+ participant.participantId ?? '',
4519
+ participant.participantUid ?? '',
4520
+ participant.permission ?? '',
4521
+ participant.threshold ?? '',
4522
+ participant.synchronizerIds.join('|'),
4523
+ ].join('::');
4524
+ if (!deduped.has(key)) {
4525
+ deduped.set(key, participant);
4526
+ }
4527
+ }
4528
+ return Array.from(deduped.values());
4529
+ }
4530
+ dedupePartyTopologyKeys(keyMappings) {
4531
+ const deduped = new Map();
4532
+ for (const keyMapping of keyMappings) {
4533
+ const key = [
4534
+ keyMapping.keyFingerprint ?? '',
4535
+ keyMapping.publicKey ?? '',
4536
+ keyMapping.purpose ?? '',
4537
+ keyMapping.keyType ?? '',
4538
+ keyMapping.keyFormat ?? '',
4539
+ keyMapping.keySpec ?? '',
4540
+ keyMapping.threshold ?? '',
4541
+ keyMapping.synchronizerIds.join('|'),
4542
+ ].join('::');
4543
+ if (!deduped.has(key)) {
4544
+ deduped.set(key, keyMapping);
4545
+ }
4546
+ }
4547
+ return Array.from(deduped.values());
4548
+ }
4549
+ extractNamespaceIdentifier(partyId) {
4550
+ const normalizedPartyId = this.normalizePartyIdentifier(partyId);
4551
+ const separatorIndex = normalizedPartyId.lastIndexOf('::');
4552
+ if (separatorIndex < 0) {
4553
+ return null;
4554
+ }
4555
+ const namespaceId = normalizedPartyId.slice(separatorIndex + 2).trim();
4556
+ return namespaceId.length > 0 ? namespaceId : null;
4557
+ }
4558
+ paginateNamespacePartyIds(partyIds, options) {
4559
+ const limit = Math.max(1, Math.trunc(options.limit));
4560
+ if (options.after) {
4561
+ const endIndex = partyIds.findIndex((value) => value === options.after);
4562
+ const normalizedEndIndex = endIndex >= 0 ? endIndex : partyIds.length;
4563
+ const startIndex = Math.max(0, normalizedEndIndex - limit);
4564
+ const items = partyIds.slice(startIndex, normalizedEndIndex);
4565
+ return {
4566
+ items,
4567
+ nextBefore: normalizedEndIndex < partyIds.length && items.length > 0 ? items[items.length - 1] ?? null : null,
4568
+ nextAfter: startIndex > 0 && items.length > 0 ? items[0] ?? null : null,
4569
+ };
4570
+ }
4571
+ const startIndex = options.before
4572
+ ? (() => {
4573
+ const index = partyIds.findIndex((value) => value === options.before);
4574
+ return index >= 0 ? index + 1 : 0;
4575
+ })()
4576
+ : 0;
4577
+ const items = partyIds.slice(startIndex, startIndex + limit);
4578
+ return {
4579
+ items,
4580
+ nextBefore: startIndex + limit < partyIds.length && items.length > 0 ? items[items.length - 1] ?? null : null,
4581
+ nextAfter: startIndex > 0 && items.length > 0 ? items[0] ?? null : null,
4582
+ };
4583
+ }
4584
+ normalizeTemplateIdentifier(templateId) {
4585
+ if (typeof templateId !== 'string') {
4586
+ return null;
4587
+ }
4588
+ return templateId.replace(/^t\|#[^:]+:/, '');
4589
+ }
4590
+ normalizeChoiceIdentifier(choice) {
4591
+ if (typeof choice !== 'string') {
4592
+ return null;
4593
+ }
4594
+ const normalized = choice.replace(/^c\|/, '');
4595
+ const separatorIndex = normalized.lastIndexOf('_');
4596
+ return separatorIndex >= 0 ? normalized.slice(separatorIndex + 1) : normalized;
4597
+ }
4598
+ normalizeParties(parties) {
4599
+ if (Array.isArray(parties)) {
4600
+ return parties
4601
+ .filter((party) => typeof party === 'string')
4602
+ .map((party) => this.normalizePartyIdentifier(party))
4603
+ .filter(Boolean);
4604
+ }
4605
+ if (typeof parties !== 'string') {
4606
+ return [];
4607
+ }
4608
+ const trimmed = parties.trim();
4609
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
4610
+ return trimmed ? [trimmed] : [];
4611
+ }
4612
+ return trimmed
4613
+ .slice(1, -1)
4614
+ .split(',')
4615
+ .map((party) => this.normalizePartyIdentifier(party.trim().replace(/^"(.*)"$/, '$1')))
4616
+ .filter(Boolean);
4617
+ }
4618
+ normalizePartyIdentifier(party) {
4619
+ const trimmed = party.trim();
4620
+ if (!trimmed) {
4621
+ return '';
4622
+ }
4623
+ return trimmed.replace(/^p\|/, '');
4624
+ }
4625
+ defaultRow() {
4626
+ return {
4627
+ pqs_database: 'unknown',
4628
+ active_contract_count: 0,
4629
+ latest_offset: null,
4630
+ latest_event_at: null,
4631
+ total_update_count: 0,
4632
+ };
4633
+ }
4634
+ };
4635
+ exports.PqsSummaryService = PqsSummaryService;
4636
+ exports.PqsSummaryService = PqsSummaryService = __decorate([
4637
+ (0, common_1.Injectable)(),
4638
+ __param(1, (0, common_1.Optional)()),
4639
+ __param(2, (0, common_1.Optional)()),
4640
+ __param(3, (0, common_1.Optional)()),
4641
+ __param(4, (0, common_1.Optional)()),
4642
+ __param(5, (0, common_1.Optional)()),
4643
+ __param(6, (0, common_1.Optional)()),
4644
+ __metadata("design:paramtypes", [pqs_client_factory_1.PqsClientFactory,
4645
+ daml_value_decoder_service_1.DamlValueDecoderService,
4646
+ package_cache_service_1.PackageCacheService,
4647
+ package_registry_service_1.PackageRegistryService,
4648
+ node_config_service_1.NodeConfigService,
4649
+ grpc_operations_service_1.GrpcOperationsService,
4650
+ package_sync_service_1.PackageSyncService])
4651
+ ], PqsSummaryService);
4652
+ //# sourceMappingURL=pqs-summary.service.js.map