@distrohelena/canton-explorer 0.1.4 → 0.1.5

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 (28) hide show
  1. package/dist/public/assets/DebuggerView-VieEr0b3.js +2 -0
  2. package/dist/public/assets/index-BmT5Hrud.js +1 -0
  3. package/dist/public/assets/index-DnEheBxd.css +1 -0
  4. package/dist/public/assets/{editor.api-8w-mShFJ.js → monaco-runtime-DiIHw4v2.js} +1 -1
  5. package/dist/public/index.html +2 -2
  6. package/dist/src/api/nodes.controller.d.ts +2 -2
  7. package/dist/src/api/nodes.controller.js +20 -8
  8. package/dist/src/api/nodes.controller.js.map +1 -1
  9. package/dist/src/cache/node-cache.service.js +23 -9
  10. package/dist/src/cache/node-cache.service.js.map +1 -1
  11. package/dist/src/domain/node-health.js +1 -1
  12. package/dist/src/domain/node-health.js.map +1 -1
  13. package/dist/src/domain/node.types.d.ts +2 -0
  14. package/dist/src/grpc/daml-lf-archive.d.ts +2 -0
  15. package/dist/src/grpc/daml-lf-archive.js +104 -0
  16. package/dist/src/grpc/daml-lf-archive.js.map +1 -0
  17. package/dist/src/grpc/grpc-operations.service.d.ts +1 -0
  18. package/dist/src/grpc/grpc-operations.service.js +10 -6
  19. package/dist/src/grpc/grpc-operations.service.js.map +1 -1
  20. package/dist/src/pqs/pqs-summary.service.d.ts +19 -1
  21. package/dist/src/pqs/pqs-summary.service.js +430 -61
  22. package/dist/src/pqs/pqs-summary.service.js.map +1 -1
  23. package/dist/tsconfig.build.tsbuildinfo +1 -1
  24. package/package.json +1 -1
  25. package/dist/public/assets/DebuggerView-GIrRlNTJ.js +0 -2
  26. package/dist/public/assets/index-CdWdP2w6.js +0 -1
  27. package/dist/public/assets/index-fkZAWLbW.css +0 -1
  28. /package/dist/public/assets/{editor-C5uazxST.css → monaco-runtime-C5uazxST.css} +0 -0
@@ -28,6 +28,12 @@ const CANTON_COIN_TOKEN_ID = 'canton-coin';
28
28
  const CANTON_COIN_TOKEN_NAME = 'Canton Coin';
29
29
  const CANTON_COIN_TRANSFER_TEMPLATE_ID = 'Splice.AmuletTransferInstruction:AmuletTransferInstruction';
30
30
  const CANTON_COIN_AMULET_TEMPLATE_ID = 'Splice.Amulet:Amulet';
31
+ const AMULET_RULES_TEMPLATE_ID = 'Splice.AmuletRules:AmuletRules';
32
+ const NATIVE_AMULET_SUPPORT_TEMPLATE_IDS = [
33
+ CANTON_COIN_AMULET_TEMPLATE_ID,
34
+ AMULET_RULES_TEMPLATE_ID,
35
+ 'Splice.Amulet:ValidatorRight',
36
+ ];
31
37
  const NATIVE_AMULET_INTRINSIC_ID = 'Amulet';
32
38
  const CIP56_HOLDING_TEMPLATE_ID = 'Splice.Api.Token.HoldingV1:Holding';
33
39
  const CIP56_TRANSFER_TEMPLATE_ID = 'Splice.Api.Token.TransferInstructionV1:Transfer';
