envio 3.0.0-alpha.21 → 3.0.0-alpha.23

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 (220) hide show
  1. package/README.md +3 -3
  2. package/bin.mjs +2 -48
  3. package/evm.schema.json +67 -0
  4. package/fuel.schema.json +67 -0
  5. package/index.d.ts +822 -38
  6. package/index.js +5 -3
  7. package/package.json +10 -8
  8. package/rescript.json +5 -9
  9. package/src/Address.res +4 -5
  10. package/src/Address.res.mjs +9 -12
  11. package/src/Api.res +15 -0
  12. package/src/Api.res.mjs +20 -0
  13. package/src/Batch.res +32 -34
  14. package/src/Batch.res.mjs +172 -187
  15. package/src/Bin.res +89 -0
  16. package/src/Bin.res.mjs +97 -0
  17. package/src/ChainFetcher.res +33 -57
  18. package/src/ChainFetcher.res.mjs +197 -227
  19. package/src/ChainManager.res +6 -14
  20. package/src/ChainManager.res.mjs +74 -85
  21. package/src/ChainMap.res +14 -16
  22. package/src/ChainMap.res.mjs +38 -38
  23. package/src/Config.res +193 -135
  24. package/src/Config.res.mjs +566 -592
  25. package/src/Core.res +182 -0
  26. package/src/Core.res.mjs +207 -0
  27. package/src/Ecosystem.res +25 -4
  28. package/src/Ecosystem.res.mjs +12 -13
  29. package/src/Env.res +20 -13
  30. package/src/Env.res.mjs +124 -113
  31. package/src/EnvSafe.res +269 -0
  32. package/src/EnvSafe.res.mjs +296 -0
  33. package/src/EnvSafe.resi +18 -0
  34. package/src/Envio.res +37 -26
  35. package/src/Envio.res.mjs +59 -60
  36. package/src/ErrorHandling.res +2 -2
  37. package/src/ErrorHandling.res.mjs +15 -15
  38. package/src/EventConfigBuilder.res +219 -81
  39. package/src/EventConfigBuilder.res.mjs +259 -202
  40. package/src/EventProcessing.res +27 -38
  41. package/src/EventProcessing.res.mjs +165 -183
  42. package/src/EventUtils.res +11 -11
  43. package/src/EventUtils.res.mjs +21 -22
  44. package/src/EvmTypes.res +0 -1
  45. package/src/EvmTypes.res.mjs +5 -5
  46. package/src/FetchState.res +360 -256
  47. package/src/FetchState.res.mjs +958 -914
  48. package/src/GlobalState.res +365 -351
  49. package/src/GlobalState.res.mjs +958 -992
  50. package/src/GlobalStateManager.res +1 -2
  51. package/src/GlobalStateManager.res.mjs +36 -44
  52. package/src/HandlerLoader.res +107 -23
  53. package/src/HandlerLoader.res.mjs +128 -38
  54. package/src/HandlerRegister.res +127 -103
  55. package/src/HandlerRegister.res.mjs +164 -164
  56. package/src/HandlerRegister.resi +12 -4
  57. package/src/Hasura.res +35 -22
  58. package/src/Hasura.res.mjs +158 -167
  59. package/src/InMemoryStore.res +20 -27
  60. package/src/InMemoryStore.res.mjs +64 -80
  61. package/src/InMemoryTable.res +34 -39
  62. package/src/InMemoryTable.res.mjs +165 -170
  63. package/src/Internal.res +52 -33
  64. package/src/Internal.res.mjs +84 -81
  65. package/src/LazyLoader.res.mjs +55 -61
  66. package/src/LoadLayer.res +77 -78
  67. package/src/LoadLayer.res.mjs +160 -189
  68. package/src/LoadManager.res +16 -21
  69. package/src/LoadManager.res.mjs +79 -84
  70. package/src/LogSelection.res +236 -68
  71. package/src/LogSelection.res.mjs +211 -141
  72. package/src/Logging.res +13 -9
  73. package/src/Logging.res.mjs +130 -143
  74. package/src/Main.res +430 -51
  75. package/src/Main.res.mjs +530 -271
  76. package/src/Persistence.res +80 -84
  77. package/src/Persistence.res.mjs +131 -132
  78. package/src/PgStorage.res +294 -167
  79. package/src/PgStorage.res.mjs +799 -817
  80. package/src/Prometheus.res +50 -58
  81. package/src/Prometheus.res.mjs +345 -373
  82. package/src/ReorgDetection.res +22 -24
  83. package/src/ReorgDetection.res.mjs +100 -106
  84. package/src/SafeCheckpointTracking.res +7 -7
  85. package/src/SafeCheckpointTracking.res.mjs +40 -43
  86. package/src/SimulateItems.res +41 -49
  87. package/src/SimulateItems.res.mjs +257 -272
  88. package/src/Sink.res +2 -2
  89. package/src/Sink.res.mjs +22 -26
  90. package/src/TableIndices.res +1 -2
  91. package/src/TableIndices.res.mjs +42 -48
  92. package/src/TestIndexer.res +196 -189
  93. package/src/TestIndexer.res.mjs +536 -536
  94. package/src/TestIndexerProxyStorage.res +16 -16
  95. package/src/TestIndexerProxyStorage.res.mjs +99 -122
  96. package/src/TestIndexerWorker.res +4 -0
  97. package/src/TestIndexerWorker.res.mjs +7 -0
  98. package/src/Throttler.res +3 -3
  99. package/src/Throttler.res.mjs +23 -24
  100. package/src/Time.res +1 -1
  101. package/src/Time.res.mjs +18 -21
  102. package/src/TopicFilter.res +3 -3
  103. package/src/TopicFilter.res.mjs +29 -30
  104. package/src/UserContext.res +93 -54
  105. package/src/UserContext.res.mjs +197 -182
  106. package/src/Utils.res +141 -86
  107. package/src/Utils.res.mjs +334 -295
  108. package/src/bindings/BigDecimal.res +0 -2
  109. package/src/bindings/BigDecimal.res.mjs +19 -23
  110. package/src/bindings/ClickHouse.res +28 -27
  111. package/src/bindings/ClickHouse.res.mjs +243 -240
  112. package/src/bindings/DateFns.res +11 -11
  113. package/src/bindings/DateFns.res.mjs +7 -7
  114. package/src/bindings/EventSource.res.mjs +2 -2
  115. package/src/bindings/Express.res +2 -5
  116. package/src/bindings/Hrtime.res +2 -2
  117. package/src/bindings/Hrtime.res.mjs +30 -32
  118. package/src/bindings/Lodash.res.mjs +1 -1
  119. package/src/bindings/NodeJs.res +14 -9
  120. package/src/bindings/NodeJs.res.mjs +20 -20
  121. package/src/bindings/Pino.res +8 -10
  122. package/src/bindings/Pino.res.mjs +40 -43
  123. package/src/bindings/Postgres.res +7 -5
  124. package/src/bindings/Postgres.res.mjs +9 -9
  125. package/src/bindings/PromClient.res +17 -2
  126. package/src/bindings/PromClient.res.mjs +30 -7
  127. package/src/bindings/SDSL.res.mjs +2 -2
  128. package/src/bindings/Viem.res +4 -4
  129. package/src/bindings/Viem.res.mjs +20 -22
  130. package/src/bindings/Vitest.res +1 -1
  131. package/src/bindings/Vitest.res.mjs +2 -2
  132. package/src/bindings/WebSocket.res +1 -1
  133. package/src/db/EntityHistory.res +9 -3
  134. package/src/db/EntityHistory.res.mjs +84 -59
  135. package/src/db/InternalTable.res +62 -60
  136. package/src/db/InternalTable.res.mjs +271 -203
  137. package/src/db/Schema.res +1 -2
  138. package/src/db/Schema.res.mjs +28 -32
  139. package/src/db/Table.res +28 -27
  140. package/src/db/Table.res.mjs +276 -292
  141. package/src/sources/EventRouter.res +21 -16
  142. package/src/sources/EventRouter.res.mjs +55 -57
  143. package/src/sources/Evm.res +17 -1
  144. package/src/sources/Evm.res.mjs +16 -8
  145. package/src/sources/EvmChain.res +15 -17
  146. package/src/sources/EvmChain.res.mjs +40 -42
  147. package/src/sources/Fuel.res +14 -1
  148. package/src/sources/Fuel.res.mjs +16 -8
  149. package/src/sources/FuelSDK.res +1 -1
  150. package/src/sources/FuelSDK.res.mjs +6 -8
  151. package/src/sources/HyperFuel.res +8 -10
  152. package/src/sources/HyperFuel.res.mjs +113 -123
  153. package/src/sources/HyperFuelClient.res.mjs +6 -7
  154. package/src/sources/HyperFuelSource.res +19 -20
  155. package/src/sources/HyperFuelSource.res.mjs +339 -356
  156. package/src/sources/HyperSync.res +11 -13
  157. package/src/sources/HyperSync.res.mjs +206 -220
  158. package/src/sources/HyperSyncClient.res +5 -7
  159. package/src/sources/HyperSyncClient.res.mjs +70 -75
  160. package/src/sources/HyperSyncHeightStream.res +8 -9
  161. package/src/sources/HyperSyncHeightStream.res.mjs +78 -86
  162. package/src/sources/HyperSyncJsonApi.res +18 -15
  163. package/src/sources/HyperSyncJsonApi.res.mjs +201 -231
  164. package/src/sources/HyperSyncSource.res +17 -21
  165. package/src/sources/HyperSyncSource.res.mjs +268 -290
  166. package/src/sources/Rpc.res +5 -5
  167. package/src/sources/Rpc.res.mjs +168 -192
  168. package/src/sources/RpcSource.res +166 -167
  169. package/src/sources/RpcSource.res.mjs +972 -1046
  170. package/src/sources/RpcWebSocketHeightStream.res +10 -11
  171. package/src/sources/RpcWebSocketHeightStream.res.mjs +131 -145
  172. package/src/sources/SimulateSource.res +1 -1
  173. package/src/sources/SimulateSource.res.mjs +35 -38
  174. package/src/sources/Source.res +1 -1
  175. package/src/sources/Source.res.mjs +3 -3
  176. package/src/sources/SourceManager.res +39 -20
  177. package/src/sources/SourceManager.res.mjs +340 -371
  178. package/src/sources/SourceManager.resi +2 -1
  179. package/src/sources/Svm.res +12 -5
  180. package/src/sources/Svm.res.mjs +44 -41
  181. package/src/tui/Tui.res +23 -12
  182. package/src/tui/Tui.res.mjs +292 -290
  183. package/src/tui/bindings/Ink.res +2 -4
  184. package/src/tui/bindings/Ink.res.mjs +35 -41
  185. package/src/tui/components/BufferedProgressBar.res +7 -7
  186. package/src/tui/components/BufferedProgressBar.res.mjs +46 -46
  187. package/src/tui/components/CustomHooks.res +1 -2
  188. package/src/tui/components/CustomHooks.res.mjs +102 -122
  189. package/src/tui/components/Messages.res +1 -2
  190. package/src/tui/components/Messages.res.mjs +38 -42
  191. package/src/tui/components/SyncETA.res +10 -11
  192. package/src/tui/components/SyncETA.res.mjs +178 -196
  193. package/src/tui/components/TuiData.res +1 -1
  194. package/src/tui/components/TuiData.res.mjs +7 -6
  195. package/src/vendored/Rest.res +52 -66
  196. package/src/vendored/Rest.res.mjs +324 -364
  197. package/svm.schema.json +67 -0
  198. package/src/Address.gen.ts +0 -8
  199. package/src/Config.gen.ts +0 -19
  200. package/src/Envio.gen.ts +0 -55
  201. package/src/EvmTypes.gen.ts +0 -6
  202. package/src/InMemoryStore.gen.ts +0 -6
  203. package/src/Internal.gen.ts +0 -64
  204. package/src/PgStorage.gen.ts +0 -10
  205. package/src/PgStorage.res.d.mts +0 -5
  206. package/src/Types.ts +0 -56
  207. package/src/bindings/BigDecimal.gen.ts +0 -14
  208. package/src/bindings/BigDecimal.res.d.mts +0 -5
  209. package/src/bindings/BigInt.gen.ts +0 -10
  210. package/src/bindings/BigInt.res +0 -70
  211. package/src/bindings/BigInt.res.d.mts +0 -5
  212. package/src/bindings/BigInt.res.mjs +0 -154
  213. package/src/bindings/Ethers.res.d.mts +0 -5
  214. package/src/bindings/Pino.gen.ts +0 -17
  215. package/src/bindings/Postgres.gen.ts +0 -8
  216. package/src/bindings/Postgres.res.d.mts +0 -5
  217. package/src/bindings/Promise.res +0 -67
  218. package/src/bindings/Promise.res.mjs +0 -26
  219. package/src/db/InternalTable.gen.ts +0 -36
  220. package/src/sources/HyperSyncClient.gen.ts +0 -19
