envio 3.0.0-alpha.2 → 3.0.0-alpha.21

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