envio 3.9.0 → 3.10.0-subgraph

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 (143) hide show
  1. package/evm.schema.json +26 -0
  2. package/fuel.schema.json +26 -0
  3. package/index.d.ts +41 -8
  4. package/package.json +6 -7
  5. package/src/Batch.res +9 -0
  6. package/src/ChainState.res +35 -39
  7. package/src/ChainState.res.mjs +14 -11
  8. package/src/ChainState.resi +15 -7
  9. package/src/Config.res +61 -44
  10. package/src/Config.res.mjs +56 -19
  11. package/src/Core.res +19 -0
  12. package/src/Core.res.mjs +11 -0
  13. package/src/CrossChainState.res +15 -23
  14. package/src/CrossChainState.res.mjs +10 -18
  15. package/src/CrossChainState.resi +4 -1
  16. package/src/Ecosystem.res +7 -2
  17. package/src/Envio.res +6 -4
  18. package/src/EventConfigBuilder.res +169 -26
  19. package/src/EventConfigBuilder.res.mjs +124 -15
  20. package/src/EventProcessing.res +6 -6
  21. package/src/EventProcessing.res.mjs +8 -6
  22. package/src/HandlerLoader.res +20 -8
  23. package/src/HandlerLoader.res.mjs +11 -2
  24. package/src/HandlerRegister.res +21 -4
  25. package/src/HandlerRegister.res.mjs +24 -5
  26. package/src/InMemoryStore.res +9 -7
  27. package/src/InMemoryStore.res.mjs +3 -3
  28. package/src/IndexerState.res +43 -6
  29. package/src/IndexerState.res.mjs +36 -7
  30. package/src/IndexerState.resi +4 -4
  31. package/src/Internal.res +16 -17
  32. package/src/Metrics.res +72 -6
  33. package/src/Metrics.res.mjs +30 -2
  34. package/src/Persistence.res +36 -30
  35. package/src/Persistence.res.mjs +1 -20
  36. package/src/PgStorage.res +104 -56
  37. package/src/PgStorage.res.mjs +57 -35
  38. package/src/PruneStaleHistory.res +85 -52
  39. package/src/PruneStaleHistory.res.mjs +63 -37
  40. package/src/Rollback.res +68 -45
  41. package/src/Rollback.res.mjs +38 -19
  42. package/src/RollbackCommit.res +7 -9
  43. package/src/RollbackCommit.res.mjs +5 -6
  44. package/src/SafeCheckpointTracking.res +11 -11
  45. package/src/SafeCheckpointTracking.res.mjs +7 -6
  46. package/src/SimulateItems.res +33 -18
  47. package/src/SimulateItems.res.mjs +26 -22
  48. package/src/Sink.res +38 -26
  49. package/src/Sink.res.mjs +21 -16
  50. package/src/TestIndexer.res +5 -5
  51. package/src/TestIndexer.res.mjs +1 -1
  52. package/src/UserContext.res +358 -51
  53. package/src/UserContext.res.mjs +271 -35
  54. package/src/Utils.res +170 -0
  55. package/src/Utils.res.mjs +153 -0
  56. package/src/Writing.res +2 -2
  57. package/src/Writing.res.mjs +1 -1
  58. package/src/bindings/ClickHouse.res +366 -698
  59. package/src/bindings/ClickHouse.res.mjs +371 -421
  60. package/src/bindings/ClickHouseSink.res +337 -0
  61. package/src/bindings/ClickHouseSink.res.mjs +246 -0
  62. package/src/bindings/EventSource.res +8 -2
  63. package/src/bindings/NodeJs.res +6 -0
  64. package/src/bindings/Vitest.res +23 -0
  65. package/src/bindings/Vitest.res.mjs +3 -0
  66. package/src/bindings/WebSocket.res +9 -10
  67. package/src/db/CheckpointBounds.res +53 -0
  68. package/src/db/CheckpointBounds.res.mjs +44 -0
  69. package/src/db/EntityFilter.res +36 -17
  70. package/src/db/EntityFilter.res.mjs +25 -14
  71. package/src/db/EntityHistory.res +47 -22
  72. package/src/db/EntityHistory.res.mjs +19 -12
  73. package/src/db/InternalTable.res +84 -35
  74. package/src/db/InternalTable.res.mjs +56 -23
  75. package/src/db/RollbackFloors.res +55 -0
  76. package/src/db/RollbackFloors.res.mjs +92 -0
  77. package/src/db/Table.res +22 -4
  78. package/src/db/Table.res.mjs +30 -9
  79. package/src/sources/Evm.res +2 -0
  80. package/src/sources/Evm.res.mjs +2 -0
  81. package/src/sources/EvmHyperSyncSource.res +3 -3
  82. package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
  83. package/src/sources/EvmRpcWs.res +124 -0
  84. package/src/sources/EvmRpcWs.res.mjs +117 -0
  85. package/src/sources/Fuel.res +2 -0
  86. package/src/sources/Fuel.res.mjs +2 -0
  87. package/src/sources/FuelHyperSyncSource.res +1 -1
  88. package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
  89. package/src/sources/HeightFeed.res +568 -0
  90. package/src/sources/HeightFeed.res.mjs +452 -0
  91. package/src/sources/HeightStream.res +257 -0
  92. package/src/sources/HeightStream.res.mjs +194 -0
  93. package/src/sources/HyperSync.res +5 -0
  94. package/src/sources/HyperSync.res.mjs +3 -0
  95. package/src/sources/HyperSync.resi +4 -0
  96. package/src/sources/HyperSyncSSE.res +49 -0
  97. package/src/sources/HyperSyncSSE.res.mjs +79 -0
  98. package/src/sources/RpcSource.res +1 -1
  99. package/src/sources/RpcSource.res.mjs +2 -2
  100. package/src/sources/Source.res +54 -1
  101. package/src/sources/Source.res.mjs +25 -0
  102. package/src/sources/SourceManager.res +220 -227
  103. package/src/sources/SourceManager.res.mjs +139 -164
  104. package/src/sources/SourceManager.resi +8 -0
  105. package/src/sources/Svm.res +2 -0
  106. package/src/sources/Svm.res.mjs +2 -0
  107. package/src/sources/SvmHyperSyncClient.res +70 -127
  108. package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
  109. package/src/sources/SvmHyperSyncSource.res +18 -15
  110. package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
  111. package/src/subgraph/blocks.ts +176 -0
  112. package/src/subgraph/calls.ts +217 -0
  113. package/src/subgraph/conformance.ts +102 -0
  114. package/src/subgraph/division.ts +133 -0
  115. package/src/subgraph/errors.ts +90 -0
  116. package/src/subgraph/graph-ts-types/VERSION +2 -0
  117. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
  118. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
  119. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
  120. package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
  121. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
  122. package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
  123. package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
  124. package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
  125. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  126. package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
  127. package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
  128. package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
  129. package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
  130. package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
  131. package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
  132. package/src/subgraph/graph-ts-types/index.d.ts +102 -0
  133. package/src/subgraph/graph-ts.ts +1948 -0
  134. package/src/subgraph/hosts.ts +148 -0
  135. package/src/subgraph/runtime.ts +863 -0
  136. package/src/subgraph/scope.ts +66 -0
  137. package/svm.schema.json +6 -81
  138. package/src/MemoryStorage.res +0 -729
  139. package/src/MemoryStorage.res.mjs +0 -586
  140. package/src/sources/HyperSyncHeightStream.res +0 -129
  141. package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
  142. package/src/sources/RpcWebSocketHeightStream.res +0 -175
  143. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
