envio 3.0.0-alpha.2 → 3.0.0-alpha.20

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 (175) hide show
  1. package/README.md +164 -30
  2. package/bin.mjs +49 -0
  3. package/evm.schema.json +79 -169
  4. package/fuel.schema.json +50 -21
  5. package/index.d.ts +497 -1
  6. package/index.js +4 -0
  7. package/package.json +42 -31
  8. package/rescript.json +4 -1
  9. package/src/Batch.res +11 -8
  10. package/src/Batch.res.mjs +11 -9
  11. package/src/ChainFetcher.res +531 -0
  12. package/src/ChainFetcher.res.mjs +339 -0
  13. package/src/ChainManager.res +190 -0
  14. package/src/ChainManager.res.mjs +166 -0
  15. package/src/Change.res +3 -3
  16. package/src/Config.gen.ts +19 -0
  17. package/src/Config.res +737 -22
  18. package/src/Config.res.mjs +703 -26
  19. package/src/{Indexer.res → Ctx.res} +1 -1
  20. package/src/Ecosystem.res +9 -124
  21. package/src/Ecosystem.res.mjs +19 -160
  22. package/src/Env.res +30 -74
  23. package/src/Env.res.mjs +25 -87
  24. package/src/Envio.gen.ts +3 -1
  25. package/src/Envio.res +20 -9
  26. package/src/EventProcessing.res +469 -0
  27. package/src/EventProcessing.res.mjs +337 -0
  28. package/src/EvmTypes.gen.ts +6 -0
  29. package/src/EvmTypes.res +1 -0
  30. package/src/FetchState.res +1256 -639
  31. package/src/FetchState.res.mjs +1135 -612
  32. package/src/GlobalState.res +1190 -0
  33. package/src/GlobalState.res.mjs +1183 -0
  34. package/src/GlobalStateManager.res +68 -0
  35. package/src/GlobalStateManager.res.mjs +75 -0
  36. package/src/GlobalStateManager.resi +7 -0
  37. package/src/HandlerLoader.res +89 -0
  38. package/src/HandlerLoader.res.mjs +79 -0
  39. package/src/HandlerRegister.res +357 -0
  40. package/src/HandlerRegister.res.mjs +299 -0
  41. package/src/{EventRegister.resi → HandlerRegister.resi} +13 -13
  42. package/src/Hasura.res +111 -175
  43. package/src/Hasura.res.mjs +88 -150
  44. package/src/InMemoryStore.res +1 -1
  45. package/src/InMemoryStore.res.mjs +3 -3
  46. package/src/InMemoryTable.res +1 -1
  47. package/src/InMemoryTable.res.mjs +1 -1
  48. package/src/Internal.gen.ts +4 -0
  49. package/src/Internal.res +230 -12
  50. package/src/Internal.res.mjs +115 -1
  51. package/src/LoadLayer.res +444 -0
  52. package/src/LoadLayer.res.mjs +296 -0
  53. package/src/LoadLayer.resi +32 -0
  54. package/src/LogSelection.res +33 -27
  55. package/src/LogSelection.res.mjs +6 -0
  56. package/src/Logging.res +21 -7
  57. package/src/Logging.res.mjs +16 -8
  58. package/src/Main.res +377 -0
  59. package/src/Main.res.mjs +339 -0
  60. package/src/Persistence.res +7 -21
  61. package/src/Persistence.res.mjs +3 -3
  62. package/src/PgStorage.gen.ts +10 -0
  63. package/src/PgStorage.res +116 -69
  64. package/src/PgStorage.res.d.mts +5 -0
  65. package/src/PgStorage.res.mjs +93 -50
  66. package/src/Prometheus.res +294 -224
  67. package/src/Prometheus.res.mjs +353 -340
  68. package/src/ReorgDetection.res +6 -10
  69. package/src/ReorgDetection.res.mjs +6 -6
  70. package/src/SafeCheckpointTracking.res +4 -4
  71. package/src/SafeCheckpointTracking.res.mjs +2 -2
  72. package/src/Sink.res +4 -2
  73. package/src/Sink.res.mjs +2 -1
  74. package/src/TableIndices.res +0 -1
  75. package/src/TestIndexer.res +692 -0
  76. package/src/TestIndexer.res.mjs +527 -0
  77. package/src/TestIndexerProxyStorage.res +205 -0
  78. package/src/TestIndexerProxyStorage.res.mjs +151 -0
  79. package/src/TopicFilter.res +1 -1
  80. package/src/Types.ts +1 -1
  81. package/src/UserContext.res +424 -0
  82. package/src/UserContext.res.mjs +279 -0
  83. package/src/Utils.res +97 -26
  84. package/src/Utils.res.mjs +91 -44
  85. package/src/bindings/BigInt.res +10 -0
  86. package/src/bindings/BigInt.res.mjs +15 -0
  87. package/src/bindings/ClickHouse.res +120 -23
  88. package/src/bindings/ClickHouse.res.mjs +118 -28
  89. package/src/bindings/DateFns.res +74 -0
  90. package/src/bindings/DateFns.res.mjs +22 -0
  91. package/src/bindings/EventSource.res +8 -1
  92. package/src/bindings/EventSource.res.mjs +8 -1
  93. package/src/bindings/Express.res +1 -0
  94. package/src/bindings/Hrtime.res +14 -1
  95. package/src/bindings/Hrtime.res.mjs +22 -2
  96. package/src/bindings/Hrtime.resi +4 -0
  97. package/src/bindings/Lodash.res +0 -1
  98. package/src/bindings/NodeJs.res +49 -3
  99. package/src/bindings/NodeJs.res.mjs +11 -3
  100. package/src/bindings/Pino.res +24 -10
  101. package/src/bindings/Pino.res.mjs +14 -8
  102. package/src/bindings/Postgres.gen.ts +8 -0
  103. package/src/bindings/Postgres.res +5 -1
  104. package/src/bindings/Postgres.res.d.mts +5 -0
  105. package/src/bindings/PromClient.res +0 -10
  106. package/src/bindings/PromClient.res.mjs +0 -3
  107. package/src/bindings/Vitest.res +142 -0
  108. package/src/bindings/Vitest.res.mjs +9 -0
  109. package/src/bindings/WebSocket.res +27 -0
  110. package/src/bindings/WebSocket.res.mjs +2 -0
  111. package/src/bindings/Yargs.res +8 -0
  112. package/src/bindings/Yargs.res.mjs +2 -0
  113. package/src/db/EntityHistory.res +7 -7
  114. package/src/db/EntityHistory.res.mjs +9 -9
  115. package/src/db/InternalTable.res +59 -111
  116. package/src/db/InternalTable.res.mjs +73 -104
  117. package/src/db/Table.res +27 -8
  118. package/src/db/Table.res.mjs +25 -14
  119. package/src/sources/Evm.res +84 -0
  120. package/src/sources/Evm.res.mjs +105 -0
  121. package/src/sources/EvmChain.res +94 -0
  122. package/src/sources/EvmChain.res.mjs +60 -0
  123. package/src/sources/Fuel.res +19 -34
  124. package/src/sources/Fuel.res.mjs +34 -16
  125. package/src/sources/FuelSDK.res +38 -0
  126. package/src/sources/FuelSDK.res.mjs +29 -0
  127. package/src/sources/HyperFuel.res +2 -2
  128. package/src/sources/HyperFuel.resi +1 -1
  129. package/src/sources/HyperFuelClient.res +2 -2
  130. package/src/sources/HyperFuelSource.res +33 -13
  131. package/src/sources/HyperFuelSource.res.mjs +24 -16
  132. package/src/sources/HyperSync.res +36 -6
  133. package/src/sources/HyperSync.res.mjs +9 -7
  134. package/src/sources/HyperSync.resi +4 -0
  135. package/src/sources/HyperSyncClient.res +1 -1
  136. package/src/sources/HyperSyncHeightStream.res +47 -116
  137. package/src/sources/HyperSyncHeightStream.res.mjs +46 -73
  138. package/src/sources/HyperSyncSource.res +118 -139
  139. package/src/sources/HyperSyncSource.res.mjs +104 -121
  140. package/src/sources/Rpc.res +86 -14
  141. package/src/sources/Rpc.res.mjs +101 -9
  142. package/src/sources/RpcSource.res +621 -364
  143. package/src/sources/RpcSource.res.mjs +843 -410
  144. package/src/sources/RpcWebSocketHeightStream.res +181 -0
  145. package/src/sources/RpcWebSocketHeightStream.res.mjs +196 -0
  146. package/src/sources/Source.res +7 -5
  147. package/src/sources/SourceManager.res +325 -225
  148. package/src/sources/SourceManager.res.mjs +314 -171
  149. package/src/sources/SourceManager.resi +17 -6
  150. package/src/sources/Svm.res +81 -0
  151. package/src/sources/Svm.res.mjs +90 -0
  152. package/src/tui/Tui.res +247 -0
  153. package/src/tui/Tui.res.mjs +337 -0
  154. package/src/tui/bindings/Ink.res +371 -0
  155. package/src/tui/bindings/Ink.res.mjs +72 -0
  156. package/src/tui/bindings/Style.res +123 -0
  157. package/src/tui/bindings/Style.res.mjs +2 -0
  158. package/src/tui/components/BufferedProgressBar.res +40 -0
  159. package/src/tui/components/BufferedProgressBar.res.mjs +57 -0
  160. package/src/tui/components/CustomHooks.res +122 -0
  161. package/src/tui/components/CustomHooks.res.mjs +179 -0
  162. package/src/tui/components/Messages.res +41 -0
  163. package/src/tui/components/Messages.res.mjs +75 -0
  164. package/src/tui/components/SyncETA.res +174 -0
  165. package/src/tui/components/SyncETA.res.mjs +263 -0
  166. package/src/tui/components/TuiData.res +47 -0
  167. package/src/tui/components/TuiData.res.mjs +34 -0
  168. package/svm.schema.json +112 -0
  169. package/bin.js +0 -48
  170. package/src/EventRegister.res +0 -241
  171. package/src/EventRegister.res.mjs +0 -240
  172. package/src/bindings/Ethers.gen.ts +0 -14
  173. package/src/bindings/Ethers.res +0 -204
  174. package/src/bindings/Ethers.res.mjs +0 -130
  175. /package/src/{Indexer.res.mjs → Ctx.res.mjs} +0 -0