package/src/Config.res CHANGED
@@ -1,5 +1,3 @@
1
- open Belt
2
-
3
1
  type sourceSyncOptions = {
4
2
  initialBlockInterval?: int,
5
3
  backoffMultiplicative?: float,
@@ -21,7 +19,7 @@ type contract = {
21
19
  eventSignatures: array<string>,
22
20
  }
23
21
 
24
- // Source config parsed from internal.config.json - sources are created lazily in ChainFetcher
22
+ // Sources are instantiated lazily in ChainFetcher from this config.
25
23
  type evmRpcConfig = {
26
24
  url: string,
27
25
  sourceFor: Source.sourceFor,
@@ -62,6 +60,11 @@ type multichain = Internal.multichain =
62
60
  | @as("ordered") Ordered
63
61
  | @as("unordered") Unordered
64
62
 
63
+ type storage = {
64
+ postgres: bool,
65
+ clickhouse: bool,
66
+ }
67
+
65
68
  type contractHandler = {
66
69
  name: string,
67
70
  handler: option<string>,
@@ -74,6 +77,7 @@ type t = {
74
77
  contractHandlers: array<contractHandler>,
75
78
  shouldRollbackOnReorg: bool,
76
79
  shouldSaveFullHistory: bool,
80
+ storage: storage,
77
81
  multichain: multichain,
78
82
  chainMap: ChainMap.t<chain>,
79
83
  defaultChain: option<chain>,
@@ -82,45 +86,44 @@ type t = {
82
86
  maxAddrInPartition: int,
83
87
  batchSize: int,
84
88
  lowercaseAddresses: bool,
85
- addContractNameToContractNameMapping: dict<string>,
86
89
  userEntitiesByName: dict<Internal.entityConfig>,
87
90
  userEntities: array<Internal.entityConfig>,
88
91
  allEntities: array<Internal.entityConfig>,
89
92
  allEnums: array<Table.enumConfig<Table.enum>>,
90
93
  }
91
94
 
92
- module DynamicContractRegistry = {
93
- let name = "dynamic_contract_registry"
95
+ module EnvioAddresses = {
96
+ let name = "envio_addresses"
94
97
  let index = -1
95
98
 
96
- let makeId = (~chainId, ~contractAddress) => {
97
- chainId->Belt.Int.toString ++ "-" ++ contractAddress->Address.toString
99
+ let makeId = (~chainId, ~address) => {
100
+ chainId->Belt.Int.toString ++ "-" ++ address->Address.toString
98
101
  }
99
102
 
100
- @genType
101
103
  type t = {
102
104
  id: string,
103
105
  @as("chain_id") chainId: int,
104
- @as("registering_event_block_number") registeringEventBlockNumber: int,
105
- @as("registering_event_log_index") registeringEventLogIndex: int,
106
- @as("registering_event_block_timestamp") registeringEventBlockTimestamp: int,
107
- @as("registering_event_contract_name") registeringEventContractName: string,
108
- @as("registering_event_name") registeringEventName: string,
109
- @as("registering_event_src_address") registeringEventSrcAddress: Address.t,
110
- @as("contract_address") contractAddress: Address.t,
106
+ @as("registration_block") registrationBlock: int,
107
+ // -1 when the address was registered from a block handler (no log index)
108
+ @as("registration_log_index") registrationLogIndex: int,
111
109
  @as("contract_name") contractName: string,
112
110
  }
113
111
 
112
+ // Extract the raw contract address from the composite id ({chainId}-{address}).
113
+ // Inverse of makeId. Keep in sync with makeId above and the SUBSTRING SQL in
114
+ // InternalTable.Chains.makeGetInitialStateQuery.
115
+ let getAddress = (entity: t): Address.t => {
116
+ let sepIdx = entity.id->String.indexOf("-")
117
+ entity.id
118
+ ->String.slice(~start=sepIdx + 1, ~end=entity.id->String.length)
119
+ ->Address.unsafeFromString
120
+ }
121
+
114
122
  let schema = S.schema(s => {
115
123
  id: s.matches(S.string),
116
124
  chainId: s.matches(S.int),
117
- registeringEventBlockNumber: s.matches(S.int),
118
- registeringEventLogIndex: s.matches(S.int),
119
- registeringEventContractName: s.matches(S.string),
120
- registeringEventName: s.matches(S.string),
121
- registeringEventSrcAddress: s.matches(Address.schema),
122
- registeringEventBlockTimestamp: s.matches(S.int),
123
- contractAddress: s.matches(Address.schema),
125
+ registrationBlock: s.matches(S.int),
126
+ registrationLogIndex: s.matches(S.int),
124
127
  contractName: s.matches(S.string),
125
128
  })
126
129
 
@@ -131,13 +134,9 @@ module DynamicContractRegistry = {
131
134
  ~fields=[
132
135
  Table.mkField("id", String, ~isPrimaryKey=true, ~fieldSchema=S.string),
133
136
  Table.mkField("chain_id", Int32, ~fieldSchema=S.int),
134
- Table.mkField("registering_event_block_number", Int32, ~fieldSchema=S.int),
135
- Table.mkField("registering_event_log_index", Int32, ~fieldSchema=S.int),
136
- Table.mkField("registering_event_block_timestamp", Int32, ~fieldSchema=S.int),
137
- Table.mkField("registering_event_contract_name", String, ~fieldSchema=S.string),
138
- Table.mkField("registering_event_name", String, ~fieldSchema=S.string),
139
- Table.mkField("registering_event_src_address", String, ~fieldSchema=Address.schema),
140
- Table.mkField("contract_address", String, ~fieldSchema=Address.schema),
137
+ Table.mkField("registration_block", Int32, ~fieldSchema=S.int),
138
+ // -1 sentinel when registered from a block handler (no log index)
139
+ Table.mkField("registration_log_index", Int32, ~fieldSchema=S.int),
141
140
  Table.mkField("contract_name", String, ~fieldSchema=S.string),
142
141
  ],
143
142
  )
@@ -153,7 +152,6 @@ module DynamicContractRegistry = {
153
152
  }->Internal.fromGenericEntityConfig
154
153
  }
155
154
 
156
- // Types for parsing source config from internal.config.json
157
155
  type rpcSourceFor = | @as("sync") Sync | @as("fallback") Fallback | @as("live") Live
158
156
 
159
157
  let rpcSourceForSchema = S.enum([Sync, Fallback, Live])
@@ -281,18 +279,18 @@ let entityJsonSchema = S.schema(s =>
281
279
 
282
280
  let getFieldTypeAndSchema = (prop, ~enumConfigsByName: dict<Table.enumConfig<Table.enum>>) => {
283
281
  let typ = prop["type"]
284
- let isNullable = prop["isNullable"]->Option.getWithDefault(false)
285
- let isArray = prop["isArray"]->Option.getWithDefault(false)
286
- let isIndex = prop["isIndex"]->Option.getWithDefault(false)
282
+ let isNullable = prop["isNullable"]->Option.getOr(false)
283
+ let isArray = prop["isArray"]->Option.getOr(false)
284
+ let isIndex = prop["isIndex"]->Option.getOr(false)
287
285
 
288
286
  let (fieldType, baseSchema) = switch typ {
289
287
  | "string" => (Table.String, S.string->S.toUnknown)
290
288
  | "boolean" => (Table.Boolean, S.bool->S.toUnknown)
291
289
  | "int" => (Table.Int32, S.int->S.toUnknown)
292
- | "bigint" => (Table.BigInt({precision: ?prop["precision"]}), BigInt.schema->S.toUnknown)
290
+ | "bigint" => (Table.BigInt({precision: ?prop["precision"]}), Utils.BigInt.schema->S.toUnknown)
293
291
  | "bigdecimal" => (
294
292
  Table.BigDecimal({
295
- config: ?prop["precision"]->Option.map(p => (p, prop["scale"]->Option.getWithDefault(0))),
293
+ config: ?(prop["precision"]->Option.map(p => (p, prop["scale"]->Option.getOr(0)))),
296
294
  }),
297
295
  BigDecimal.schema->S.toUnknown,
298
296
  )
@@ -301,18 +299,27 @@ let getFieldTypeAndSchema = (prop, ~enumConfigsByName: dict<Table.enumConfig<Tab
301
299
  | "json" => (Table.Json, S.json(~validate=false)->S.toUnknown)
302
300
  | "date" => (Table.Date, Utils.Schema.dbDate->S.toUnknown)
303
301
  | "enum" => {
304
- let enumName = prop["enum"]->Option.getExn
305
- let enumConfig =
306
- enumConfigsByName
307
- ->Js.Dict.get(enumName)
308
- ->Option.getExn
302
+ let enumName = prop["enum"]->Option.getOrThrow
303
+
304
+ // Build contracts for this chain from per-chain contract data + contract configs
305
+
306
+ // Get per-chain contract data (addresses, startBlock)
307
+
308
+ // Build event configs from JSON (field selections resolved inline)
309
+
310
+ // Build sourceConfig from the parsed chain config
311
+
312
+ // Build syncConfig from flattened fields
313
+
314
+ // Fuel doesn't have reorgs, SVM reorg handling is not supported
315
+ let enumConfig = enumConfigsByName->Dict.get(enumName)->Option.getOrThrow
309
316
  (Table.Enum({config: enumConfig}), enumConfig.schema->S.toUnknown)
310
317
  }
311
318
  | "entity" => {
312
- let entityName = prop["entity"]->Option.getExn
319
+ let entityName = prop["entity"]->Option.getOrThrow
313
320
  (Table.Entity({name: entityName}), S.string->S.toUnknown)
314
321
  }
315
- | other => Js.Exn.raiseError("Unknown field type in entity config: " ++ other)
322
+ | other => JsError.throwWithMessage("Unknown field type in entity config: " ++ other)
316
323
  }
317
324
 
318
325
  let fieldSchema = if isArray {
@@ -331,7 +338,7 @@ let getFieldTypeAndSchema = (prop, ~enumConfigsByName: dict<Table.enumConfig<Tab
331
338
 
332
339
  let parseEnumsFromJson = (enumsJson: dict<array<string>>): array<Table.enumConfig<Table.enum>> => {
333
340
  enumsJson
334
- ->Js.Dict.entries
341
+ ->Dict.toArray
335
342
  ->Array.map(((name, variants)) =>
336
343
  Table.makeEnumConfig(~name, ~variants)->Table.fromGenericEnumConfig
337
344
  )
@@ -341,7 +348,7 @@ let parseEntitiesFromJson = (
341
348
  entitiesJson: array<'entityJson>,
342
349
  ~enumConfigsByName: dict<Table.enumConfig<Table.enum>>,
343
350
  ): array<Internal.entityConfig> => {
344
- entitiesJson->Array.mapWithIndex((index, entityJson) => {
351
+ entitiesJson->Array.mapWithIndex((entityJson, index) => {
345
352
  let entityName = entityJson["name"]
346
353
 
347
354
  let fields: array<Table.fieldOrDerived> = entityJson["properties"]->Array.map(prop => {
@@ -363,7 +370,7 @@ let parseEntitiesFromJson = (
363
370
 
364
371
  let derivedFields: array<Table.fieldOrDerived> =
365
372
  entityJson["derivedFields"]
366
- ->Option.getWithDefault([])
373
+ ->Option.getOr([])
367
374
  ->Array.map(df =>
368
375
  Table.mkDerivedFromField(
369
376
  df["fieldName"],
@@ -374,7 +381,7 @@ let parseEntitiesFromJson = (
374
381
 
375
382
  let compositeIndices =
376
383
  entityJson["compositeIndices"]
377
- ->Option.getWithDefault([])
384
+ ->Option.getOr([])
378
385
  ->Array.map(ci =>
379
386
  ci->Array.map(
380
387
  f => {
@@ -394,7 +401,7 @@ let parseEntitiesFromJson = (
394
401
  // Use db field names (with _id suffix for linked entities) as schema locations
395
402
  // to match the database column names used in Table.toSqlParams
396
403
  let schema = S.schema(s => {
397
- let dict = Js.Dict.empty()
404
+ let dict = Dict.make()
398
405
  entityJson["properties"]->Array.forEach(
399
406
  prop => {
400
407
  let (_, fieldSchema, _, _, _) = getFieldTypeAndSchema(prop, ~enumConfigsByName)
@@ -402,7 +409,7 @@ let parseEntitiesFromJson = (
402
409
  | Some(_) => prop["name"] ++ "_id"
403
410
  | None => prop["name"]
404
411
  }
405
- dict->Js.Dict.set(dbFieldName, s.matches(fieldSchema))
412
+ dict->Dict.set(dbFieldName, s.matches(fieldSchema))
406
413
  },
407
414
  )
408
415
  dict
@@ -420,6 +427,13 @@ let parseEntitiesFromJson = (
420
427
  })
421
428
  }
422
429
 
430
+ let publicConfigStorageSchema = S.schema(s =>
431
+ {
432
+ "postgres": s.matches(S.bool),
433
+ "clickhouse": s.matches(S.option(S.bool)),
434
+ }
435
+ )
436
+
423
437
  let publicConfigSchema = S.schema(s =>
424
438
  {
425
439
  "name": s.matches(S.string),
@@ -430,6 +444,7 @@ let publicConfigSchema = S.schema(s =>
430
444
  "rollbackOnReorg": s.matches(S.option(S.bool)),
431
445
  "saveFullHistory": s.matches(S.option(S.bool)),
432
446
  "rawEvents": s.matches(S.option(S.bool)),
447
+ "storage": s.matches(publicConfigStorageSchema),
433
448
  "evm": s.matches(S.option(publicConfigEvmSchema)),
434
449
  "fuel": s.matches(S.option(publicConfigEcosystemSchema)),
435
450
  "svm": s.matches(S.option(publicConfigEcosystemSchema)),
@@ -438,12 +453,13 @@ let publicConfigSchema = S.schema(s =>
438
453
  }
439
454
  )
440
455
 
441
- let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
456
+ let fromPublic = (publicConfigJson: JSON.t) => {
457
+ let maxAddrInPartition = Env.maxAddrInPartition
442
458
  // Parse public config
443
459
  let publicConfig = try publicConfigJson->S.parseOrThrow(publicConfigSchema) catch {
444
460
  | S.Raised(exn) =>
445
- Js.Exn.raiseError(
446
- `Invalid internal.config.ts: ${exn->Utils.prettifyExn->(Utils.magic: exn => string)}`,
461
+ JsError.throwWithMessage(
462
+ `Invalid indexer config: ${exn->Utils.prettifyExn->(Utils.magic: exn => string)}`,
447
463
  )
448
464
  }
449
465
 
@@ -457,16 +473,22 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
457
473
  | (None, Some(ecosystemConfig), None) => (ecosystemConfig["chains"], Ecosystem.Fuel)
458
474
  | (None, None, Some(ecosystemConfig)) => (ecosystemConfig["chains"], Ecosystem.Svm)
459
475
  | (None, None, None) =>
460
- Js.Exn.raiseError("Invalid indexer config: No ecosystem configured (evm, fuel, or svm)")
476
+ JsError.throwWithMessage("Invalid indexer config: No ecosystem configured (evm, fuel, or svm)")
461
477
  | _ =>
462
- Js.Exn.raiseError(
478
+ JsError.throwWithMessage(
463
479
  "Invalid indexer config: Multiple ecosystems are not supported for a single indexer",
464
480
  )
465
481
  }
466
482
 
483
+ let ecosystem = switch ecosystemName {
484
+ | Ecosystem.Evm => Evm.ecosystem
485
+ | Ecosystem.Fuel => Fuel.ecosystem
486
+ | Ecosystem.Svm => Svm.ecosystem
487
+ }
488
+
467
489
  // Extract EVM-specific options with defaults
468
490
  let lowercaseAddresses = switch publicConfig["evm"] {
469
- | Some(evm) => evm["addressFormat"]->Option.getWithDefault(Checksum) == Lowercase
491
+ | Some(evm) => evm["addressFormat"]->Option.getOr(Checksum) == Lowercase
470
492
  | None => false
471
493
  }
472
494
 
@@ -483,32 +505,32 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
483
505
  Utils.Set.fromArray(
484
506
  Array.concat(
485
507
  EventConfigBuilder.alwaysIncludedBlockFields,
486
- evm["globalBlockFields"]->Option.getWithDefault([]),
508
+ evm["globalBlockFields"]->Option.getOr([]),
487
509
  ),
488
510
  ),
489
- Utils.Set.fromArray(evm["globalTransactionFields"]->Option.getWithDefault([])),
511
+ Utils.Set.fromArray(evm["globalTransactionFields"]->Option.getOr([])),
490
512
  )
491
513
  | None => (Utils.Set.fromArray(EventConfigBuilder.alwaysIncludedBlockFields), Utils.Set.make())
492
514
  }
493
515
 
494
516
  // Build contract data lookup: ABI, event signatures, event configs (keyed by capitalized name)
495
- let contractDataByName: Js.Dict.t<{
517
+ let contractDataByName: dict<{
496
518
  "abi": EvmTypes.Abi.t,
497
519
  "eventSignatures": array<string>,
498
520
  "events": option<array<_>>,
499
- }> = Js.Dict.empty()
521
+ }> = Dict.make()
500
522
  switch publicContractsConfig {
501
523
  | Some(contractsDict) =>
502
524
  contractsDict
503
- ->Js.Dict.entries
525
+ ->Dict.toArray
504
526
  ->Array.forEach(((contractName, contractConfig)) => {
505
527
  let capitalizedName = contractName->Utils.String.capitalize
506
- let abi = contractConfig["abi"]->(Utils.magic: Js.Json.t => EvmTypes.Abi.t)
528
+ let abi = contractConfig["abi"]->(Utils.magic: JSON.t => EvmTypes.Abi.t)
507
529
  let eventSignatures = switch contractConfig["events"] {
508
530
  | Some(events) => events->Array.map(eventItem => eventItem["event"])
509
531
  | None => []
510
532
  }
511
- contractDataByName->Js.Dict.set(
533
+ contractDataByName->Dict.set(
512
534
  capitalizedName,
513
535
  {"abi": abi, "eventSignatures": eventSignatures, "events": contractConfig["events"]},
514
536
  )
@@ -517,19 +539,21 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
517
539
  }
518
540
 
519
541
  // Build event configs for a contract from JSON event items
520
- let buildContractEvents = (~contractName, ~events: option<array<_>>, ~abi) => {
542
+ let buildContractEvents = (
543
+ ~contractName,
544
+ ~events: option<array<_>>,
545
+ ~abi,
546
+ ~chainId: int,
547
+ ~startBlock: option<int>,
548
+ ) => {
521
549
  switch events {
522
550
  | None => []
523
551
  | Some(eventItems) =>
524
552
  eventItems->Array.map(eventItem => {
525
553
  let eventName = eventItem["name"]
526
554
  let sighash = eventItem["sighash"]
527
- let params = eventItem["params"]->Option.getWithDefault([])
555
+ let params = eventItem["params"]->Option.getOr([])
528
556
  let kind = eventItem["kind"]
529
- // Get handler registration data
530
- let isWildcard = HandlerRegister.isWildcard(~contractName, ~eventName)
531
- let handler = HandlerRegister.getHandler(~contractName, ~eventName)
532
- let contractRegister = HandlerRegister.getContractRegister(~contractName, ~eventName)
533
557
 
534
558
  switch ecosystemName {
535
559
  | Ecosystem.Fuel =>
@@ -540,13 +564,14 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
540
564
  ~eventName,
541
565
  ~kind=fuelKind,
542
566
  ~sighash,
543
- ~rawAbi=abi->(Utils.magic: EvmTypes.Abi.t => Js.Json.t),
544
- ~isWildcard,
545
- ~handler,
546
- ~contractRegister,
567
+ ~rawAbi=abi->(Utils.magic: EvmTypes.Abi.t => JSON.t),
568
+ ~isWildcard=false,
569
+ ~handler=None,
570
+ ~contractRegister=None,
571
+ ~startBlock?,
547
572
  ) :> Internal.eventConfig)
548
573
  | None =>
549
- Js.Exn.raiseError(
574
+ JsError.throwWithMessage(
550
575
  `Fuel event ${contractName}.${eventName} is missing "kind" in internal config`,
551
576
  )
552
577
  }
@@ -556,12 +581,15 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
556
581
  ~eventName,
557
582
  ~sighash,
558
583
  ~params,
559
- ~isWildcard,
560
- ~handler,
561
- ~contractRegister,
562
- ~eventFilters=HandlerRegister.getEventFilters(~contractName, ~eventName),
584
+ ~isWildcard=false,
585
+ ~handler=None,
586
+ ~contractRegister=None,
587
+ ~eventFilters=None,
588
+ ~probeChainId=chainId,
589
+ ~onEventBlockFilterSchema=ecosystem.onEventBlockFilterSchema,
563
590
  ~blockFields=?eventItem["blockFields"],
564
591
  ~transactionFields=?eventItem["transactionFields"],
592
+ ~startBlock?,
565
593
  ~globalBlockFieldsSet,
566
594
  ~globalTransactionFieldsSet,
567
595
  ) :> Internal.eventConfig)
@@ -595,31 +623,35 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
595
623
  // Build chains from JSON config (no more codegenChains)
596
624
  let chains =
597
625
  publicChainsConfig
598
- ->Js.Dict.keys
599
- ->Js.Array2.map(chainName => {
600
- let publicChainConfig = publicChainsConfig->Js.Dict.unsafeGet(chainName)
626
+ ->Dict.keysToArray
627
+ ->Array.map(chainName => {
628
+ let publicChainConfig = publicChainsConfig->Dict.getUnsafe(chainName)
601
629
  let chainId = publicChainConfig["id"]
602
630
 
603
- // Build contracts for this chain from per-chain contract data + contract configs
604
- let chainContracts = publicChainConfig["contracts"]->Option.getWithDefault(Js.Dict.empty())
631
+ let chainContracts = publicChainConfig["contracts"]->Option.getOr(Dict.make())
605
632
  let contracts =
606
633
  contractDataByName
607
- ->Js.Dict.entries
634
+ ->Dict.toArray
608
635
  ->Array.map(((capitalizedName, contractData)) => {
609
- // Get per-chain contract data (addresses, startBlock)
610
- let chainContract = chainContracts->Js.Dict.get(capitalizedName)
636
+ let chainContract = chainContracts->Dict.get(capitalizedName)
611
637
  let addresses =
612
638
  chainContract
613
639
  ->Option.flatMap(cc => cc["addresses"])
614
- ->Option.getWithDefault([])
640
+ ->Option.getOr([])
615
641
  ->Array.map(parseAddress)
616
642
  let startBlock = chainContract->Option.flatMap(cc => cc["startBlock"])
617
643
 
618
644
  // Build event configs from JSON (field selections resolved inline)
645
+ // chainId is threaded in so the where-callback detection probe
646
+ // exercises the callback with this chain's real id — handlers
647
+ // that branch on `chain.id` are taken through the same path
648
+ // they will follow at runtime.
619
649
  let events = buildContractEvents(
620
650
  ~contractName=capitalizedName,
621
651
  ~events=contractData["events"],
622
652
  ~abi=contractData["abi"],
653
+ ~chainId,
654
+ ~startBlock,
623
655
  )
624
656
 
625
657
  {
@@ -632,14 +664,12 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
632
664
  }
633
665
  })
634
666
 
635
- // Build sourceConfig from the parsed chain config
636
667
  let sourceConfig = switch ecosystemName {
637
668
  | Ecosystem.Evm =>
638
669
  let rpcs =
639
670
  publicChainConfig["rpcs"]
640
- ->Option.getWithDefault([])
671
+ ->Option.getOr([])
641
672
  ->Array.map((rpcConfig): evmRpcConfig => {
642
- // Build syncConfig from flattened fields
643
673
  let initialBlockInterval = rpcConfig["initialBlockInterval"]
644
674
  let backoffMultiplicative = rpcConfig["backoffMultiplicative"]
645
675
  let accelerationAdditive = rpcConfig["accelerationAdditive"]
@@ -682,12 +712,13 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
682
712
  | Ecosystem.Fuel =>
683
713
  switch publicChainConfig["hypersync"] {
684
714
  | Some(hypersync) => FuelSourceConfig({hypersync: hypersync})
685
- | None => Js.Exn.raiseError(`Chain ${chainName} is missing hypersync endpoint in config`)
715
+ | None =>
716
+ JsError.throwWithMessage(`Chain ${chainName} is missing hypersync endpoint in config`)
686
717
  }
687
718
  | Ecosystem.Svm =>
688
719
  switch publicChainConfig["rpc"] {
689
720
  | Some(rpc) => SvmSourceConfig({rpc: rpc})
690
- | None => Js.Exn.raiseError(`Chain ${chainName} is missing rpc endpoint in config`)
721
+ | None => JsError.throwWithMessage(`Chain ${chainName} is missing rpc endpoint in config`)
691
722
  }
692
723
  }
693
724
 
@@ -697,11 +728,11 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
697
728
  startBlock: publicChainConfig["startBlock"],
698
729
  endBlock: ?publicChainConfig["endBlock"],
699
730
  maxReorgDepth: switch ecosystemName {
700
- | Ecosystem.Evm => publicChainConfig["maxReorgDepth"]->Option.getWithDefault(200)
701
- // Fuel doesn't have reorgs, SVM reorg handling is not supported
731
+ | Ecosystem.Evm => publicChainConfig["maxReorgDepth"]->Option.getOr(200)
732
+
702
733
  | Ecosystem.Fuel | Ecosystem.Svm => 0
703
734
  },
704
- blockLag: publicChainConfig["blockLag"]->Option.getWithDefault(0),
735
+ blockLag: publicChainConfig["blockLag"]->Option.getOr(0),
705
736
  contracts,
706
737
  sourceConfig,
707
738
  }
@@ -709,57 +740,40 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
709
740
 
710
741
  let chainMap =
711
742
  chains
712
- ->Js.Array2.map(chain => {
743
+ ->Array.map(chain => {
713
744
  (ChainMap.Chain.makeUnsafe(~chainId=chain.id), chain)
714
745
  })
715
746
  ->ChainMap.fromArrayUnsafe
716
747
 
717
- // Build the contract name mapping for efficient lookup
718
- let addContractNameToContractNameMapping = Js.Dict.empty()
719
- chains->Array.forEach(chainConfig => {
720
- chainConfig.contracts->Array.forEach(contract => {
721
- let addKey = "add" ++ contract.name->Utils.String.capitalize
722
- addContractNameToContractNameMapping->Js.Dict.set(addKey, contract.name)
723
- })
724
- })
725
-
726
- let ecosystem = switch ecosystemName {
727
- | Ecosystem.Evm => Evm.ecosystem
728
- | Ecosystem.Fuel => Fuel.ecosystem
729
- | Ecosystem.Svm => Svm.ecosystem
730
- }
731
-
732
748
  // Parse enums and entities from JSON config
733
749
  let allEnums =
734
750
  publicConfig["enums"]
735
- ->Option.getWithDefault(Js.Dict.empty())
751
+ ->Option.getOr(Dict.make())
736
752
  ->parseEnumsFromJson
737
753
 
738
754
  let enumConfigsByName =
739
- allEnums
740
- ->Js.Array2.map(enumConfig => (enumConfig.name, enumConfig))
741
- ->Js.Dict.fromArray
755
+ allEnums->Array.map(enumConfig => (enumConfig.name, enumConfig))->Dict.fromArray
742
756
 
743
757
  let userEntities =
744
758
  publicConfig["entities"]
745
- ->Option.getWithDefault([])
759
+ ->Option.getOr([])
746
760
  ->parseEntitiesFromJson(~enumConfigsByName)
747
761
 
748
- let allEntities = userEntities->Js.Array2.concat([DynamicContractRegistry.entityConfig])
762
+ let allEntities = userEntities->Array.concat([EnvioAddresses.entityConfig])
749
763
 
750
764
  let userEntitiesByName =
751
765
  userEntities
752
- ->Js.Array2.map(entityConfig => {
766
+ ->Array.map(entityConfig => {
753
767
  (entityConfig.name, entityConfig)
754
768
  })
755
- ->Js.Dict.fromArray
769
+ ->Dict.fromArray
756
770
 
757
771
  // Extract contract handlers from the public config
758
772
  let contractHandlers = switch publicContractsConfig {
759
773
  | Some(contractsDict) =>
760
774
  contractsDict
761
- ->Js.Dict.entries
762
- ->Js.Array2.map(((contractName, contractConfig)) => {
775
+ ->Dict.toArray
776
+ ->Array.map(((contractName, contractConfig)) => {
763
777
  {
764
778
  name: contractName->Utils.String.capitalize,
765
779
  handler: contractConfig["handler"],
@@ -771,19 +785,22 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
771
785
  {
772
786
  name: publicConfig["name"],
773
787
  description: publicConfig["description"],
774
- handlers: publicConfig["handlers"]->Option.getWithDefault("src/handlers"),
788
+ handlers: publicConfig["handlers"]->Option.getOr("src/handlers"),
775
789
  contractHandlers,
776
- shouldRollbackOnReorg: publicConfig["rollbackOnReorg"]->Option.getWithDefault(true),
777
- shouldSaveFullHistory: publicConfig["saveFullHistory"]->Option.getWithDefault(false),
778
- multichain: publicConfig["multichain"]->Option.getWithDefault(Unordered),
790
+ shouldRollbackOnReorg: publicConfig["rollbackOnReorg"]->Option.getOr(true),
791
+ shouldSaveFullHistory: publicConfig["saveFullHistory"]->Option.getOr(false),
792
+ storage: {
793
+ postgres: publicConfig["storage"]["postgres"],
794
+ clickhouse: publicConfig["storage"]["clickhouse"]->Option.getOr(false),
795
+ },
796
+ multichain: publicConfig["multichain"]->Option.getOr(Unordered),
779
797
  chainMap,
780
798
  defaultChain: chains->Array.get(0),
781
- enableRawEvents: publicConfig["rawEvents"]->Option.getWithDefault(false),
799
+ enableRawEvents: publicConfig["rawEvents"]->Option.getOr(false),
782
800
  ecosystem,
783
801
  maxAddrInPartition,
784
- batchSize: publicConfig["fullBatchSize"]->Option.getWithDefault(5000),
802
+ batchSize: publicConfig["fullBatchSize"]->Option.getOr(5000),
785
803
  lowercaseAddresses,
786
- addContractNameToContractNameMapping,
787
804
  userEntitiesByName,
788
805
  userEntities,
789
806
  allEntities,
@@ -791,18 +808,32 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
791
808
  }
792
809
  }
793
810
 
794
- let getEventConfig = (config: t, ~contractName, ~eventName) => {
795
- config.chainMap
796
- ->ChainMap.values
797
- ->Js.Array2.reduce((acc, chain) => {
811
+ // Look up an event config by (contract, event) name. When `chainId` is given,
812
+ // returns that chain's per-chain event config (matters for where-callback
813
+ // probe detection, which runs with the chain's real id). Without `chainId`,
814
+ // falls back to the first chain that declares this event.
815
+ let getEventConfig = (config: t, ~contractName, ~eventName, ~chainId: option<int>=?) => {
816
+ let chains = switch chainId {
817
+ | Some(chainId) =>
818
+ let chain = ChainMap.Chain.makeUnsafe(~chainId)
819
+ switch config.chainMap->ChainMap.get(chain) {
820
+ | chainConfig => [chainConfig]
821
+ | exception _ =>
822
+ JsError.throwWithMessage(
823
+ `Chain ${chainId->Int.toString} is not configured. Add it to config.yaml or pass a configured chain.`,
824
+ )
825
+ }
826
+ | None => config.chainMap->ChainMap.values
827
+ }
828
+ chains->Array.reduce(None, (acc, chain) => {
798
829
  switch acc {
799
830
  | Some(_) => acc
800
831
  | None =>
801
832
  chain.contracts
802
- ->Js.Array2.find(c => c.name == contractName)
803
- ->Belt.Option.flatMap(contract => contract.events->Js.Array2.find(e => e.name == eventName))
833
+ ->Array.find(c => c.name == contractName)
834
+ ->Belt.Option.flatMap(contract => contract.events->Array.find(e => e.name == eventName))
804
835
  }
805
- }, None)
836
+ })
806
837
  }
807
838
 
808
839
  let shouldSaveHistory = (config, ~isInReorgThreshold) =>
@@ -815,7 +846,34 @@ let getChain = (config, ~chainId) => {
815
846
  let chain = ChainMap.Chain.makeUnsafe(~chainId)
816
847
  config.chainMap->ChainMap.has(chain)
817
848
  ? chain
818
- : Js.Exn.raiseError(
849
+ : JsError.throwWithMessage(
819
850
  "No chain with id " ++ chain->ChainMap.Chain.toString ++ " found in config.yaml",
820
851
  )
821
852
  }
853
+
854
+ // A CLI command payload already contains the resolved JSON; priming lets
855
+ // downstream callers skip the NAPI `getConfigJson` round-trip. Calling
856
+ // `prime` again invalidates the memo.
857
+ %%private(let primedJson: ref<option<JSON.t>> = ref(None))
858
+ %%private(let cached: ref<option<t>> = ref(None))
859
+ let prime = (json: JSON.t): unit => {
860
+ primedJson := Some(json)
861
+ cached := None
862
+ }
863
+
864
+ // The returned value is a pure function of the JSON — no handler
865
+ // registrations are applied here. Post-registration configs come from
866
+ // `HandlerLoader.applyRegistrations`. That purity is what lets this
867
+ // memoize without invalidation.
868
+ let loadWithoutRegistrations = () =>
869
+ switch cached.contents {
870
+ | Some(c) => c
871
+ | None => {
872
+ let c = switch primedJson.contents {
873
+ | Some(json) => json->fromPublic
874
+ | None => Core.getConfigJson()->JSON.parseOrThrow->fromPublic
875
+ }
876
+ cached := Some(c)
877
+ c
878
+ }
879
+ }