@@ -17,6 +17,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
17
17
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
18
18
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
19
19
  import * as ContractMapping from "./ContractMapping.res.mjs";
20
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
20
21
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
21
22
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
22
23
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
@@ -73,12 +74,6 @@ let publicConfigChainSchema = S$RescriptSchema.schema(s => ({
73
74
 
74
75
  let svmEventDescriptorSchema = S$RescriptSchema.schema(s => ({
75
76
  discriminator: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
76
- discriminatorByteLen: s.m(S$RescriptSchema.int),
77
- accountFilters: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.array(S$RescriptSchema.schema(s => ({
78
- position: s.m(S$RescriptSchema.int),
79
- values: s.m(S$RescriptSchema.array(S$RescriptSchema.string))
80
- })))))),
81
- isInner: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
82
77
  accounts: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.string))),
83
78
  args: s.m(S$RescriptSchema.option(S$RescriptSchema.json(false)))
84
79
  }));
@@ -226,6 +221,12 @@ function getFieldTypeAndSchema(prop, enumConfigsByName) {
226
221
  S$RescriptSchema.bool
227
222
  ];
228
223
  break;
224
+ case "bytes" :
225
+ match = [
226
+ "Bytea",
227
+ Utils.Schema.bytes
228
+ ];
229
+ break;
229
230
  case "date" :