package/src/Config.res CHANGED
@@ -8,6 +8,7 @@ type sourceSyncOptions = {
8
8
  backoffMillis?: int,
9
9
  queryTimeoutMillis?: int,
10
10
  fallbackStallTimeout?: int,
11
+ pollingInterval?: int,
11
12
  }
12
13
 
13
14
  type contract = {
@@ -16,15 +17,47 @@ type contract = {
16
17
  addresses: array<Address.t>,
17
18
  events: array<Internal.eventConfig>,
18
19
  startBlock: option<int>,
20
+ // EVM-specific: event sighashes for HyperSync queries
21
+ eventSignatures: array<string>,
19
22
  }
20
23
 
24
+ type codegenContract = {
25
+ name: string,
26
+ addresses: array<string>,
27
+ events: array<Internal.eventConfig>,
28
+ startBlock: option<int>,
29
+ }
30
+
31
+ // Source config is now parsed from internal.config.json and sources are created lazily
32
+ type codegenChain = {
33
+ id: int,
34
+ contracts: array<codegenContract>,
35
+ }
36
+
37
+ // Source config parsed from internal.config.json - sources are created lazily in ChainFetcher
38
+ type evmRpcConfig = {
39
+ url: string,
40
+ sourceFor: Source.sourceFor,
41
+ syncConfig: option<sourceSyncOptions>,
42
+ ws: option<string>,
43
+ }
44
+
45
+ type sourceConfig =
46
+ | EvmSourceConfig({hypersync: option<string>, rpcs: array<evmRpcConfig>})
47
+ | FuelSourceConfig({hypersync: string})
48
+ | SvmSourceConfig({rpc: string})
49
+ // For tests: pass custom sources directly
50
+ | CustomSources(array<Source.t>)
51
+
21
52
  type chain = {
53
+ name: string,
22
54
  id: int,
23
55
  startBlock: int,
24
56
  endBlock?: int,
25
57
  maxReorgDepth: int,
58
+ blockLag: int,
26
59
  contracts: array<contract>,
27
- sources: array<Source.t>,
60
+ sourceConfig: sourceConfig,
28
61
  }
29
62
 
30
63
  type sourceSync = {
@@ -35,11 +68,21 @@ type sourceSync = {
35
68
  backoffMillis: int,
36
69
  queryTimeoutMillis: int,
37
70
  fallbackStallTimeout: int,
71
+ pollingInterval: int,
38
72
  }
39
73
 
40
74
  type multichain = | @as("ordered") Ordered | @as("unordered") Unordered
41
75
 
76
+ type contractHandler = {
77
+ name: string,
78
+ handler: option<string>,
79
+ }
80
+
42
81
  type t = {
82
+ name: string,
83
+ description: option<string>,
84
+ handlers: string,
85
+ contractHandlers: array<contractHandler>,
43
86
  shouldRollbackOnReorg: bool,
44
87
  shouldSaveFullHistory: bool,
45
88
  multichain: multichain,
@@ -51,31 +94,652 @@ type t = {
51
94
  batchSize: int,
52
95
  lowercaseAddresses: bool,
53
96
  addContractNameToContractNameMapping: dict<string>,
97
+ userEntitiesByName: dict<Internal.entityConfig>,
98
+ userEntities: array<Internal.entityConfig>,
99
+ allEntities: array<Internal.entityConfig>,
100
+ allEnums: array<Table.enumConfig<Table.enum>>,
54
101
  }
55
102
 
56
- let make = (
57
- ~shouldRollbackOnReorg=true,
58
- ~shouldSaveFullHistory=false,
59
- ~chains: array<chain>=[],
60
- ~enableRawEvents=false,
61
- ~ecosystem: Ecosystem.name=Ecosystem.Evm,
62
- ~batchSize=5000,
63
- ~lowercaseAddresses=false,
64
- ~multichain=Unordered,
65
- ~shouldUseHypersyncClientDecoder=true,
103
+ module DynamicContractRegistry = {
104
+ let name = "dynamic_contract_registry"
105
+ let index = -1
106
+
107
+ let makeId = (~chainId, ~contractAddress) => {
108
+ chainId->Belt.Int.toString ++ "-" ++ contractAddress->Address.toString
109
+ }
110
+
111
+ @genType
112
+ type t = {
113
+ id: string,
114
+ @as("chain_id") chainId: int,
115
+ @as("registering_event_block_number") registeringEventBlockNumber: int,
116
+ @as("registering_event_log_index") registeringEventLogIndex: int,
117
+ @as("registering_event_block_timestamp") registeringEventBlockTimestamp: int,
118
+ @as("registering_event_contract_name") registeringEventContractName: string,
119
+ @as("registering_event_name") registeringEventName: string,
120
+ @as("registering_event_src_address") registeringEventSrcAddress: Address.t,
121
+ @as("contract_address") contractAddress: Address.t,
122
+ @as("contract_name") contractName: string,
123
+ }
124
+
125
+ let schema = S.schema(s => {
126
+ id: s.matches(S.string),
127
+ chainId: s.matches(S.int),
128
+ registeringEventBlockNumber: s.matches(S.int),
129
+ registeringEventLogIndex: s.matches(S.int),
130
+ registeringEventContractName: s.matches(S.string),
131
+ registeringEventName: s.matches(S.string),
132
+ registeringEventSrcAddress: s.matches(Address.schema),
133
+ registeringEventBlockTimestamp: s.matches(S.int),
134
+ contractAddress: s.matches(Address.schema),
135
+ contractName: s.matches(S.string),
136
+ })
137
+
138
+ let rowsSchema = S.array(schema)
139
+
140
+ let table = Table.mkTable(
141
+ name,
142
+ ~fields=[
143
+ Table.mkField("id", String, ~isPrimaryKey=true, ~fieldSchema=S.string),
144
+ Table.mkField("chain_id", Int32, ~fieldSchema=S.int),
145
+ Table.mkField("registering_event_block_number", Int32, ~fieldSchema=S.int),
146
+ Table.mkField("registering_event_log_index", Int32, ~fieldSchema=S.int),
147
+ Table.mkField("registering_event_block_timestamp", Int32, ~fieldSchema=S.int),
148
+ Table.mkField("registering_event_contract_name", String, ~fieldSchema=S.string),
149
+ Table.mkField("registering_event_name", String, ~fieldSchema=S.string),
150
+ Table.mkField("registering_event_src_address", String, ~fieldSchema=Address.schema),
151
+ Table.mkField("contract_address", String, ~fieldSchema=Address.schema),
152
+ Table.mkField("contract_name", String, ~fieldSchema=S.string),
153
+ ],
154
+ )
155
+
156
+ external castToInternal: t => Internal.entity = "%identity"
157
+
158
+ let entityConfig = {
159
+ name,
160
+ index,
161
+ schema,
162
+ rowsSchema,
163
+ table,
164
+ }->Internal.fromGenericEntityConfig
165
+ }
166
+
167
+ // Types for parsing source config from internal.config.json
168
+ type rpcSourceFor = | @as("sync") Sync | @as("fallback") Fallback | @as("live") Live
169
+
170
+ let rpcSourceForSchema = S.enum([Sync, Fallback, Live])
171
+
172
+ let rpcConfigSchema = S.schema(s =>
173
+ {
174
+ "url": s.matches(S.string),
175
+ "for": s.matches(rpcSourceForSchema),
176
+ "ws": s.matches(S.option(S.string)),
177
+ "initialBlockInterval": s.matches(S.option(S.int)),
178
+ "backoffMultiplicative": s.matches(S.option(S.float)),
179
+ "accelerationAdditive": s.matches(S.option(S.int)),
180
+ "intervalCeiling": s.matches(S.option(S.int)),
181
+ "backoffMillis": s.matches(S.option(S.int)),
182
+ "fallbackStallTimeout": s.matches(S.option(S.int)),
183
+ "queryTimeoutMillis": s.matches(S.option(S.int)),
184
+ "pollingInterval": s.matches(S.option(S.int)),
185
+ }
186
+ )
187
+
188
+ let publicConfigChainSchema = S.schema(s =>
189
+ {
190
+ "id": s.matches(S.int),
191
+ "startBlock": s.matches(S.int),
192
+ "endBlock": s.matches(S.option(S.int)),
193
+ "maxReorgDepth": s.matches(S.option(S.int)),
194
+ "blockLag": s.matches(S.option(S.int)),
195
+ // EVM/Fuel source config (hypersync for EVM, hyperfuel for Fuel)
196
+ "hypersync": s.matches(S.option(S.string)),
197
+ "rpcs": s.matches(S.option(S.array(rpcConfigSchema))),
198
+ // SVM source config
199
+ "rpc": s.matches(S.option(S.string)),
200
+ }
201
+ )
202
+
203
+ let contractEventItemSchema = S.schema(s =>
204
+ {
205
+ "event": s.matches(S.string),
206
+ "name": s.matches(S.string),
207
+ "blockFields": s.matches(S.option(S.array(S.string))),
208
+ "transactionFields": s.matches(S.option(S.array(S.string))),
209
+ }
210
+ )
211
+
212
+ let contractConfigSchema = S.schema(s =>
213
+ {
214
+ "abi": s.matches(S.json(~validate=false)),
215
+ "handler": s.matches(S.option(S.string)),
216
+ // EVM-specific: event signatures for HyperSync queries
217
+ "events": s.matches(S.option(S.array(contractEventItemSchema))),
218
+ }
219
+ )
220
+
221
+ let publicConfigEcosystemSchema = S.schema(s =>
222
+ {
223
+ "chains": s.matches(S.dict(publicConfigChainSchema)),
224
+ "contracts": s.matches(S.option(S.dict(contractConfigSchema))),
225
+ }
226
+ )
227
+
228
+ type addressFormat = | @as("lowercase") Lowercase | @as("checksum") Checksum
229
+
230
+ let publicConfigEvmSchema = S.schema(s =>
231
+ {
232
+ "chains": s.matches(S.dict(publicConfigChainSchema)),
233
+ "contracts": s.matches(S.option(S.dict(contractConfigSchema))),
234
+ "addressFormat": s.matches(S.option(S.enum([Lowercase, Checksum]))),
235
+ "globalBlockFields": s.matches(S.option(S.array(Internal.evmBlockFieldSchema))),
236
+ "globalTransactionFields": s.matches(S.option(S.array(Internal.evmTransactionFieldSchema))),
237
+ }
238
+ )
239
+
240
+ let multichainSchema = S.enum([Ordered, Unordered])
241
+
242
+ let compositeIndexFieldSchema = S.schema(s =>
243
+ {
244
+ "fieldName": s.matches(S.string),
245
+ "direction": s.matches(S.string),
246
+ }
247
+ )
248
+
249
+ let derivedFieldSchema = S.schema(s =>
250
+ {
251
+ "fieldName": s.matches(S.string),
252
+ "derivedFromEntity": s.matches(S.string),
253
+ "derivedFromField": s.matches(S.string),
254
+ }
255
+ )
256
+
257
+ let propertySchema = S.schema(s =>
258
+ {
259
+ "name": s.matches(S.string),
260
+ "type": s.matches(S.string),
261
+ "isNullable": s.matches(S.option(S.bool)),
262
+ "isArray": s.matches(S.option(S.bool)),
263
+ "isIndex": s.matches(S.option(S.bool)),
264
+ "linkedEntity": s.matches(S.option(S.string)),
265
+ "enum": s.matches(S.option(S.string)),
266
+ "entity": s.matches(S.option(S.string)),
267
+ "precision": s.matches(S.option(S.int)),
268
+ "scale": s.matches(S.option(S.int)),
269
+ }
270
+ )
271
+
272
+ let entityJsonSchema = S.schema(s =>
273
+ {
274
+ "name": s.matches(S.string),
275
+ "properties": s.matches(S.array(propertySchema)),
276
+ "derivedFields": s.matches(S.option(S.array(derivedFieldSchema))),
277
+ "compositeIndices": s.matches(S.option(S.array(S.array(compositeIndexFieldSchema)))),
278
+ }
279
+ )
280
+
281
+ let getFieldTypeAndSchema = (prop, ~enumConfigsByName: dict<Table.enumConfig<Table.enum>>) => {
282
+ let typ = prop["type"]
283
+ let isNullable = prop["isNullable"]->Option.getWithDefault(false)
284
+ let isArray = prop["isArray"]->Option.getWithDefault(false)
285
+ let isIndex = prop["isIndex"]->Option.getWithDefault(false)
286
+
287
+ let (fieldType, baseSchema) = switch typ {
288
+ | "string" => (Table.String, S.string->S.toUnknown)
289
+ | "boolean" => (Table.Boolean, S.bool->S.toUnknown)
290
+ | "int" => (Table.Int32, S.int->S.toUnknown)
291
+ | "bigint" => (Table.BigInt({precision: ?prop["precision"]}), BigInt.schema->S.toUnknown)
292
+ | "bigdecimal" => (
293
+ Table.BigDecimal({
294
+ config: ?prop["precision"]->Option.map(p => (p, prop["scale"]->Option.getWithDefault(0))),
295
+ }),
296
+ BigDecimal.schema->S.toUnknown,
297
+ )
298
+ | "float" => (Table.Number, S.float->S.toUnknown)
299
+ | "serial" => (Table.Serial, S.int->S.toUnknown)
300
+ | "json" => (Table.Json, S.json(~validate=false)->S.toUnknown)
301
+ | "date" => (Table.Date, Utils.Schema.dbDate->S.toUnknown)
302
+ | "enum" => {
303
+ let enumName = prop["enum"]->Option.getExn
304
+ let enumConfig =
305
+ enumConfigsByName
306
+ ->Js.Dict.get(enumName)
307
+ ->Option.getExn
308
+ (Table.Enum({config: enumConfig}), enumConfig.schema->S.toUnknown)
309
+ }
310
+ | "entity" => {
311
+ let entityName = prop["entity"]->Option.getExn
312
+ (Table.Entity({name: entityName}), S.string->S.toUnknown)
313
+ }
314
+ | other => Js.Exn.raiseError("Unknown field type in entity config: " ++ other)
315
+ }
316
+
317
+ let fieldSchema = if isArray {
318
+ S.array(baseSchema)->S.toUnknown
319
+ } else {
320
+ baseSchema
321
+ }
322
+ let fieldSchema = if isNullable {
323
+ S.null(fieldSchema)->S.toUnknown
324
+ } else {
325
+ fieldSchema
326
+ }
327
+
328
+ (fieldType, fieldSchema, isNullable, isArray, isIndex)
329
+ }
330
+
331
+ let parseEnumsFromJson = (enumsJson: dict<array<string>>): array<Table.enumConfig<Table.enum>> => {
332
+ enumsJson
333
+ ->Js.Dict.entries
334
+ ->Array.map(((name, variants)) =>
335
+ Table.makeEnumConfig(~name, ~variants)->Table.fromGenericEnumConfig
336
+ )
337
+ }
338
+
339
+ let parseEntitiesFromJson = (
340
+ entitiesJson: array<'entityJson>,
341
+ ~enumConfigsByName: dict<Table.enumConfig<Table.enum>>,
342
+ ): array<Internal.entityConfig> => {
343
+ entitiesJson->Array.mapWithIndex((index, entityJson) => {
344
+ let entityName = entityJson["name"]
345
+
346
+ let fields: array<Table.fieldOrDerived> = entityJson["properties"]->Array.map(prop => {
347
+ let (fieldType, fieldSchema, isNullable, isArray, isIndex) = getFieldTypeAndSchema(
348
+ prop,
349
+ ~enumConfigsByName,
350
+ )
351
+ Table.mkField(
352
+ prop["name"],
353
+ fieldType,
354
+ ~fieldSchema,
355
+ ~isPrimaryKey=prop["name"] === "id",
356
+ ~isNullable,
357
+ ~isArray,
358
+ ~isIndex,
359
+ ~linkedEntity=?prop["linkedEntity"],
360
+ )
361
+ })
362
+
363
+ let derivedFields: array<Table.fieldOrDerived> =
364
+ entityJson["derivedFields"]
365
+ ->Option.getWithDefault([])
366
+ ->Array.map(df =>
367
+ Table.mkDerivedFromField(
368
+ df["fieldName"],
369
+ ~derivedFromEntity=df["derivedFromEntity"],
370
+ ~derivedFromField=df["derivedFromField"],
371
+ )
372
+ )
373
+
374
+ let compositeIndices =
375
+ entityJson["compositeIndices"]
376
+ ->Option.getWithDefault([])
377
+ ->Array.map(ci =>
378
+ ci->Array.map(
379
+ f => {
380
+ Table.fieldName: f["fieldName"],
381
+ direction: f["direction"] == "Asc" ? Table.Asc : Table.Desc,
382
+ },
383
+ )
384
+ )
385
+
386
+ let table = Table.mkTable(
387
+ entityName,
388
+ ~fields=Array.concat(fields, derivedFields),
389
+ ~compositeIndices,
390
+ )
391
+
392
+ // Build schema dynamically from properties
393
+ // Use db field names (with _id suffix for linked entities) as schema locations
394
+ // to match the database column names used in Table.toSqlParams
395
+ let schema = S.schema(s => {
396
+ let dict = Js.Dict.empty()
397
+ entityJson["properties"]->Array.forEach(
398
+ prop => {
399
+ let (_, fieldSchema, _, _, _) = getFieldTypeAndSchema(prop, ~enumConfigsByName)
400
+ let dbFieldName = switch prop["linkedEntity"] {
401
+ | Some(_) => prop["name"] ++ "_id"
402
+ | None => prop["name"]
403
+ }
404
+ dict->Js.Dict.set(dbFieldName, s.matches(fieldSchema))
405
+ },
406
+ )
407
+ dict
408
+ })
409
+
410
+ {
411
+ Internal.name: entityName,
412
+ index,
413
+ schema: schema->(Utils.magic: S.t<dict<unknown>> => S.t<Internal.entity>),
414
+ rowsSchema: S.array(schema)->(
415
+ Utils.magic: S.t<array<dict<unknown>>> => S.t<array<Internal.entity>>
416
+ ),
417
+ table,
418
+ }->Internal.fromGenericEntityConfig
419
+ })
420
+ }
421
+
422
+ let publicConfigSchema = S.schema(s =>
423
+ {
424
+ "name": s.matches(S.string),
425
+ "description": s.matches(S.option(S.string)),
426
+ "handlers": s.matches(S.option(S.string)),
427
+ "multichain": s.matches(S.option(multichainSchema)),
428
+ "fullBatchSize": s.matches(S.option(S.int)),
429
+ "rollbackOnReorg": s.matches(S.option(S.bool)),
430
+ "saveFullHistory": s.matches(S.option(S.bool)),
431
+ "rawEvents": s.matches(S.option(S.bool)),
432
+ "evm": s.matches(S.option(publicConfigEvmSchema)),
433
+ "fuel": s.matches(S.option(publicConfigEcosystemSchema)),
434
+ "svm": s.matches(S.option(publicConfigEcosystemSchema)),
435
+ "enums": s.matches(S.option(S.dict(S.array(S.string)))),
436
+ "entities": s.matches(S.option(S.array(entityJsonSchema))),
437
+ }
438
+ )
439
+
440
+ // Always-included block fields (number, timestamp, hash) are not in the JSON;
441
+ // they're prepended at runtime so they're always present.
442
+ let alwaysIncludedBlockFields: array<Internal.evmBlockField> = [Number, Timestamp, Hash]
443
+
444
+ // Enrich EVM event configs with field selections from the JSON config.
445
+ // Mutates the event configs in-place to set selectedBlockFields/selectedTransactionFields.
446
+ let enrichEvmFieldSelections = (
447
+ events: array<Internal.eventConfig>,
448
+ ~jsonEvents: option<array<_>>,
449
+ ~globalBlockFieldsSet: Utils.Set.t<Internal.evmBlockField>,
450
+ ~globalTransactionFieldsSet: Utils.Set.t<Internal.evmTransactionField>,
451
+ ) => {
452
+ // Build a lookup by event name for events with per-event field overrides
453
+ let fieldsByName: Js.Dict.t<_> = Js.Dict.empty()
454
+ switch jsonEvents {
455
+ | Some(jes) =>
456
+ jes->Array.forEach(je => {
457
+ let name = je["name"]
458
+ if je["blockFields"] != None || je["transactionFields"] != None {
459
+ fieldsByName->Js.Dict.set(name, je)
460
+ }
461
+ })
462
+ | None => ()
463
+ }
464
+ events->Js.Array2.forEachi((event, i) => {
465
+ let evmEvent = event->(Utils.magic: Internal.eventConfig => Internal.evmEventConfig)
466
+ let (selectedBlockFields, selectedTransactionFields) = switch fieldsByName->Js.Dict.get(
467
+ evmEvent.name,
468
+ ) {
469
+ | Some(je) => (
470
+ switch je["blockFields"] {
471
+ | Some(fields) =>
472
+ // Prepend always-included block fields for per-event overrides too
473
+ Utils.Set.fromArray(
474
+ Array.concat(
475
+ alwaysIncludedBlockFields,
476
+ fields->(Utils.magic: array<string> => array<Internal.evmBlockField>),
477
+ ),
478
+ )
479
+ | None => globalBlockFieldsSet
480
+ },
481
+ switch je["transactionFields"] {
482
+ | Some(fields) =>
483
+ Utils.Set.fromArray(
484
+ fields->(Utils.magic: array<string> => array<Internal.evmTransactionField>),
485
+ )
486
+ | None => globalTransactionFieldsSet
487
+ },
488
+ )
489
+ | None => (globalBlockFieldsSet, globalTransactionFieldsSet)
490
+ }
491
+ events->Js.Array2.unsafe_set(
492
+ i,
493
+ {...evmEvent, selectedBlockFields, selectedTransactionFields}->(
494
+ Utils.magic: Internal.evmEventConfig => Internal.eventConfig
495
+ ),
496
+ )
497
+ })
498
+ }
499
+
500
+ let fromPublic = (
501
+ publicConfigJson: Js.Json.t,
502
+ ~codegenChains: array<codegenChain>=[],
66
503
  ~maxAddrInPartition=5000,
67
504
  ) => {
68
- // Validate that lowercase addresses is not used with viem decoder
69
- if lowercaseAddresses && !shouldUseHypersyncClientDecoder {
505
+ // Parse public config
506
+ let publicConfig = try publicConfigJson->S.parseOrThrow(publicConfigSchema) catch {
507
+ | S.Raised(exn) =>
70
508
  Js.Exn.raiseError(
71
- "lowercase addresses is not supported when event_decoder is 'viem'. Please set event_decoder to 'hypersync-client' or change address_format to 'checksum'.",
509
+ `Invalid internal.config.ts: ${exn->Utils.prettifyExn->(Utils.magic: exn => string)}`,
72
510
  )
73
511
  }
74
512
 
513
+ // Determine ecosystem from publicConfig (extract just chains for unified handling)
514
+ let (publicChainsConfig, ecosystemName) = switch (
515
+ publicConfig["evm"],
516
+ publicConfig["fuel"],
517
+ publicConfig["svm"],
518
+ ) {
519
+ | (Some(ecosystemConfig), None, None) => (ecosystemConfig["chains"], Ecosystem.Evm)
520
+ | (None, Some(ecosystemConfig), None) => (ecosystemConfig["chains"], Ecosystem.Fuel)
521
+ | (None, None, Some(ecosystemConfig)) => (ecosystemConfig["chains"], Ecosystem.Svm)
522
+ | (None, None, None) =>
523
+ Js.Exn.raiseError("Invalid indexer config: No ecosystem configured (evm, fuel, or svm)")
524
+ | _ =>
525
+ Js.Exn.raiseError(
526
+ "Invalid indexer config: Multiple ecosystems are not supported for a single indexer",
527
+ )
528
+ }
529
+
530
+ // Extract EVM-specific options with defaults
531
+ let lowercaseAddresses = switch publicConfig["evm"] {
532
+ | Some(evm) => evm["addressFormat"]->Option.getWithDefault(Checksum) == Lowercase
533
+ | None => false
534
+ }
535
+
536
+ // Parse ABIs from public config
537
+ let publicContractsConfig = switch (ecosystemName, publicConfig["evm"], publicConfig["fuel"]) {
538
+ | (Ecosystem.Evm, Some(evm), _) => evm["contracts"]
539
+ | (Ecosystem.Fuel, _, Some(fuel)) => fuel["contracts"]
540
+ | _ => None
541
+ }
542
+
543
+ // Create global field selection Sets once (shared across events without per-event overrides)
544
+ let (globalBlockFieldsSet, globalTransactionFieldsSet) = switch publicConfig["evm"] {
545
+ | Some(evm) => (
546
+ Utils.Set.fromArray(
547
+ Array.concat(
548
+ alwaysIncludedBlockFields,
549
+ evm["globalBlockFields"]->Option.getWithDefault([]),
550
+ ),
551
+ ),
552
+ Utils.Set.fromArray(evm["globalTransactionFields"]->Option.getWithDefault([])),
553
+ )
554
+ | None => (Utils.Set.fromArray(alwaysIncludedBlockFields), Utils.Set.make())
555
+ }
556
+
557
+ // Store ABI and event signatures for each contract
558
+ let contractsWithAbis: Js.Dict.t<(EvmTypes.Abi.t, array<string>)> = Js.Dict.empty()
559
+ // Per-event field selection overrides, keyed by capitalized contract name
560
+ let jsonEventsByContract: Js.Dict.t<array<_>> = Js.Dict.empty()
561
+ switch publicContractsConfig {
562
+ | Some(contractsDict) =>
563
+ contractsDict
564
+ ->Js.Dict.entries
565
+ ->Array.forEach(((contractName, contractConfig)) => {
566
+ let capitalizedName = contractName->Utils.String.capitalize
567
+ let abi = contractConfig["abi"]->(Utils.magic: Js.Json.t => EvmTypes.Abi.t)
568
+ let eventSignatures = switch contractConfig["events"] {
569
+ | Some(events) => events->Array.map(eventItem => eventItem["event"])
570
+ | None => []
571
+ }
572
+ contractsWithAbis->Js.Dict.set(capitalizedName, (abi, eventSignatures))
573
+ switch contractConfig["events"] {
574
+ | Some(events) => jsonEventsByContract->Js.Dict.set(capitalizedName, events)
575
+ | None => ()
576
+ }
577
+ })
578
+ | None => ()
579
+ }
580
+
581
+ // Index codegenChains by id for efficient lookup
582
+ let codegenChainById = Js.Dict.empty()
583
+ codegenChains->Array.forEach(codegenChain => {
584
+ codegenChainById->Js.Dict.set(codegenChain.id->Int.toString, codegenChain)
585
+ })
586
+
587
+ // Create a dictionary to store merged contracts with ABIs by chain id
588
+ let contractsByChainId: Js.Dict.t<array<contract>> = Js.Dict.empty()
589
+ codegenChains->Array.forEach(codegenChain => {
590
+ let mergedContracts = codegenChain.contracts->Array.map(codegenContract => {
591
+ switch contractsWithAbis->Js.Dict.get(codegenContract.name) {
592
+ | Some((abi, eventSignatures)) =>
593
+ // Parse addresses based on ecosystem and address format
594
+ let parsedAddresses = codegenContract.addresses->Array.map(
595
+ addressString => {
596
+ switch ecosystemName {
597
+ | Ecosystem.Evm =>
598
+ if lowercaseAddresses {
599
+ addressString->Address.Evm.fromStringLowercaseOrThrow
600
+ } else {
601
+ addressString->Address.Evm.fromStringOrThrow
602
+ }
603
+ | Ecosystem.Fuel | Ecosystem.Svm => addressString->Address.unsafeFromString
604
+ }
605
+ },
606
+ )
607
+
608
+ // Enrich EVM event configs with field selections from JSON config
609
+ if ecosystemName == Ecosystem.Evm {
610
+ enrichEvmFieldSelections(
611
+ codegenContract.events,
612
+ ~jsonEvents=jsonEventsByContract->Js.Dict.get(codegenContract.name),
613
+ ~globalBlockFieldsSet,
614
+ ~globalTransactionFieldsSet,
615
+ )
616
+ }
617
+ // Convert codegenContract to contract by adding abi and eventSignatures
618
+ {
619
+ name: codegenContract.name,
620
+ abi,
621
+ addresses: parsedAddresses,
622
+ events: codegenContract.events,
623
+ startBlock: codegenContract.startBlock,
624
+ eventSignatures,
625
+ }
626
+ | None =>
627
+ Js.Exn.raiseError(
628
+ `Contract "${codegenContract.name}" is missing ABI in public config (internal.config.ts)`,
629
+ )
630
+ }
631
+ })
632
+ contractsByChainId->Js.Dict.set(codegenChain.id->Int.toString, mergedContracts)
633
+ })
634
+
635
+ // Helper to convert parsed RPC config to evmRpcConfig
636
+ let parseRpcSourceFor = (sourceFor: rpcSourceFor): Source.sourceFor => {
637
+ switch sourceFor {
638
+ | Sync => Source.Sync
639
+ | Fallback => Source.Fallback
640
+ | Live => Source.Live
641
+ }
642
+ }
643
+
644
+ // Merge codegenChains with names from publicConfig
645
+ let chains =
646
+ publicChainsConfig
647
+ ->Js.Dict.keys
648
+ ->Js.Array2.map(chainName => {
649
+ let publicChainConfig = publicChainsConfig->Js.Dict.unsafeGet(chainName)
650
+ let chainId = publicChainConfig["id"]
651
+ let codegenChain = switch codegenChainById->Js.Dict.get(chainId->Int.toString) {
652
+ | Some(c) => c
653
+ | None =>
654
+ Js.Exn.raiseError(`Chain with id ${chainId->Int.toString} not found in codegen chains`)
655
+ }
656
+ let mergedContracts = switch contractsByChainId->Js.Dict.get(chainId->Int.toString) {
657
+ | Some(contracts) => contracts
658
+ | None =>
659
+ Js.Exn.raiseError(
660
+ `Contracts for chain with id ${chainId->Int.toString} not found in merged contracts`,
661
+ )
662
+ }
663
+
664
+ // Build sourceConfig from the parsed chain config
665
+ let sourceConfig = switch ecosystemName {
666
+ | Ecosystem.Evm =>
667
+ let rpcs =
668
+ publicChainConfig["rpcs"]
669
+ ->Option.getWithDefault([])
670
+ ->Array.map((rpcConfig): evmRpcConfig => {
671
+ // Build syncConfig from flattened fields
672
+ let initialBlockInterval = rpcConfig["initialBlockInterval"]
673
+ let backoffMultiplicative = rpcConfig["backoffMultiplicative"]
674
+ let accelerationAdditive = rpcConfig["accelerationAdditive"]
675
+ let intervalCeiling = rpcConfig["intervalCeiling"]
676
+ let backoffMillis = rpcConfig["backoffMillis"]
677
+ let queryTimeoutMillis = rpcConfig["queryTimeoutMillis"]
678
+ let fallbackStallTimeout = rpcConfig["fallbackStallTimeout"]
679
+ let pollingInterval = rpcConfig["pollingInterval"]
680
+ let hasSyncConfig =
681
+ initialBlockInterval->Option.isSome ||
682
+ backoffMultiplicative->Option.isSome ||
683
+ accelerationAdditive->Option.isSome ||
684
+ intervalCeiling->Option.isSome ||
685
+ backoffMillis->Option.isSome ||
686
+ queryTimeoutMillis->Option.isSome ||
687
+ fallbackStallTimeout->Option.isSome ||
688
+ pollingInterval->Option.isSome
689
+ let syncConfig: option<sourceSyncOptions> = if hasSyncConfig {
690
+ Some({
691
+ ?initialBlockInterval,
692
+ ?backoffMultiplicative,
693
+ ?accelerationAdditive,
694
+ ?intervalCeiling,
695
+ ?backoffMillis,
696
+ ?queryTimeoutMillis,
697
+ ?fallbackStallTimeout,
698
+ ?pollingInterval,
699
+ })
700
+ } else {
701
+ None
702
+ }
703
+ {
704
+ url: rpcConfig["url"],
705
+ sourceFor: parseRpcSourceFor(rpcConfig["for"]),
706
+ syncConfig,
707
+ ws: rpcConfig["ws"],
708
+ }
709
+ })
710
+ EvmSourceConfig({hypersync: publicChainConfig["hypersync"], rpcs})
711
+ | Ecosystem.Fuel =>
712
+ switch publicChainConfig["hypersync"] {
713
+ | Some(hypersync) => FuelSourceConfig({hypersync: hypersync})
714
+ | None => Js.Exn.raiseError(`Chain ${chainName} is missing hypersync endpoint in config`)
715
+ }
716
+ | Ecosystem.Svm =>
717
+ switch publicChainConfig["rpc"] {
718
+ | Some(rpc) => SvmSourceConfig({rpc: rpc})
719
+ | None => Js.Exn.raiseError(`Chain ${chainName} is missing rpc endpoint in config`)
720
+ }
721
+ }
722
+
723
+ {
724
+ name: chainName,
725
+ id: codegenChain.id,
726
+ startBlock: publicChainConfig["startBlock"],
727
+ endBlock: ?publicChainConfig["endBlock"],
728
+ maxReorgDepth: switch ecosystemName {
729
+ | Ecosystem.Evm => publicChainConfig["maxReorgDepth"]->Option.getWithDefault(200)
730
+ // Fuel doesn't have reorgs, SVM reorg handling is not supported
731
+ | Ecosystem.Fuel | Ecosystem.Svm => 0
732
+ },
733
+ blockLag: publicChainConfig["blockLag"]->Option.getWithDefault(0),
734
+ contracts: mergedContracts,
735
+ sourceConfig,
736
+ }
737
+ })
738
+
75
739
  let chainMap =
76
740
  chains
77
- ->Js.Array2.map(n => {
78
- (ChainMap.Chain.makeUnsafe(~chainId=n.id), n)
741
+ ->Js.Array2.map(chain => {
742
+ (ChainMap.Chain.makeUnsafe(~chainId=chain.id), chain)
79
743
  })
80
744
  ->ChainMap.fromArrayUnsafe
81
745
 
@@ -88,20 +752,71 @@ let make = (
88
752
  })
89
753
  })
90
754
 
91
- let ecosystem = Ecosystem.fromName(ecosystem)
755
+ let ecosystem = switch ecosystemName {
756
+ | Ecosystem.Evm => Evm.ecosystem
757
+ | Ecosystem.Fuel => Fuel.ecosystem
758
+ | Ecosystem.Svm => Svm.ecosystem
759
+ }
760
+
761
+ // Parse enums and entities from JSON config
762
+ let allEnums =
763
+ publicConfig["enums"]
764
+ ->Option.getWithDefault(Js.Dict.empty())
765
+ ->parseEnumsFromJson
766
+
767
+ let enumConfigsByName =
768
+ allEnums
769
+ ->Js.Array2.map(enumConfig => (enumConfig.name, enumConfig))
770
+ ->Js.Dict.fromArray
771
+
772
+ let userEntities =
773
+ publicConfig["entities"]
774
+ ->Option.getWithDefault([])
775
+ ->parseEntitiesFromJson(~enumConfigsByName)
776
+
777
+ let allEntities = userEntities->Js.Array2.concat([DynamicContractRegistry.entityConfig])
778
+
779
+ let userEntitiesByName =
780
+ userEntities
781
+ ->Js.Array2.map(entityConfig => {
782
+ (entityConfig.name, entityConfig)
783
+ })
784
+ ->Js.Dict.fromArray
785
+
786
+ // Extract contract handlers from the public config
787
+ let contractHandlers = switch publicContractsConfig {
788
+ | Some(contractsDict) =>
789
+ contractsDict
790
+ ->Js.Dict.entries
791
+ ->Js.Array2.map(((contractName, contractConfig)) => {
792
+ {
793
+ name: contractName->Utils.String.capitalize,
794
+ handler: contractConfig["handler"],
795
+ }
796
+ })
797
+ | None => []
798
+ }
92
799
 
93
800
  {
94
- shouldRollbackOnReorg,
95
- shouldSaveFullHistory,
96
- multichain,
801
+ name: publicConfig["name"],
802
+ description: publicConfig["description"],
803
+ handlers: publicConfig["handlers"]->Option.getWithDefault("src/handlers"),
804
+ contractHandlers,
805
+ shouldRollbackOnReorg: publicConfig["rollbackOnReorg"]->Option.getWithDefault(true),
806
+ shouldSaveFullHistory: publicConfig["saveFullHistory"]->Option.getWithDefault(false),
807
+ multichain: publicConfig["multichain"]->Option.getWithDefault(Unordered),
97
808
  chainMap,
98
809
  defaultChain: chains->Array.get(0),
99
- enableRawEvents,
810
+ enableRawEvents: publicConfig["rawEvents"]->Option.getWithDefault(false),
100
811
  ecosystem,
101
812
  maxAddrInPartition,
102
- batchSize,
813
+ batchSize: publicConfig["fullBatchSize"]->Option.getWithDefault(5000),
103
814
  lowercaseAddresses,
104
815
  addContractNameToContractNameMapping,
816
+ userEntitiesByName,
817
+ userEntities,
818
+ allEntities,
819
+ allEnums,
105
820
  }
106
821
  }
107
822