@@ -856,13 +862,14 @@ function singleUpdateQuery(node, eventOffset) {
856
862
  limit 1
857
863
  `;
858
864
  }
859
- function updateEventsQuery(node, updateId) {
865
+ function updateEventsByUpdateIdsQuery(node, updateIds) {
860
866
  const relations = pqsCoreRelations(node);
861
- const quotedId = `'${escapeSqlLiteral(updateId)}'`;
867
+ const quotedIds = updateIds.map((updateId) => `'${escapeSqlLiteral(updateId)}'`).join(', ');
862
868
  const contractTemplateId = contractTemplateIdentifierExpression('contract_tpe_row');
863
869
  const exerciseTemplateId = contractTemplateIdentifierExpression('exercise_contract_tpe_row');
864
870
  return `
865
871
  select
872
+ update_id,
866
873
  event_kind,
867
874
  event_id,
868
875
  contract_id,
@@ -876,6 +883,7 @@ function updateEventsQuery(node, updateId) {
876
883
  raw
877
884
  from (
878
885
  select
886
+ tx.transaction_id::text as update_id,
879
887
  'create'::text as event_kind,
880
888
  event_row.event_id::text as event_id,
881
889
  contract_row.contract_id::text as contract_id,
@@ -903,11 +911,12 @@ function updateEventsQuery(node, updateId) {
903
911
  on contract_tpe_row.pk = contract_row.tpe_pk
904
912
  left join ${relations.packages} package_row
905
913
  on package_row.pk = contract_row.package_pk
906
- where tx.transaction_id = ${quotedId}
914
+ where tx.transaction_id in (${quotedIds})
907
915
 
908
916
  union all
909
917
 
910
918
  select
919
+ tx.transaction_id::text as update_id,
911
920
  case
912
921
  when exercise_tpe_row.consuming then 'consuming_exercise'::text
913
922
  else 'non_consuming_exercise'::text
@@ -941,11 +950,14 @@ function updateEventsQuery(node, updateId) {
941
950
  on exercise_tpe_row.pk = exercise_row.tpe_pk
942
951
  left join ${relations.packages} package_row
943
952
  on package_row.pk = exercise_row.package_pk
944
- where tx.transaction_id = ${quotedId}
953
+ where tx.transaction_id in (${quotedIds})
945
954
  ) update_events
946
- order by event_id asc nulls last, event_kind asc, contract_id asc, template_id asc
955
+ order by update_id asc nulls last, event_id asc nulls last, event_kind asc, contract_id asc, template_id asc
947
956
  `;
948
957
  }
958
+ function updateEventsQuery(node, updateId) {
959
+ return updateEventsByUpdateIdsQuery(node, [updateId]);
960
+ }
949
961
  function rewardCouponInstanceQuery(node, updateId) {
950
962
  const relations = pqsCoreRelations(node);
951
963
  const quotedId = `'${escapeSqlLiteral(normalizeByteaHex(updateId))}'`;
@@ -971,9 +983,11 @@ function rewardCouponInstanceQuery(node, updateId) {
971
983
  limit 1
972
984
  `;
973
985
  }
974
- function contractDetailQuery(node, contractId) {
986
+ function contractDetailsQuery(node, contractIds) {
975
987
  const relations = pqsCoreRelations(node);
976
- const quotedId = `'${escapeSqlLiteral(normalizeByteaHex(contractId))}'`;
988
+ const quotedIds = contractIds
989
+ .map((contractId) => `'${escapeSqlLiteral(normalizeByteaHex(contractId))}'`)
990
+ .join(', ');
977
991
  const contractTemplateId = contractTemplateIdentifierExpression('contract_tpe_row');
978
992
  return `
979
993
  with selected_contract as (
@@ -989,8 +1003,7 @@ function contractDetailQuery(node, contractId) {
989
1003
  on contract_tpe_row.pk = contract_row.tpe_pk
990
1004
  left join ${relations.packages} package_row
991
1005
  on package_row.pk = contract_row.package_pk
992
- where contract_row.contract_id = ${quotedId}
993
- limit 1
1006
+ where contract_row.contract_id in (${quotedIds})
994
1007
  )
995
1008
  select
996
1009
  contract_row.contract_id,
@@ -1010,6 +1023,9 @@ function contractDetailQuery(node, contractId) {
1010
1023
  on archived_tx.ix = contract_row.archived_at_ix
1011
1024
  `;
1012
1025
  }
1026
+ function contractDetailQuery(node, contractId) {
1027
+ return contractDetailsQuery(node, [contractId]);
1028
+ }
1013
1029
  function tokenRowsQuery(node, limit, templateIds, templatePatterns = []) {
1014
1030
  const relations = pqsCoreRelations(node);
1015
1031
  const normalizedLimit = Number.isFinite(limit) && Number(limit) > 0 ? Math.trunc(limit) : TOKEN_TRANSFER_CACHE_LIMIT;
@@ -1055,6 +1071,21 @@ function tokenRowsQuery(node, limit, templateIds, templatePatterns = []) {
1055
1071
  limit ${normalizedLimit}
1056
1072
  `;
1057
1073
  }
1074
+ function nativeAmuletSupportQuery(node) {
1075
+ const relations = pqsCoreRelations(node);
1076
+ const templateIds = NATIVE_AMULET_SUPPORT_TEMPLATE_IDS
1077
+ .map((templateId) => `'${escapeSqlLiteral(templateId)}'`)
1078
+ .join(',\n ');
1079
+ const templateId = contractTemplateIdentifierExpression('contract_tpe_row');
1080
+ return `
1081
+ select ${templateId} as template_id
1082
+ from ${relations.contractTpe} contract_tpe_row
1083
+ where ${templateId} in (
1084
+ ${templateIds}
1085
+ )
1086
+ limit 1
1087
+ `;
1088
+ }
1058
1089
  function recentCip112MovementUpdateIdsQuery(node, limit) {
1059
1090
  const relations = pqsCoreRelations(node);
1060
1091
  const normalizedLimit = Number.isFinite(limit) && Number(limit) > 0 ? Math.trunc(limit) : TOKEN_TRANSFER_CACHE_LIMIT;
@@ -1284,7 +1315,7 @@ let PqsSummaryService = class PqsSummaryService {
1284
1315
  if (nodesToFetch.length === 0) {
1285
1316
  break;
1286
1317
  }
1287
- const responses = await Promise.all(nodesToFetch.map((state) => this.fetchRecentUpdates(state.node, {
1318
+ const settled = await Promise.allSettled(nodesToFetch.map((state) => this.fetchRecentUpdates(state.node, {
1288
1319
  limit: pageSize,
1289
1320
  parties,
1290
1321
  templates,
@@ -1292,8 +1323,25 @@ let PqsSummaryService = class PqsSummaryService {
1292
1323
  hideSplice,
1293
1324
  before: state.nextBefore,
1294
1325
  })));
1295
- responses.forEach((response, index) => {
1326
+ const successfulResponses = [];
1327
+ settled.forEach((result, index) => {
1296
1328
  const state = nodesToFetch[index];
1329
+ if (!state) {
1330
+ return;
1331
+ }
1332
+ if (result.status !== 'fulfilled') {
1333
+ state.exhausted = true;
1334
+ return;
1335
+ }
1336
+ successfulResponses.push({
1337
+ state,
1338
+ response: result.value,
1339
+ });
1340
+ });
1341
+ if (successfulResponses.length === 0) {
1342
+ throw settled.find((result) => result.status === 'rejected')?.reason;
1343
+ }
1344
+ successfulResponses.forEach(({ response, state }) => {
1297
1345
  for (const update of response.updates) {
1298
1346
  mergedUpdatesByKey.set(`${state.node.id}:${update.eventOffset}:${update.updateId}`, {
1299
1347
  nodeId: state.node.id,
@@ -1717,29 +1765,52 @@ let PqsSummaryService = class PqsSummaryService {
1717
1765
  }
1718
1766
  async fetchActiveParties(nodes) {
1719
1767
  return {
1720
- nodes: await Promise.all(nodes.map(async (node) => ({
1768
+ nodes: await Promise.all(nodes.map(async (node) => this.buildActivePartiesEntry(node))),
1769
+ };
1770
+ }
1771
+ async buildActivePartiesEntry(node) {
1772
+ try {
1773
+ return {
1721
1774
  nodeId: node.id,
1722
1775
  label: node.label,
1723
1776
  mode: node.mode,
1724
1777
  parties: await this.fetchActivePartiesForNode(node),
1725
- }))),
1726
- };
1778
+ activePartiesStatus: 'ok',
1779
+ activePartiesError: null,
1780
+ };
1781
+ }
1782
+ catch (error) {
1783
+ return {
1784
+ nodeId: node.id,
1785
+ label: node.label,
1786
+ mode: node.mode,
1787
+ parties: [],
1788
+ activePartiesStatus: 'pqs_error',
1789
+ activePartiesError: error instanceof Error ? error.message : 'Unknown error',
1790
+ };
1791
+ }
1727
1792
  }
1728
1793
  async fetchPartyDetail(nodes, partyId) {
1729
1794
  const normalizedPartyId = this.normalizePartyIdentifier(partyId);
1730
1795
  const grpcOperationsService = this.grpcOperationsService;
1731
- const activePartiesByNode = await Promise.all(nodes.map(async (node) => ({
1796
+ const activePartiesByNode = (await Promise.allSettled(nodes.map(async (node) => ({
1732
1797
  node,
1733
1798
  parties: await this.fetchActivePartiesForNode(node),
1734
- })));
1735
- const recentUpdatesByNode = await Promise.all(nodes.map(async (node) => ({
1799
+ }))))
1800
+ .filter((result) => result.status === 'fulfilled')
1801
+ .map((result) => result.value);
1802
+ const recentUpdatesByNode = (await Promise.allSettled(nodes.map(async (node) => ({
1736
1803
  node,
1737
1804
  updates: await this.fetchPartyRecentUpdatesForNode(node, normalizedPartyId, 10),
1738
- })));
1739
- const recentContractsByNode = await Promise.all(nodes.map(async (node) => ({
1805
+ }))))
1806
+ .filter((result) => result.status === 'fulfilled')
1807
+ .map((result) => result.value);
1808
+ const recentContractsByNode = (await Promise.allSettled(nodes.map(async (node) => ({
1740
1809
  node,
1741
1810
  contracts: await this.fetchPartyRecentContractsForNode(node, normalizedPartyId, 10),
1742
- })));
1811
+ }))))
1812
+ .filter((result) => result.status === 'fulfilled')
1813
+ .map((result) => result.value);
1743
1814
  const localPartiesByNode = grpcOperationsService
1744
1815
  ? await Promise.all(nodes.map(async (node) => {
1745
1816
  try {
@@ -1847,10 +1918,12 @@ let PqsSummaryService = class PqsSummaryService {
1847
1918
  async fetchNamespaceDetail(nodes, namespaceId) {
1848
1919
  const normalizedNamespaceId = namespaceId.trim();
1849
1920
  const grpcOperationsService = this.grpcOperationsService;
1850
- const activePartiesByNode = await Promise.all(nodes.map(async (node) => ({
1921
+ const activePartiesByNode = (await Promise.allSettled(nodes.map(async (node) => ({
1851
1922
  node,
1852
1923
  parties: (await this.fetchActivePartiesForNode(node)).filter((partyId) => this.extractNamespaceIdentifier(partyId) === normalizedNamespaceId),
1853
- })));
1924
+ }))))
1925
+ .filter((result) => result.status === 'fulfilled')
1926
+ .map((result) => result.value);
1854
1927
  const localPartiesByNode = grpcOperationsService
1855
1928
  ? await Promise.all(nodes.map(async (node) => {
1856
1929
  try {
@@ -1943,7 +2016,9 @@ let PqsSummaryService = class PqsSummaryService {
1943
2016
  async fetchNamespaceParties(nodes, namespaceId, options) {
1944
2017
  const normalizedNamespaceId = namespaceId.trim();
1945
2018
  const matchingParties = new Set();
1946
- const activePartiesByNode = await Promise.all(nodes.map(async (node) => (await this.fetchActivePartiesForNode(node)).filter((partyId) => this.extractNamespaceIdentifier(partyId) === normalizedNamespaceId)));
2019
+ const activePartiesByNode = (await Promise.allSettled(nodes.map(async (node) => (await this.fetchActivePartiesForNode(node)).filter((partyId) => this.extractNamespaceIdentifier(partyId) === normalizedNamespaceId))))
2020
+ .filter((result) => result.status === 'fulfilled')
2021
+ .map((result) => result.value);
1947
2022
  for (const parties of activePartiesByNode) {
1948
2023
  for (const partyId of parties) {
1949
2024
  matchingParties.add(partyId);
@@ -2039,7 +2114,7 @@ let PqsSummaryService = class PqsSummaryService {
2039
2114
  if (nodesToFetch.length === 0) {
2040
2115
  break;
2041
2116
  }
2042
- const responses = await Promise.all(nodesToFetch.map((state) => this.fetchNodeContracts(state.node, {
2117
+ const settled = await Promise.allSettled(nodesToFetch.map((state) => this.fetchNodeContracts(state.node, {
2043
2118
  limit: pageSize,
2044
2119
  before: state.nextBefore,
2045
2120
  parties: activePartyFilters.length > 0 ? activePartyFilters : undefined,
@@ -2047,8 +2122,25 @@ let PqsSummaryService = class PqsSummaryService {
2047
2122
  partyMode,
2048
2123
  hideSplice,
2049
2124
  })));
2050
- responses.forEach((response, index) => {
2125
+ const successfulResponses = [];
2126
+ settled.forEach((result, index) => {
2051
2127
  const state = nodesToFetch[index];
2128
+ if (!state) {
2129
+ return;
2130
+ }
2131
+ if (result.status !== 'fulfilled') {
2132
+ state.exhausted = true;
2133
+ return;
2134
+ }
2135
+ successfulResponses.push({
2136
+ state,
2137
+ response: result.value,
2138
+ });
2139
+ });
2140
+ if (successfulResponses.length === 0) {
2141
+ throw settled.find((result) => result.status === 'rejected')?.reason;
2142
+ }
2143
+ successfulResponses.forEach(({ response, state }) => {
2052
2144
  for (const contract of response.contracts) {
2053
2145
  mergedContractsByKey.set(`${state.node.id}:${contract.contractId}`, {
2054
2146
  nodeId: state.node.id,
@@ -2129,12 +2221,29 @@ let PqsSummaryService = class PqsSummaryService {
2129
2221
  if (nodesToFetch.length === 0) {
2130
2222
  break;
2131
2223
  }
2132
- const responses = await Promise.all(nodesToFetch.map((state) => this.fetchPartyContractsForNode(state.node, normalizedPartyId, {
2224
+ const settled = await Promise.allSettled(nodesToFetch.map((state) => this.fetchPartyContractsForNode(state.node, normalizedPartyId, {
2133
2225
  limit: pageSize,
2134
2226
  before: state.nextBefore,
2135
2227
  })));
2136
- responses.forEach((response, index) => {
2228
+ const successfulResponses = [];
2229
+ settled.forEach((result, index) => {
2137
2230
  const state = nodesToFetch[index];
2231
+ if (!state) {
2232
+ return;
2233
+ }
2234
+ if (result.status !== 'fulfilled') {
2235
+ state.exhausted = true;
2236
+ return;
2237
+ }
2238
+ successfulResponses.push({
2239
+ state,
2240
+ response: result.value,
2241
+ });
2242
+ });
2243
+ if (successfulResponses.length === 0) {
2244
+ throw settled.find((result) => result.status === 'rejected')?.reason;
2245
+ }
2246
+ successfulResponses.forEach(({ response, state }) => {
2138
2247
  for (const contract of response.contracts) {
2139
2248
  mergedContractsByKey.set(`${contract.nodeId}:${contract.contractId}`, contract);
2140
2249
  }
@@ -2190,18 +2299,18 @@ let PqsSummaryService = class PqsSummaryService {
2190
2299
  return this.paginateTokens(filteredTokens.sort(compareGlobalTokens), limit, options);
2191
2300
  }
2192
2301
  async fetchLatestTokenTransfers(nodes, limit = 25, options) {
2193
- const mergedTransfers = this.filterTokenTransfersByParties(await this.loadMergedTokenTransfers(nodes), options);
2302
+ const mergedTransfers = this.filterTokenTransfers(await this.loadMergedTokenTransfers(nodes), options);
2194
2303
  return this.paginateTokenTransfers(mergedTransfers, limit, options);
2195
2304
  }
2196
2305
  async fetchTokenTransfers(nodes, tokenId, limit = 25, options) {
2197
2306
  const normalizedTokenId = this.normalizeTokenId(tokenId);
2198
2307
  await this.findObservedToken(nodes, normalizedTokenId);
2199
- const mergedTransfers = this.filterTokenTransfersByParties(await this.loadMergedTokenTransfers(nodes), options);
2308
+ const mergedTransfers = this.filterTokenTransfers(await this.loadMergedTokenTransfers(nodes), options);
2200
2309
  const directTransfers = mergedTransfers.filter((transfer) => transfer.tokenId === normalizedTokenId);
2201
2310
  if (directTransfers.length > 0) {
2202
2311
  return this.paginateTokenTransfers(directTransfers, limit, options);
2203
2312
  }
2204
- const recoveredTransfers = this.filterTokenTransfersByParties(await this.recoverTokenTransfersFromActiveHolders(nodes, normalizedTokenId), options);
2313
+ const recoveredTransfers = this.filterTokenTransfers(await this.recoverTokenTransfersFromActiveHolders(nodes, normalizedTokenId), options);
2205
2314
  return this.paginateTokenTransfers(recoveredTransfers, limit, options);
2206
2315
  }
2207
2316
  async fetchTokenTransferDetail(nodes, transferId) {
@@ -2341,10 +2450,11 @@ let PqsSummaryService = class PqsSummaryService {
2341
2450
  node,
2342
2451
  transfers: await this.loadCachedTokenTransfers(node),
2343
2452
  })));
2344
- const successfulTransfers = refreshResults
2453
+ const successfulRefreshes = refreshResults.filter((result) => result.status === 'fulfilled');
2454
+ const successfulTransfers = successfulRefreshes
2345
2455
  .filter((result) => result.status === 'fulfilled')
2346
2456
  .flatMap((result) => result.value.transfers);
2347
- if (successfulTransfers.length === 0) {
2457
+ if (successfulRefreshes.length === 0) {
2348
2458
  const firstFailure = refreshResults.find((result) => result.status === 'rejected');
2349
2459
  if (firstFailure) {
2350
2460
  throw firstFailure.reason;
@@ -2364,9 +2474,9 @@ let PqsSummaryService = class PqsSummaryService {
2364
2474
  .filter((holder) => holder.tokenId === tokenId && holder.contractId !== null);
2365
2475
  const recoveredTransfers = [];
2366
2476
  const seenContracts = new Set();
2477
+ const contractIdsByNode = new Map();
2367
2478
  for (const holder of candidateHolders) {
2368
- const node = nodeById.get(holder.nodeId);
2369
- if (!node || !holder.contractId) {
2479
+ if (!nodeById.has(holder.nodeId) || !holder.contractId) {
2370
2480
  continue;
2371
2481
  }
2372
2482
  const dedupeKey = `${holder.nodeId}\u0000${holder.contractId}`;
@@ -2374,19 +2484,38 @@ let PqsSummaryService = class PqsSummaryService {
2374
2484
  continue;
2375
2485
  }
2376
2486
  seenContracts.add(dedupeKey);
2377
- const client = this.clientFactory.getClient(node);
2378
- const contractResult = await client.query(contractDetailQuery(node, holder.contractId));
2379
- const contractRow = contractResult.rows[0];
2380
- if (!contractRow || typeof contractRow.created_update_id !== 'string') {
2487
+ const nodeContractIds = contractIdsByNode.get(holder.nodeId);
2488
+ if (nodeContractIds) {
2489
+ nodeContractIds.push(holder.contractId);
2490
+ }
2491
+ else {
2492
+ contractIdsByNode.set(holder.nodeId, [holder.contractId]);
2493
+ }
2494
+ }
2495
+ for (const [nodeId, contractIds] of contractIdsByNode) {
2496
+ const node = nodeById.get(nodeId);
2497
+ if (!node) {
2381
2498
  continue;
2382
2499
  }
2383
- const events = await this.fetchEventsByUpdateId(node, client.query.bind(client), contractRow.created_update_id);
2384
- const inferredTransfers = this.inferCip112TokenMovements(node, {
2385
- update_id: contractRow.created_update_id,
2386
- event_offset: contractRow.created_event_offset,
2387
- record_time: contractRow.created_record_time,
2388
- }, events).filter((transfer) => transfer.tokenId === tokenId);
2389
- recoveredTransfers.push(...inferredTransfers);
2500
+ const client = this.clientFactory.getClient(node);
2501
+ const contractRows = await this.fetchContractDetailsByIds(node, client.query.bind(client), contractIds);
2502
+ const eventsByUpdateId = await this.fetchEventsByUpdateIds(node, client.query.bind(client), contractRows
2503
+ .map((contractRow) => contractRow.created_update_id)
2504
+ .filter((updateId) => typeof updateId === 'string'));
2505
+ for (const contractRow of contractRows) {
2506
+ if (typeof contractRow.created_update_id !== 'string') {
2507
+ continue;
2508
+ }
2509
+ const events = eventsByUpdateId.get(contractRow.created_update_id) ?? [];
2510
+ const inferredTransfers = this.extractCip112TokenMovements(node, {
2511
+ update_id: contractRow.created_update_id,
2512
+ event_offset: contractRow.created_event_offset,
2513
+ record_time: contractRow.created_record_time,
2514
+ }, events, {
2515
+ canonicalShareTokenId: tokenId,
2516
+ }).filter((transfer) => transfer.tokenId === tokenId);
2517
+ recoveredTransfers.push(...inferredTransfers);
2518
+ }
2390
2519
  }
2391
2520
  return this.mergeTokenTransfers(recoveredTransfers);
2392
2521
  }
@@ -2425,13 +2554,15 @@ let PqsSummaryService = class PqsSummaryService {
2425
2554
  transfers: pagedTransfers,
2426
2555
  };
2427
2556
  }
2428
- filterTokenTransfersByParties(transfers, options) {
2557
+ filterTokenTransfers(transfers, options) {
2429
2558
  const activeFromParties = normalizePartyFilters(options?.fromParties);
2430
2559
  const activeToParties = normalizePartyFilters(options?.toParties);
2560
+ const activeMovementTypes = this.normalizeTokenTransferMovementTypeFilters(options?.movementTypes);
2431
2561
  const amountGt = options?.amountGt?.trim() || null;
2432
2562
  const amountLt = options?.amountLt?.trim() || null;
2433
2563
  if (activeFromParties.length === 0 &&
2434
2564
  activeToParties.length === 0 &&
2565
+ activeMovementTypes.length === 0 &&
2435
2566
  amountGt === null &&
2436
2567
  amountLt === null) {
2437
2568
  return transfers;
@@ -2441,11 +2572,13 @@ let PqsSummaryService = class PqsSummaryService {
2441
2572
  (transfer.sender !== null && activeFromParties.includes(transfer.sender));
2442
2573
  const matchesTo = activeToParties.length === 0 ||
2443
2574
  (transfer.receiver !== null && activeToParties.includes(transfer.receiver));
2575
+ const matchesMovementType = activeMovementTypes.length === 0 ||
2576
+ activeMovementTypes.includes(this.effectiveTokenTransferMovementType(transfer).toLowerCase());
2444
2577
  const matchesAmountGt = amountGt === null ||
2445
2578
  (transfer.amount !== null && this.compareNumericStrings(transfer.amount, amountGt) > 0);
2446
2579
  const matchesAmountLt = amountLt === null ||
2447
2580
  (transfer.amount !== null && this.compareNumericStrings(transfer.amount, amountLt) < 0);
2448
- return matchesFrom && matchesTo && matchesAmountGt && matchesAmountLt;
2581
+ return matchesFrom && matchesTo && matchesMovementType && matchesAmountGt && matchesAmountLt;
2449
2582
  });
2450
2583
  }
2451
2584
  async fetchUpdateDetail(node, eventOffset) {
@@ -2606,16 +2739,15 @@ let PqsSummaryService = class PqsSummaryService {
2606
2739
  return cached.tokens;
2607
2740
  }
2608
2741
  const useGrpcHoldingViews = node.mode === 'pqs_with_grpc' && this.grpcOperationsService !== undefined;
2609
- const [pqsTokens, grpcTokens] = await Promise.all([
2610
- this.fetchObservedTokensForNode(node, TOKEN_TRANSFER_CACHE_LIMIT, {
2611
- includeCip112: !useGrpcHoldingViews,
2612
- }),
2742
+ const [pqsTokens, grpcTokens, builtinTokens] = await Promise.all([
2743
+ this.fetchObservedTokensForNode(node, TOKEN_TRANSFER_CACHE_LIMIT),
2613
2744
  useGrpcHoldingViews
2614
2745
  ? this.grpcOperationsService.fetchHoldingV2Tokens(node)
2615
2746
  : Promise.resolve([]),
2747
+ this.fetchBuiltinTokensForNode(node),
2616
2748
  ]);
2617
2749
  const dedupedTokens = new Map();
2618
- for (const token of [...pqsTokens, ...grpcTokens]) {
2750
+ for (const token of [...pqsTokens, ...grpcTokens, ...builtinTokens]) {
2619
2751
  if (!dedupedTokens.has(token.tokenId)) {
2620
2752
  dedupedTokens.set(token.tokenId, token);
2621
2753
  }
@@ -2627,6 +2759,31 @@ let PqsSummaryService = class PqsSummaryService {
2627
2759
  });
2628
2760
  return tokens;
2629
2761
  }
2762
+ async fetchBuiltinTokensForNode(node) {
2763
+ const client = this.clientFactory.getClient(node);
2764
+ const result = await client.query(nativeAmuletSupportQuery(node));
2765
+ const rows = Array.isArray(result.rows) ? result.rows : [];
2766
+ const hasNativeAmuletSupport = rows.some((row) => {
2767
+ if (!row || typeof row !== 'object') {
2768
+ return false;
2769
+ }
2770
+ const templateId = row.template_id;
2771
+ return (typeof templateId === 'string' &&
2772
+ NATIVE_AMULET_SUPPORT_TEMPLATE_IDS.includes(this.normalizeTemplateIdentifier(templateId)));
2773
+ });
2774
+ if (!hasNativeAmuletSupport) {
2775
+ return [];
2776
+ }
2777
+ return [
2778
+ {
2779
+ tokenId: CANTON_COIN_TOKEN_ID,
2780
+ name: CANTON_COIN_TOKEN_NAME,
2781
+ symbol: null,
2782
+ issuer: null,
2783
+ source: 'pqs',
2784
+ },
2785
+ ];
2786
+ }
2630
2787
  async fetchObservedTokensForNode(node, limit, options) {
2631
2788
  const client = this.clientFactory.getClient(node);
2632
2789
  const includeCip112 = options?.includeCip112 ?? true;
@@ -2812,17 +2969,93 @@ let PqsSummaryService = class PqsSummaryService {
2812
2969
  const result = await client.query(recentCip112MovementUpdateIdsQuery(node, limit));
2813
2970
  const rows = result.rows ?? [];
2814
2971
  const transfers = [];
2972
+ const rawUpdateIds = rows
2973
+ .map((row) => (typeof row.update_id === 'string' ? row.update_id : null))
2974
+ .filter((updateId) => Boolean(updateId && this.isHexTokenMovementUpdateId(updateId)));
2975
+ const eventsByUpdateId = await this.fetchEventsByUpdateIds(node, client.query.bind(client), rawUpdateIds);
2815
2976
  for (const row of rows) {
2816
2977
  const rawUpdateId = typeof row.update_id === 'string' ? row.update_id : null;
2817
2978
  if (!rawUpdateId || !this.isHexTokenMovementUpdateId(rawUpdateId)) {
2818
2979
  continue;
2819
2980
  }
2820
- const events = await this.fetchEventsByUpdateId(node, client.query.bind(client), rawUpdateId);
2821
- transfers.push(...this.inferCip112TokenMovements(node, row, events));
2981
+ const events = eventsByUpdateId.get(rawUpdateId) ?? [];
2982
+ transfers.push(...this.extractCip112TokenMovements(node, row, events));
2983
+ }
2984
+ return transfers;
2985
+ }
2986
+ extractCip112TokenMovements(node, update, events, options) {
2987
+ const standardTransfers = this.extractStandardCip112TokenMovements(node, update, events, options);
2988
+ if (standardTransfers.length > 0) {
2989
+ return standardTransfers;
2990
+ }
2991
+ return this.inferCip112TokenMovements(node, update, events, options);
2992
+ }
2993
+ extractStandardCip112TokenMovements(node, update, events, options) {
2994
+ const createTokensById = new Map();
2995
+ for (const event of events) {
2996
+ if (event.eventKind !== 'create' ||
2997
+ !this.isCip112TemplateId(event.templateId) ||
2998
+ event.createData?.status !== 'decoded') {
2999
+ continue;
3000
+ }
3001
+ const token = this.extractObservedTokenSummary(event.templateId, event.createData);
3002
+ if (token) {
3003
+ createTokensById.set(token.tokenId, token);
3004
+ }
3005
+ }
3006
+ const transfers = [];
3007
+ const eventLogExercises = events.filter((event) => event.eventKind === 'non_consuming_exercise' &&
3008
+ (event.choice === 'HoldingsChange' || event.choice === 'EventLog_HoldingsChange') &&
3009
+ event.exerciseData?.argument.status === 'decoded' &&
3010
+ this.isRecordValue(event.exerciseData.argument.value));
3011
+ for (const event of eventLogExercises) {
3012
+ const argument = event.exerciseData?.argument;
3013
+ if (!argument || argument.status !== 'decoded' || !this.isRecordValue(argument.value)) {
3014
+ continue;
3015
+ }
3016
+ const account = this.findRecordField(argument.value, 'account');
3017
+ const transferLegs = this.readListField(argument.value, 'transferLegSides');
3018
+ const admin = this.readScalarField(argument.value, 'admin');
3019
+ if (!account || !admin || transferLegs.length === 0) {
3020
+ continue;
3021
+ }
3022
+ for (const leg of transferLegs) {
3023
+ if (!this.isRecordValue(leg)) {
3024
+ continue;
3025
+ }
3026
+ const side = this.readConstructorField(leg, 'side');
3027
+ if (side !== 'SenderSide') {
3028
+ continue;
3029
+ }
3030
+ const transferLegId = this.readScalarField(leg, 'transferLegId');
3031
+ const amount = this.readScalarField(leg, 'amount');
3032
+ const instrumentId = this.readScalarField(leg, 'instrumentId');
3033
+ const otherSideAccount = this.findRecordField(leg, 'otherside');
3034
+ if (!transferLegId || !instrumentId || !otherSideAccount) {
3035
+ continue;
3036
+ }
3037
+ const token = this.resolveEventLogTokenSummary(admin, instrumentId, createTokensById, options?.canonicalShareTokenId ?? null);
3038
+ const movementType = this.classifyEventLogMovementType(account, otherSideAccount, admin, instrumentId);
3039
+ transfers.push({
3040
+ rowId: this.buildTokenTransferLegRowId(typeof update.update_id === 'string' ? this.normalizeUpdateId(update.update_id) : '', event.eventId, transferLegId, movementType),
3041
+ movementType,
3042
+ source: 'pqs',
3043
+ nodeId: node.id,
3044
+ label: node.label,
3045
+ tokenId: token.tokenId,
3046
+ tokenName: token.name,
3047
+ amount,
3048
+ sender: this.readAccountParty(account),
3049
+ receiver: this.readAccountParty(otherSideAccount),
3050
+ eventOffset: this.normalizeOptionalScalar(update.event_offset) ?? '',
3051
+ updateId: typeof update.update_id === 'string' ? this.normalizeUpdateId(update.update_id) : '',
3052
+ recordTime: typeof update.record_time === 'string' ? update.record_time : null,
3053
+ });
3054
+ }
2822
3055
  }
2823
3056
  return transfers;
2824
3057
  }
2825
- inferCip112TokenMovements(node, update, events) {
3058
+ inferCip112TokenMovements(node, update, events, options) {
2826
3059
  const createEvents = events.filter((event) => event.eventKind === 'create' &&
2827
3060
  this.isCip112TemplateId(event.templateId) &&
2828
3061
  event.createData?.status === 'decoded' &&
@@ -2842,10 +3075,11 @@ let PqsSummaryService = class PqsSummaryService {
2842
3075
  ? this.readScalarField(event.createData.value, 'owner')
2843
3076
  : null)
2844
3077
  .find((owner) => Boolean(owner));
2845
- if (transferExercises.length > 0) {
3078
+ const shouldEmitCreateMovements = transferExercises.length > 0 || (transferExercises.length === 0 && mintExercises.length === 0);
3079
+ if (shouldEmitCreateMovements) {
2846
3080
  for (const event of createEvents) {
2847
3081
  const movementType = 'Create';
2848
- const transfer = this.buildInferredCip112Movement(node, update, event, movementType, !this.isShareLikeCip112Template(event.templateId) ? shareOwnerFallback : null);
3082
+ const transfer = this.buildInferredCip112Movement(node, update, event, movementType, !this.isShareLikeCip112Template(event.templateId) ? shareOwnerFallback : null, options?.canonicalShareTokenId);
2849
3083
  if (transfer) {
2850
3084
  transfers.push(transfer);
2851
3085
  }
@@ -2864,7 +3098,7 @@ let PqsSummaryService = class PqsSummaryService {
2864
3098
  }
2865
3099
  return transfers;
2866
3100
  }
2867
- buildInferredCip112Movement(node, update, event, movementType, receiverFallback = null) {
3101
+ buildInferredCip112Movement(node, update, event, movementType, receiverFallback = null, canonicalShareTokenId = null) {
2868
3102
  if (event.createData?.status !== 'decoded') {
2869
3103
  return null;
2870
3104
  }
@@ -2876,13 +3110,16 @@ let PqsSummaryService = class PqsSummaryService {
2876
3110
  if (!token) {
2877
3111
  return null;
2878
3112
  }
3113
+ const tokenId = canonicalShareTokenId && this.isShareLikeCip112Template(event.templateId)
3114
+ ? canonicalShareTokenId
3115
+ : token.tokenId;
2879
3116
  return {
2880
3117
  rowId: this.buildTokenMovementRowId(typeof update.update_id === 'string' ? update.update_id : this.normalizeUpdateId(''), event.eventId, event.templateId, movementType),
2881
3118
  movementType,
2882
3119
  source: INFERRED_HOLDING_V2_SOURCE,
2883
3120
  nodeId: node.id,
2884
3121
  label: node.label,
2885
- tokenId: token.tokenId,
3122
+ tokenId,
2886
3123
  tokenName: token.name,
2887
3124
  amount: this.readScalarField(record, 'amount'),
2888
3125
  sender: movementType === 'Mint' ? this.readScalarField(record, 'issuer') : null,
@@ -2984,6 +3221,17 @@ let PqsSummaryService = class PqsSummaryService {
2984
3221
  normalizeTokenId(tokenId) {
2985
3222
  return tokenId.trim();
2986
3223
  }
3224
+ normalizeTokenTransferMovementTypeFilters(movementTypes) {
3225
+ return Array.from(new Set((movementTypes ?? [])
3226
+ .map((movementType) => movementType.trim().toLowerCase())
3227
+ .filter((movementType) => movementType.length > 0)));
3228
+ }
3229
+ effectiveTokenTransferMovementType(transfer) {
3230
+ const normalizedMovementType = transfer.movementType?.trim();
3231
+ return normalizedMovementType && normalizedMovementType.length > 0
3232
+ ? normalizedMovementType
3233
+ : 'Transfer';
3234
+ }
2987
3235
  isCip112TemplateId(templateId) {
2988
3236
  return typeof templateId === 'string' && templateId.includes('.CIP112:');
2989
3237
  }
@@ -3001,6 +3249,33 @@ let PqsSummaryService = class PqsSummaryService {
3001
3249
  buildTokenMovementRowId(updateId, eventId, templateId, movementType) {
3002
3250
  return [updateId, eventId ?? '', templateId ?? '', movementType].join(':');
3003
3251
  }
3252
+ buildTokenTransferLegRowId(updateId, eventId, transferLegId, movementType) {
3253
+ return [updateId, eventId ?? '', transferLegId, movementType].join(':');
3254
+ }
3255
+ resolveEventLogTokenSummary(admin, instrumentId, tokensById, canonicalShareTokenId) {
3256
+ const fallbackToken = this.buildObservedTokenSummary(instrumentId, admin, this.isNativeAmuletIntrinsicId(instrumentId) ? CANTON_COIN_TOKEN_NAME : instrumentId, null);
3257
+ const tokenId = canonicalShareTokenId && fallbackToken.tokenId === this.normalizeShareLikeIntrinsicTokenId(instrumentId)
3258
+ ? canonicalShareTokenId
3259
+ : fallbackToken.tokenId;
3260
+ return tokensById.get(tokenId) ?? tokensById.get(fallbackToken.tokenId) ?? {
3261
+ ...fallbackToken,
3262
+ tokenId,
3263
+ };
3264
+ }
3265
+ classifyEventLogMovementType(senderAccount, receiverAccount, admin, instrumentId) {
3266
+ if (this.isSyntheticEventLogAccount(senderAccount, admin, `${instrumentId}:mint`)) {
3267
+ return 'Mint';
3268
+ }
3269
+ if (this.isSyntheticEventLogAccount(receiverAccount, admin, `${instrumentId}:burn`)) {
3270
+ return 'Burn';
3271
+ }
3272
+ return 'Transfer';
3273
+ }
3274
+ isSyntheticEventLogAccount(account, provider, expectedId) {
3275
+ return (this.readOptionalScalarField(account, 'owner') === null &&
3276
+ this.readOptionalScalarField(account, 'provider') === provider &&
3277
+ this.readScalarField(account, 'id') === expectedId);
3278
+ }
3004
3279
  compareNumericStrings(left, right) {
3005
3280
  const leftValue = left === null ? Number.NEGATIVE_INFINITY : Number(left);
3006
3281
  const rightValue = right === null ? Number.NEGATIVE_INFINITY : Number(right);
@@ -3198,9 +3473,43 @@ let PqsSummaryService = class PqsSummaryService {
3198
3473
  };
3199
3474
  }
3200
3475
  async fetchEventsByUpdateId(node, query, rawUpdateId) {
3201
- const result = await query(updateEventsQuery(node, rawUpdateId));
3476
+ const eventsByUpdateId = await this.fetchEventsByUpdateIds(node, query, [rawUpdateId]);
3477
+ return eventsByUpdateId.get(rawUpdateId) ?? [];
3478
+ }
3479
+ async fetchEventsByUpdateIds(node, query, rawUpdateIds) {
3480
+ if (rawUpdateIds.length === 0) {
3481
+ return new Map();
3482
+ }
3483
+ const result = await query(updateEventsByUpdateIdsQuery(node, rawUpdateIds));
3202
3484
  const rows = result.rows ?? [];
3203
- return Promise.all(rows.map((row) => this.normalizeEventRow(node, row)));
3485
+ const onlyRequestedUpdateId = rawUpdateIds.length === 1 ? rawUpdateIds[0] : null;
3486
+ const normalizedRows = await Promise.all(rows.map(async (row) => ({
3487
+ updateId: typeof row.update_id === 'string'
3488
+ ? row.update_id
3489
+ : onlyRequestedUpdateId,
3490
+ event: await this.normalizeEventRow(node, row),
3491
+ })));
3492
+ const eventsByUpdateId = new Map();
3493
+ for (const { updateId, event } of normalizedRows) {
3494
+ if (!updateId) {
3495
+ continue;
3496
+ }
3497
+ const existingEvents = eventsByUpdateId.get(updateId);
3498
+ if (existingEvents) {
3499
+ existingEvents.push(event);
3500
+ }
3501
+ else {
3502
+ eventsByUpdateId.set(updateId, [event]);
3503
+ }
3504
+ }
3505
+ return eventsByUpdateId;
3506
+ }
3507
+ async fetchContractDetailsByIds(node, query, contractIds) {
3508
+ if (contractIds.length === 0) {
3509
+ return [];
3510
+ }
3511
+ const result = await query(contractDetailsQuery(node, contractIds));
3512
+ return result.rows ?? [];
3204
3513
  }
3205
3514
  shouldResolveRewardCoupon(events) {
3206
3515
  return events.some((event) => event.choice === 'ReceiveSvRewardCoupon');
@@ -3497,6 +3806,66 @@ let PqsSummaryService = class PqsSummaryService {
3497
3806
  }
3498
3807
  return null;
3499
3808
  }
3809
+ readOptionalScalarField(value, label) {
3810
+ const field = value.fields.find((candidate) => candidate.label === label)?.value;
3811
+ if (field === null || field === undefined) {
3812
+ return null;
3813
+ }
3814
+ if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') {
3815
+ return String(field);
3816
+ }
3817
+ if (typeof field === 'object' &&
3818
+ field !== null &&
3819
+ 'kind' in field &&
3820
+ field.kind === 'contract_id') {
3821
+ return field.value;
3822
+ }
3823
+ if (typeof field !== 'object' || !('kind' in field) || field.kind !== 'optional') {
3824
+ return null;
3825
+ }
3826
+ const innerValue = field.value;
3827
+ if (typeof innerValue === 'string' ||
3828
+ typeof innerValue === 'number' ||
3829
+ typeof innerValue === 'boolean') {
3830
+ return String(innerValue);
3831
+ }
3832
+ if (typeof innerValue === 'object' &&
3833
+ innerValue !== null &&
3834
+ 'kind' in innerValue &&
3835
+ innerValue.kind === 'contract_id') {
3836
+ return innerValue.value;
3837
+ }
3838
+ return null;
3839
+ }
3840
+ readConstructorField(value, label) {
3841
+ const field = value.fields.find((candidate) => candidate.label === label)?.value;
3842
+ if (field === null || field === undefined) {
3843
+ return null;
3844
+ }
3845
+ if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') {
3846
+ return String(field);
3847
+ }
3848
+ if (typeof field !== 'object' || !('kind' in field)) {
3849
+ return null;
3850
+ }
3851
+ if (field.kind === 'enum') {
3852
+ return field.constructor;
3853
+ }
3854
+ if (field.kind === 'variant') {
3855
+ return field.constructor;
3856
+ }
3857
+ return null;
3858
+ }
3859
+ readListField(value, label) {
3860
+ const field = value.fields.find((candidate) => candidate.label === label)?.value;
3861
+ if (!field || typeof field !== 'object' || !('kind' in field) || field.kind !== 'list') {
3862
+ return [];
3863
+ }
3864
+ return field.items;
3865
+ }
3866
+ readAccountParty(value) {
3867
+ return this.readOptionalScalarField(value, 'owner') ?? this.readOptionalScalarField(value, 'provider');
3868
+ }
3500
3869
  readTextMapEntryField(value, path, key) {
3501
3870
  let current = value;
3502
3871
  for (const segment of path) {