230
231
  match = [
231
232
  "Date",
@@ -277,10 +278,20 @@ function getFieldTypeAndSchema(prop, enumConfigsByName) {
277
278
  match = Stdlib_JsError.throwWithMessage("Unknown field type in entity config: " + typ);
278
279
  }
279
280
  let baseSchema = match[1];
280
- let fieldSchema = isArray ? S$RescriptSchema.array(baseSchema) : baseSchema;
281
- let fieldSchema$1 = isNullable ? S$RescriptSchema.$$null(fieldSchema) : fieldSchema;
281
+ let fieldType = match[0];
282
+ let fieldSchema;
283
+ let exit = 0;
284
+ if (typeof fieldType !== "object" && fieldType === "Bytea" && isArray) {
285
+ fieldSchema = Utils.Schema.bytesArray;
286
+ } else {
287
+ exit = 1;
288
+ }
289
+ if (exit === 1) {
290
+ fieldSchema = isArray ? S$RescriptSchema.array(baseSchema) : baseSchema;
291
+ }
292
+ let fieldSchema$1 = isNullable ? Utils.Schema.nullTolerant(fieldSchema) : fieldSchema;
282
293
  return [
283
- match[0],
294
+ fieldType,
284
295
  fieldSchema$1,
285
296
  isNullable,
286
297
  isArray,
@@ -390,7 +401,8 @@ let publicConfigSchema = S$RescriptSchema.schema(s => ({
390
401
  fuel: s.m(S$RescriptSchema.option(publicConfigEcosystemSchema)),
391
402
  svm: s.m(S$RescriptSchema.option(publicConfigEcosystemSchema)),
392
403
  enums: s.m(S$RescriptSchema.option(S$RescriptSchema.dict(S$RescriptSchema.array(S$RescriptSchema.string)))),
393
- entities: s.m(S$RescriptSchema.option(S$RescriptSchema.array(entityJsonSchema)))
404
+ entities: s.m(S$RescriptSchema.option(S$RescriptSchema.array(entityJsonSchema))),
405
+ subgraph: s.m(S$RescriptSchema.option(S$RescriptSchema.json(false)))
394
406
  }));
395
407
 
396
408
  function contractMappingOf(chainConfigs) {
@@ -527,11 +539,7 @@ function fromPublic(publicConfigJson) {
527
539
  ) : addresses[0];
528
540
  let s = eventItem.svm;
529
541
  let svm = s !== undefined ? Primitive_option.valFromOption(s) : Stdlib_JsError.throwWithMessage(`SVM instruction ` + contractName + `.` + eventName + ` is missing the "svm" descriptor in internal config`);
530
- let accountFilters = Stdlib_Option.getOr(svm.accountFilters, []).map(group => group.map(af => ({
531
- position: af.position,
532
- values: af.values
533
- })));
534
- return EventConfigBuilder.buildSvmInstructionEventConfig(contractName, eventName, programId, svm.discriminator, svm.discriminatorByteLen, accountFilters, svm.isInner, Stdlib_Option.getOr(svm.accounts, []), Stdlib_Option.getOr(svm.args, null), svmDefinedTypes);
542
+ return EventConfigBuilder.buildSvmInstructionEventConfig(contractName, eventName, programId, svm.discriminator, Stdlib_Option.getOr(svm.accounts, []), Stdlib_Option.getOr(svm.args, null), svmDefinedTypes);
535
543
  }
536
544
  });
537
545
  } else {
@@ -722,10 +730,19 @@ function fromPublic(publicConfigJson) {
722
730
  isDev: Stdlib_Option.getOr(publicConfig.isDev, false),
723
731
  userEntitiesByName: userEntitiesByName,
724
732
  userEntities: userEntities,
725
- allEnums: allEnums
733
+ allEnums: allEnums,
734
+ subgraph: publicConfig.subgraph
726
735
  };
727
736
  }
728
737
 
738
+ function isIsolatedMultichain(config) {
739
+ if (ChainMap.keys(config.chainMap).length > 1) {
740
+ return config.userEntities.every(entityConfig => !entityConfig.crossChain);
741
+ } else {
742
+ return false;
743
+ }
744
+ }
745
+
729
746
  function normalizeUserAddress(config, address) {
730
747
  let match = config.ecosystem.name;
731
748
  switch (match) {
@@ -789,11 +806,15 @@ function getEventConfig(config, contractName, eventName, chainId) {
789
806
  });
790
807
  }
791
808
 
792
- function shouldSaveHistory(config, isInReorgThreshold) {
809
+ function shouldSaveHistory(config, isInReorgThreshold, chainId) {
793
810
  if (config.shouldSaveFullHistory) {
794
811
  return true;
795
- } else if (config.shouldRollbackOnReorg) {
796
- return isInReorgThreshold;
812
+ } else if (config.shouldRollbackOnReorg && isInReorgThreshold) {
813
+ if (chainId !== undefined && isIsolatedMultichain(config)) {
814
+ return ChainMap.get(config.chainMap, Primitive_option.valFromOption(chainId)).maxReorgDepth > 0;
815
+ } else {
816
+ return true;
817
+ }
797
818
  } else {
798
819
  return false;
799
820
  }
@@ -867,6 +888,7 @@ function stripSensitiveData(json) {
867
888
  stripChains(cloned["evm"]);
868
889
  stripChains(cloned["fuel"]);
869
890
  stripChains(cloned["svm"]);
891
+ Utils.Dict.deleteInPlace(cloned, "subgraph");
870
892
  }
871
893
  return cloned;
872
894
  }
@@ -1031,6 +1053,19 @@ function throwIfIncompatible(changedPaths, resetCommand, runCommand, hasClickhou
1031
1053
  Stdlib_JsError.throwWithMessage(`The following config changes are incompatible with the existing indexer data:\n\n` + bullets + `\n\nPick one:\n 1. ` + padTo(option1, col) + `# resume indexing where it left off\n 2. ` + padTo(resetCommand, col) + `# delete all indexed data and start over` + option3);
1032
1054
  }
1033
1055
 
1056
+ function throwIfResumeIncompatible(storedEnvioInfo, storedContractMapping, envioInfo, contractMapping, resetCommand, runCommand) {
1057
+ let changedPaths = storedEnvioInfo !== undefined ? diffPaths(storedEnvioInfo, envioInfo) : ["storage was initialized by an older envio version"];
1058
+ let changedPaths$1 = ContractMapping.isEqual(storedContractMapping, contractMapping) ? changedPaths : changedPaths.concat(["contracts"]);
1059
+ let hasClickhouse;
1060
+ if (typeof envioInfo === "object" && envioInfo !== null && !Array.isArray(envioInfo)) {
1061
+ let match = envioInfo["storage"];
1062
+ hasClickhouse = typeof match === "object" && match !== null && !Array.isArray(match) ? Primitive_object.equal(match["clickhouse"], true) : false;
1063
+ } else {
1064
+ hasClickhouse = false;
1065
+ }
1066
+ throwIfIncompatible(changedPaths$1, resetCommand, runCommand, hasClickhouse);
1067
+ }
1068
+
1034
1069
  function load() {
1035
1070
  let c = cached.contents;
1036
1071
  if (c !== undefined) {
@@ -1075,6 +1110,7 @@ export {
1075
1110
  publicConfigSchema,
1076
1111
  contractMappingOf,
1077
1112
  fromPublic,
1113
+ isIsolatedMultichain,
1078
1114
  normalizeUserAddress,
1079
1115
  normalizeSimulateAddress,
1080
1116
  getEventConfig,
@@ -1087,6 +1123,7 @@ export {
1087
1123
  canonicalJson,
1088
1124
  diffPaths,
1089
1125
  throwIfIncompatible,
1126
+ throwIfResumeIncompatible,
1090
1127
  load,
1091
1128
  getPgUserEntities,
1092
1129
  }
package/src/Core.res CHANGED
@@ -10,6 +10,7 @@ type svmHyperSyncClientCtor
10
10
  type fuelHyperSyncClientCtor
11
11
  type transactionStoreCtor
12
12
  type blockStoreCtor
13
+ type clickHouseSinkCtor
13
14
  type addressStoreCtor
14
15
  // Test-only: a local HyperSync server, bound by MockHyperSyncServer in envio-tests.
15
16
  type mockHyperSyncServerCtor
@@ -26,9 +27,18 @@ type fromUserApiResult = {
26
27
  indexerCode: Null.t<string>,
27
28
  }
28
29
 
30
+ type fromSubgraphOptions = {
31
+ name?: string,
32
+ env?: dict<string>,
33
+ files?: dict<string>,
34
+ root?: string,
35
+ }
36
+
29
37
  type addon = {
30
38
  getConfigJson: (~configPath: Null.t<string>, ~directory: Null.t<string>) => string,
39
+ fromSubgraph: (string, string, fromSubgraphOptions) => fromUserApiResult,
31
40
  encodeIndexedTopic: (~abiType: string, ~value: unknown) => EvmTypes.Hex.t,
41
+ isSvmPubkey: (~value: string) => bool,
32
42
  fromUserApi: (string, fromUserApiOptions) => fromUserApiResult,
33
43
  runCli: (~args: array<string>, ~envioPackageDir: Null.t<string>) => promise<Null.t<string>>,
34
44
  @as("EvmHyperSyncClient")
@@ -45,6 +55,8 @@ type addon = {
45
55
  blockStore: blockStoreCtor,
46
56
  @as("AddressStore")
47
57
  addressStore: addressStoreCtor,
58
+ @as("ClickHouseSink")
59
+ clickHouseSink: clickHouseSinkCtor,
48
60
  @as("MockHyperSyncServer")
49
61
  mockHyperSyncServer: mockHyperSyncServerCtor,
50
62
  encodeAddresses: (~ecosystem: string, ~addresses: array<Address.t>) => array<NodeJs.Buffer.t>,
@@ -257,6 +269,13 @@ let fromUserApi = (~schema=?, ~env=?, ~files=?, ~withIndexerTypes=false, yaml) =
257
269
  )
258
270
  }
259
271
 
272
+ /// Parses a subgraph project without touching the filesystem: subgraph.yaml and
273
+ /// its schema inline, ABI bodies through `files`.
274
+ let fromSubgraph = (~name=?, ~env=?, ~files=?, ~root=?, ~manifest, ~schema) => {
275
+ let addon = getAddon()
276
+ addon.fromSubgraph(manifest, schema, {?name, ?env, ?files, ?root})
277
+ }
278
+
260
279
  let runCli = args => {
261
280
  let addon = getAddon()
262
281
  addon.runCli(~args, ~envioPackageDir=Null.make(envioPackageDir))
package/src/Core.res.mjs CHANGED
@@ -195,6 +195,16 @@ function fromUserApi(schema, env, files, withIndexerTypesOpt, yaml) {
195
195
  });
196
196
  }
197
197
 
198
+ function fromSubgraph(name, env, files, root, manifest, schema) {
199
+ let addon = getAddon();
200
+ return addon.fromSubgraph(manifest, schema, {
201
+ name: name,
202
+ env: env,
203
+ files: files,
204
+ root: root
205
+ });
206
+ }
207
+
198
208
  function runCli(args) {
199
209
  let addon = getAddon();
200
210
  return addon.runCli(args, envioPackageDir);
@@ -212,6 +222,7 @@ export {
212
222
  getAddon,
213
223
  getConfigJson,
214
224
  fromUserApi,
225
+ fromSubgraph,
215
226
  runCli,
216
227
  }
217
228
  /* envioPackageDir Not a pure module */
@@ -72,31 +72,23 @@ let totalReadyCount = (crossChainState: t) => {
72
72
  let nextItemIsNone = (crossChainState: t): bool =>
73
73
  !(crossChainState.chainStates->Dict.valuesToArray->Array.some(ChainState.hasReadyItem))
74
74
 
75
- let getSafeCheckpointId = (crossChainState: t) => {
76
- let result: ref<option<bigint>> = ref(None)
77
-
78
- for i in 0 to crossChainState.chainIds->Array.length - 1 {
79
- let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
80
- switch cs->ChainState.safeCheckpointTracking {
81
- | None => () // Skip chains with maxReorgDepth = 0
82
- | Some(safeCheckpointTracking) =>
83
- let safeCheckpointId =
84
- safeCheckpointTracking->SafeCheckpointTracking.getSafeCheckpointId(
75
+ // Each chain's safe checkpoint: the last one a reorg on that chain can no longer
76
+ // reach, or None while it has nothing safe yet. A chain that can't be rolled
77
+ // back (maxReorgDepth = 0) tracks none, and everything it has committed is safe.
78
+ let getSafeCheckpointIdByChain = (crossChainState: t, ~committedCheckpointId) =>
79
+ crossChainState.chainIds->Array.map(chainId => {
80
+ let cs = crossChainState->getChainState(chainId)
81
+ (
82
+ chainId,
83
+ switch cs->ChainState.safeCheckpointTracking {
84
+ | None => Some(committedCheckpointId)
85
+ | Some(tracking) =>
86
+ tracking->SafeCheckpointTracking.getSafeCheckpointId(
85
87
  ~sourceBlockNumber=cs->ChainState.knownHeight,
86
88
  )
87
- switch result.contents {
88
- | None => result := Some(safeCheckpointId)
89
- | Some(current) if safeCheckpointId < current => result := Some(safeCheckpointId)
90
- | _ => ()
91
- }
92
- }
93
- }
94
-
95
- switch result.contents {
96
- | Some(id) if id > 0n => Some(id)
97
- | _ => None // No safe checkpoint found
98
- }
99
- }
89
+ },
90
+ )
91
+ })
100
92
 
101
93
  // --- Cross-chain transitions. ---
102
94
 
@@ -65,23 +65,15 @@ function nextItemIsNone(crossChainState) {
65
65
  return !Object.values(crossChainState.chainStates).some(ChainState.hasReadyItem);
66
66
  }
67
67
 
68
- function getSafeCheckpointId(crossChainState) {
69
- let result;
70
- for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
71
- let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
72
- let safeCheckpointTracking = ChainState.safeCheckpointTracking(cs);
73
- if (safeCheckpointTracking !== undefined) {
74
- let safeCheckpointId = SafeCheckpointTracking.getSafeCheckpointId(safeCheckpointTracking, ChainState.knownHeight(cs));
75
- let current = result;
76
- if (!(current !== undefined && safeCheckpointId >= current)) {
77
- result = safeCheckpointId;
78
- }
79
- }
80
- }
81
- let id = result;
82
- if (id !== undefined && id > 0n) {
83
- return id;
84
- }
68
+ function getSafeCheckpointIdByChain(crossChainState, committedCheckpointId) {
69
+ return crossChainState.chainIds.map(chainId => {
70
+ let cs = crossChainState.chainStates[chainId];
71
+ let tracking = ChainState.safeCheckpointTracking(cs);
72
+ return [
73
+ chainId,
74
+ tracking !== undefined ? SafeCheckpointTracking.getSafeCheckpointId(tracking, ChainState.knownHeight(cs)) : committedCheckpointId
75
+ ];
76
+ });
85
77
  }
86
78
 
87
79
  function createBatch(crossChainState, processedCheckpointId, batchSizeTarget, isRollback) {
@@ -261,7 +253,7 @@ export {
261
253
  isInReorgThreshold,
262
254
  targetBufferSize,
263
255
  nextItemIsNone,
264
- getSafeCheckpointId,
256
+ getSafeCheckpointIdByChain,
265
257
  createBatch,
266
258
  enterReorgThreshold,
267
259
  applyBatchProgress,
@@ -21,7 +21,10 @@ let targetBufferSize: t => int
21
21
 
22
22
  // Derived (pure).
23
23
  let nextItemIsNone: t => bool
24
- let getSafeCheckpointId: t => option<Internal.checkpointId>
24
+ let getSafeCheckpointIdByChain: (
25
+ t,
26
+ ~committedCheckpointId: Internal.checkpointId,
27
+ ) => array<(ChainId.t, option<Internal.checkpointId>)>
25
28
 
26
29
  // Cross-chain transitions.
27
30
  let createBatch: (
package/src/Ecosystem.res CHANGED
@@ -10,6 +10,11 @@ type t = {
10
10
  for SVM. Centralised here so adding a new ecosystem only requires a
11
11
  new ecosystem record, not another switch in `Main.res`. */
12
12
  onBlockMethodName: string,
13
+ /** What the ecosystem calls a contract and one of its events, for
14
+ registration errors that are shared across ecosystems — "contract" /
15
+ "event" on EVM and Fuel, "program" / "instruction" on SVM. */
16
+ contractNoun: string,
17
+ eventNoun: string,
13
18
  /** Schema that unwraps the ecosystem-specific outer wrapper around the
14
19
  user's `where`-returned filter (`block.number` on EVM, `block.height`
15
20
  on Fuel, `slot` on SVM) and surfaces the raw inner `{_gte?, _lte?,
@@ -23,8 +28,8 @@ type t = {
23
28
  `option<unknown>`. Separate from `onBlockFilterSchema` because event
24
29
  block filters support only `_gte` (→ per-event `startBlock`) — `_lte`
25
30
  and `_every` are rejected by the inner `eventBlockRangeSchema` in
26
- `LogSelection.res`. SVM does not support event handlers, so its
27
- schema always surfaces `None`. */
31
+ `LogSelection.res`. SVM parses its own `where` (including the
32
+ `block.slot` chunk), so its schema always surfaces `None`. */
28
33
  onEventBlockFilterSchema: S.t<option<unknown>>,
29
34
  /** Base logger injected at construction. Used to build per-item child
30
35
  loggers (see `getItemLogger`). */
package/src/Envio.res CHANGED
@@ -83,10 +83,12 @@ type svmInstruction = {
83
83
  instructionName: string,
84
84
  discriminator: string,
85
85
  programId?: SvmTypes.Pubkey.t,
86
- data?: string,
86
+ data?: Uint8Array.t,
87
87
  path?: array<int>,
88
88
  isInner?: bool,
89
- args?: JSON.t,
89
+ // Decoded Borsh args; wide integers (u64/u128/i64/i128) are bigint, so the
90
+ // tree is not valid JSON.
91
+ args?: unknown,
90
92
  accounts?: dict<svmInstructionAccount>,
91
93
  accountArguments?: array<SvmTypes.Pubkey.t>,
92
94
  transaction?: svmTransaction,
@@ -283,9 +285,9 @@ type svmSimulateItem = {
283
285
  slot?: int,
284
286
  path?: array<int>,
285
287
  programId?: string,
286
- data?: string,
288
+ data?: Uint8Array.t,
287
289
  isInner?: bool,
288
- args?: JSON.t,
290
+ args?: unknown,
289
291
  accounts?: dict<{address: string}>,
290
292
  accountArguments?: array<string>,
291
293
  logs?: array<{kind?: string, message?: string}>,
@@ -317,9 +317,6 @@ let svmSelectionKinds = ["instruction", "transaction", "accountActivity", "block
317
317
  // typo, and an unrecognised key would read as an empty selection — silently
318
318
  // dropping every field `config.yaml` selected. Rejected here so the option is
319
319
  // held to the same shape whether or not the project type-checks it.
320
- let quotedJoin = (names: array<string>) =>
321
- names->Array.map(name => `"${name}"`)->Array.joinUnsafe(", ")
322
-
323
320
  let validateFieldsShapeOrThrow = (
324
321
  fields: unknown,
325
322
  ~registration: string,
@@ -337,7 +334,7 @@ let validateFieldsShapeOrThrow = (
337
334
  ->Array.forEach(key =>
338
335
  if !(validKeys->Array.includes(key)) {
339
336
  JsError.throwWithMessage(
340
- `Invalid "${key}" key in the fields option of ${registration}. Valid keys: ${quotedJoin(
337
+ `Invalid "${key}" key in the fields option of ${registration}. Valid keys: ${Utils.Array.quotedJoin(
341
338
  validKeys,
342
339
  )}.`,
343
340
  )
@@ -373,7 +370,7 @@ let parseFieldsOrThrow = (
373
370
  fields->Array.forEach(name => {
374
371
  if !(valid->Utils.Set.has(name)) {
375
372
  JsError.throwWithMessage(
376
- `Invalid "${name}" field in the fields.${kind} option of ${registration}. Valid ${kind} fields: ${quotedJoin(
373
+ `Invalid "${name}" field in the fields.${kind} option of ${registration}. Valid ${kind} fields: ${Utils.Array.quotedJoin(
377
374
  valid->Utils.Set.toArray,
378
375
  )}.`,
379
376
  )
@@ -577,7 +574,7 @@ let resolveSvmInlineFieldSelection = (
577
574
  ~contractName: string,
578
575
  ~eventName: string,
579
576
  ): Internal.fieldSelection => {
580
- let registration = `the "${eventName}" event registration on contract "${contractName}"`
577
+ let registration = `the "${eventName}" instruction on program "${contractName}"`
581
578
  validateFieldsShapeOrThrow(
582
579
  fields,
583
580
  ~registration,
@@ -638,9 +635,6 @@ let buildSvmInstructionEventConfig = (
638
635
  ~instructionName: string,
639
636
  ~programId: SvmTypes.Pubkey.t,
640
637
  ~discriminator: option<string>,
641
- ~discriminatorByteLen: int,
642
- ~accountFilters: array<Internal.svmAccountFilterGroup>,
643
- ~isInner: option<bool>,
644
638
  ~accounts: array<string>=[],
645
639
  ~args: JSON.t=JSON.Null,
646
640
  ~definedTypes: JSON.t=JSON.Null,
@@ -667,38 +661,187 @@ let buildSvmInstructionEventConfig = (
667
661
  simulateParamsSchema: paramsSchema,
668
662
  programId,
669
663
  discriminator,
670
- discriminatorByteLen,
671
664
  fieldSelection,
672
- accountFilters,
673
- isInner,
674
665
  accounts,
675
666
  args,
676
667
  definedTypes,
677
668
  }
678
669
  }
679
670
 
680
- // Enrich an SVM definition into a registration. SVM has no `where`; only the
681
- // handler binding + wildcard-derived address gate are registration state.
671
+ // ============== SVM `where` ==============
672
+
673
+ type parsedSvmWhere = {
674
+ // Disjunctive normal form: outer array is OR of AND-groups. Empty means the
675
+ // registration accepts any accounts.
676
+ accountFilters: array<Internal.svmAccountFilterGroup>,
677
+ isInner: option<bool>,
678
+ startBlock: option<int>,
679
+ }
680
+
681
+ let validSvmWhereKeys = ["accounts", "isInner", "block"]
682
+
683
+ // `{slot: {_gte?}}`. Both levels are strict: the inner `eventBlockRangeSchema`
684
+ // rejects the `_lte` / `_every` that only `onSlot` supports, the outer rejects
685
+ // a wrapper key other than `slot`.
686
+ type svmBlockFilter = {slot?: LogSelection.eventBlockRange}
687
+ let svmBlockFilterSchema: S.t<svmBlockFilter> = S.object(s => {
688
+ slot: ?s.field("slot", S.option(LogSelection.eventBlockRangeSchema)),
689
+ })->S.strict
690
+
691
+ // The source query narrows accounts through `a0..a9`, so only an instruction's
692
+ // leading account slots can be filtered.
693
+ let filterableAccountCount = 10
694
+
695
+ // Resolve the `where` option of an `onInstruction` registration. Account names
696
+ // are resolved against the instruction's declared accounts here, so the
697
+ // registration carries positions and nothing downstream needs the names.
698
+ let resolveSvmWhereOrThrow = (
699
+ where: JSON.t,
700
+ ~contractName: string,
701
+ ~eventName: string,
702
+ ~accountNames: array<string>,
703
+ ): parsedSvmWhere => {
704
+ let invalid = message =>
705
+ JsError.throwWithMessage(
706
+ `Invalid where configuration for the "${eventName}" instruction on program "${contractName}". ${message}`,
707
+ )
708
+
709
+ let obj = switch where {
710
+ | Object(obj) => obj
711
+ | _ => invalid("Expected an object.")
712
+ }
713
+ obj->Utils.Dict.forEachWithKey((_, key) =>
714
+ if !(validSvmWhereKeys->Array.includes(key)) {
715
+ invalid(`Unknown field "${key}". Valid fields: ${Utils.Array.quotedJoin(validSvmWhereKeys)}.`)
716
+ }
717
+ )
718
+
719
+ let isInner = switch obj->Dict.get("isInner") {
720
+ | None => None
721
+ | Some(Boolean(isInner)) => Some(isInner)
722
+ | Some(_) => invalid(`The "isInner" filter must be a boolean.`)
723
+ }
724
+
725
+ let parseGroup = (group: dict<JSON.t>): Internal.svmAccountFilterGroup =>
726
+ group
727
+ ->Dict.toArray
728
+ ->Array.map(((name, value)) => {
729
+ let position = switch accountNames->Array.indexOf(name) {
730
+ | -1 if accountNames->Utils.Array.isEmpty =>
731
+ invalid(
732
+ "The instruction has no named accounts to filter on. Add `accounts` and `args` to it in config.yaml, or attach an IDL.",
733
+ )
734
+ | -1 =>
735
+ invalid(
736
+ `The instruction has no account named "${name}" to filter on. Named accounts: ${Utils.Array.quotedJoin(
737
+ accountNames,
738
+ )}.`,
739
+ )
740
+ | position if position >= filterableAccountCount =>
741
+ invalid(
742
+ `Account "${name}" is at position ${position->Int.toString}, and only the first ${filterableAccountCount->Int.toString} accounts of an instruction can be filtered.`,
743
+ )
744
+ | position => position
745
+ }
746
+ let values = value->normalizeOrThrow
747
+ if values->Utils.Array.isEmpty {
748
+ invalid(`The "${name}" filter must list at least one pubkey.`)
749
+ }
750
+ {
751
+ Internal.position,
752
+ values: values->Array.map(value =>
753
+ switch value {
754
+ | JSON.String(pubkey) if Core.getAddon().isSvmPubkey(~value=pubkey) =>
755
+ pubkey->SvmTypes.Pubkey.fromStringUnsafe
756
+ | JSON.String(pubkey) =>
757
+ invalid(`The "${name}" filter value "${pubkey}" is not a base58 SVM pubkey.`)
758
+ | _ => invalid(`The "${name}" filter must list base58 pubkeys as strings.`)
759
+ }
760
+ ),
761
+ }
762
+ })
763
+
764
+ // An empty AND-group matches every instruction, so it makes the whole OR
765
+ // vacuous. Normalized away here so the source builds one unfiltered
766
+ // selection instead of a match-all selection beside the narrower ones.
767
+ let groups = switch obj->Dict.get("accounts") {
768
+ | None => []
769
+ | Some(Object(group)) => [parseGroup(group)]
770
+ | Some(Array(entries)) =>
771
+ entries->Array.map(entry =>
772
+ switch entry {
773
+ | Object(group) => parseGroup(group)
774
+ | _ => invalid(`Each entry in "accounts" must be an object.`)
775
+ }
776
+ )
777
+ | Some(_) => invalid(`Expected "accounts" to be an object or an array of objects.`)
778
+ }
779
+ let accountFilters = groups->Array.some(Utils.Array.isEmpty) ? [] : groups
780
+
781
+ let startBlock = switch obj->Dict.get("block") {
782
+ | None => None
783
+ | Some(block) =>
784
+ let filter = try block->S.parseOrThrow(svmBlockFilterSchema) catch {
785
+ | S.Raised(exn) =>
786
+ invalid(
787
+ `The "block" filter is invalid: ${exn
788
+ ->Utils.prettifyExn
789
+ ->(
790
+ Utils.magic: exn => string
791
+ )}. Only \`_gte\` is supported on instruction filters — use \`indexer.onSlot\` for \`_lte\` or \`_every\`.`,
792
+ )
793
+ }
794
+ switch filter.slot {
795
+ | Some({_gte}) => _gte
796
+ | None => None
797
+ }
798
+ }
799
+
800
+ {accountFilters, isInner, startBlock}
801
+ }
802
+
803
+ // Enrich an SVM definition into a registration: the handler binding plus the
804
+ // `where`-derived account/isInner filters and startBlock override.
682
805
  let buildSvmOnEventRegistration = (
683
806
  ~eventConfig: Internal.svmInstructionEventConfig,
684
807
  ~isWildcard: bool,
685
808
  ~handler: option<Internal.handler>,
686
809
  ~contractRegister: option<Internal.contractRegister>,
810
+ ~where: option<JSON.t>,
687
811
  ~fieldSelection: option<Internal.fieldSelection>=?,
688
812
  ~startBlock: option<int>=?,
689
813
  ): Internal.svmOnEventRegistration => {
690
- index: -1,
691
- eventConfig: (eventConfig :> Internal.eventConfig),
692
- handler,
693
- contractRegister,
694
- isWildcard,
695
- filterByAddresses: false,
696
- dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses=false),
697
- startBlock,
698
- fieldSelection: switch fieldSelection {
699
- | Some(fieldSelection) => fieldSelection
700
- | None => eventConfig.fieldSelection
701
- },
814
+ let resolvedWhere = switch where {
815
+ | None => {accountFilters: [], isInner: None, startBlock: None}
816
+ | Some(where) =>
817
+ where->resolveSvmWhereOrThrow(
818
+ ~contractName=eventConfig.contractName,
819
+ ~eventName=eventConfig.name,
820
+ ~accountNames=eventConfig.accounts,
821
+ )
822
+ }
823
+
824
+ {
825
+ index: -1,
826
+ eventConfig: (eventConfig :> Internal.eventConfig),
827
+ handler,
828
+ contractRegister,
829
+ isWildcard,
830
+ filterByAddresses: false,
831
+ dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses=false),
832
+ // `where.block.slot._gte` overrides the program-level startBlock when
833
+ // present, mirroring EVM's `where.block.number._gte`.
834
+ startBlock: switch resolvedWhere.startBlock {
835
+ | Some(_) as sb => sb
836
+ | None => startBlock
837
+ },
838
+ fieldSelection: switch fieldSelection {
839
+ | Some(fieldSelection) => fieldSelection
840
+ | None => eventConfig.fieldSelection
841
+ },
842
+ accountFilters: resolvedWhere.accountFilters,
843
+ isInner: resolvedWhere.isInner,
844
+ }
702
845
  }
703
846
 
704
847
  // ============== Build Fuel event config ==============