envio 3.3.0-alpha.7 → 3.3.0-alpha.9
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.
- package/index.d.ts +23 -5
- package/package.json +6 -6
- package/src/Api.res +1 -1
- package/src/Api.res.mjs +1 -1
- package/src/ChainFetching.res +21 -10
- package/src/ChainFetching.res.mjs +11 -10
- package/src/ChainState.res +401 -171
- package/src/ChainState.res.mjs +270 -104
- package/src/ChainState.resi +19 -7
- package/src/Config.res +11 -27
- package/src/Config.res.mjs +8 -7
- package/src/Core.res +7 -0
- package/src/CrossChainState.res +82 -82
- package/src/CrossChainState.res.mjs +52 -59
- package/src/CrossChainState.resi +1 -1
- package/src/Ecosystem.res +3 -3
- package/src/Ecosystem.res.mjs +3 -3
- package/src/Envio.res +14 -9
- package/src/EnvioGlobal.res +53 -0
- package/src/EnvioGlobal.res.mjs +31 -0
- package/src/EventConfigBuilder.res +117 -69
- package/src/EventConfigBuilder.res.mjs +71 -34
- package/src/EventProcessing.res +19 -15
- package/src/EventProcessing.res.mjs +13 -12
- package/src/FetchState.res +410 -185
- package/src/FetchState.res.mjs +268 -253
- package/src/HandlerLoader.res +8 -113
- package/src/HandlerLoader.res.mjs +2 -88
- package/src/HandlerRegister.res +518 -144
- package/src/HandlerRegister.res.mjs +285 -133
- package/src/HandlerRegister.resi +24 -8
- package/src/IndexerState.res +6 -3
- package/src/IndexerState.res.mjs +3 -3
- package/src/IndexerState.resi +1 -1
- package/src/IndexingAddresses.res +10 -7
- package/src/IndexingAddresses.res.mjs +8 -7
- package/src/IndexingAddresses.resi +3 -1
- package/src/Internal.res +104 -39
- package/src/Internal.res.mjs +11 -1
- package/src/LogSelection.res +102 -165
- package/src/LogSelection.res.mjs +101 -116
- package/src/Main.res +49 -164
- package/src/Main.res.mjs +39 -104
- package/src/Metrics.res +43 -2
- package/src/Metrics.res.mjs +39 -3
- package/src/Prometheus.res +0 -35
- package/src/Prometheus.res.mjs +33 -68
- package/src/RawEvent.res +7 -2
- package/src/RawEvent.res.mjs +4 -4
- package/src/RollbackCommit.res +4 -1
- package/src/RollbackCommit.res.mjs +3 -2
- package/src/SimulateItems.res +20 -7
- package/src/SimulateItems.res.mjs +7 -11
- package/src/TestIndexer.res +1 -1
- package/src/TestIndexer.res.mjs +1 -1
- package/src/sources/BlockStore.res +46 -0
- package/src/sources/BlockStore.res.mjs +24 -0
- package/src/sources/EventRouter.res +19 -12
- package/src/sources/EventRouter.res.mjs +7 -5
- package/src/sources/Evm.res +58 -5
- package/src/sources/Evm.res.mjs +44 -5
- package/src/sources/EvmChain.res +14 -18
- package/src/sources/EvmChain.res.mjs +12 -13
- package/src/sources/FieldMask.res +39 -0
- package/src/sources/FieldMask.res.mjs +42 -0
- package/src/sources/Fuel.res +8 -3
- package/src/sources/Fuel.res.mjs +5 -4
- package/src/sources/HyperFuelSource.res +31 -40
- package/src/sources/HyperFuelSource.res.mjs +52 -44
- package/src/sources/HyperSync.res +31 -9
- package/src/sources/HyperSync.res.mjs +47 -31
- package/src/sources/HyperSync.resi +10 -3
- package/src/sources/HyperSyncClient.res +15 -4
- package/src/sources/HyperSyncHeightStream.res +1 -8
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
- package/src/sources/HyperSyncSource.res +58 -90
- package/src/sources/HyperSyncSource.res.mjs +57 -66
- package/src/sources/RpcSource.res +118 -120
- package/src/sources/RpcSource.res.mjs +92 -74
- package/src/sources/RpcWebSocketHeightStream.res +0 -5
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
- package/src/sources/SimulateSource.res +5 -3
- package/src/sources/SimulateSource.res.mjs +12 -4
- package/src/sources/Source.res +19 -5
- package/src/sources/SourceManager.res +60 -10
- package/src/sources/SourceManager.res.mjs +54 -8
- package/src/sources/SourceManager.resi +10 -0
- package/src/sources/Svm.res +14 -10
- package/src/sources/Svm.res.mjs +23 -6
- package/src/sources/SvmHyperSyncClient.res +5 -6
- package/src/sources/SvmHyperSyncSource.res +82 -41
- package/src/sources/SvmHyperSyncSource.res.mjs +86 -41
- package/src/sources/TransactionStore.res +6 -107
- package/src/sources/TransactionStore.res.mjs +5 -86
- package/svm.schema.json +19 -0
package/src/HandlerRegister.res
CHANGED
|
@@ -10,58 +10,38 @@ let empty = {
|
|
|
10
10
|
eventOptions: None,
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
// Per-chain onEventRegistrations built from the event definitions in
|
|
14
|
+
// `Config.t` plus whatever handler/contractRegister/eventOptions got
|
|
15
|
+
// registered for them, and the onBlock registrations collected during
|
|
16
|
+
// registration.
|
|
17
|
+
type chainRegistrations = {
|
|
18
|
+
onEventRegistrations: array<Internal.onEventRegistration>,
|
|
19
|
+
onBlockRegistrations: array<Internal.onBlockRegistration>,
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
eventRegistrations: dict<eventRegistration>,
|
|
32
|
-
activeRegistration: ref<option<activeRegistration>>,
|
|
33
|
-
preRegistered: array<activeRegistration => unit>,
|
|
22
|
+
// The finished registration state returned by `finishRegistration`.
|
|
23
|
+
type registrationsByChainId = dict<chainRegistrations>
|
|
24
|
+
|
|
25
|
+
// Incrementally built during registration: every `indexer.onEvent` /
|
|
26
|
+
// `.contractRegister` call resolves its `where` per chain right away and
|
|
27
|
+
// stores the resulting registration here, keyed by "Contract.Event", so
|
|
28
|
+
// invalid configuration throws at the user's registration call site.
|
|
29
|
+
type pendingChainRegistrations = {
|
|
30
|
+
onEventRegistrations: dict<Internal.onEventRegistration>,
|
|
31
|
+
onBlockRegistrations: array<Internal.onBlockRegistration>,
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
%%private(
|
|
43
|
-
let registry: registryShape = {
|
|
44
|
-
let version = Utils.EnvioPackage.value.version
|
|
45
|
-
switch globalThis.__envioRegistry->Nullable.toOption {
|
|
46
|
-
| Some(existing) if existing.version === version => existing
|
|
47
|
-
| Some(existing) =>
|
|
48
|
-
JsError.throwWithMessage(
|
|
49
|
-
`Multiple incompatible envio versions loaded in the same process: ${existing.version} and ${version}. Deduplicate the 'envio' dependency in your project.`,
|
|
50
|
-
)
|
|
51
|
-
| None =>
|
|
52
|
-
let fresh = {
|
|
53
|
-
version,
|
|
54
|
-
eventRegistrations: Dict.make(),
|
|
55
|
-
activeRegistration: ref(None),
|
|
56
|
-
preRegistered: [],
|
|
57
|
-
}
|
|
58
|
-
globalThis.__envioRegistry = Nullable.make(fresh)
|
|
59
|
-
fresh
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
)
|
|
34
|
+
type activeRegistration = {
|
|
35
|
+
config: Config.t,
|
|
36
|
+
registrationsByChainId: dict<pendingChainRegistrations>,
|
|
37
|
+
mutable finished: bool,
|
|
38
|
+
}
|
|
63
39
|
|
|
64
|
-
|
|
40
|
+
// Registration state lives in the process-wide `EnvioGlobal` record (shared
|
|
41
|
+
// across duplicate envio module instances); the slots are opaque there, so
|
|
42
|
+
// cast them to the real types once here.
|
|
43
|
+
let eventRegistrations =
|
|
44
|
+
EnvioGlobal.value.eventRegistrations->(Utils.magic: dict<unknown> => dict<eventRegistration>)
|
|
65
45
|
|
|
66
46
|
let getKey = (~contractName, ~eventName) => contractName ++ "." ++ eventName
|
|
67
47
|
|
|
@@ -76,7 +56,8 @@ let set = (~contractName, ~eventName, registration) => {
|
|
|
76
56
|
eventRegistrations->Dict.set(getKey(~contractName, ~eventName), registration)
|
|
77
57
|
}
|
|
78
58
|
|
|
79
|
-
let
|
|
59
|
+
let getActiveRegistration = () =>
|
|
60
|
+
EnvioGlobal.value.activeRegistration->(Utils.magic: option<unknown> => option<activeRegistration>)
|
|
80
61
|
|
|
81
62
|
// Might happen for tests when the handler file
|
|
82
63
|
// is imported by a non-envio process (eg mocha)
|
|
@@ -85,10 +66,13 @@ let activeRegistration = registry.activeRegistration
|
|
|
85
66
|
// Theoretically we could keep preRegistration without an explicit start
|
|
86
67
|
// but I want it to be this way, so for the actual indexer run
|
|
87
68
|
// an error is thrown with the exact stack trace where the handler was registered.
|
|
88
|
-
let preRegistered =
|
|
69
|
+
let preRegistered =
|
|
70
|
+
EnvioGlobal.value.preRegistered->(
|
|
71
|
+
Utils.magic: array<unknown> => array<activeRegistration => unit>
|
|
72
|
+
)
|
|
89
73
|
|
|
90
74
|
let withRegistration = (fn: activeRegistration => unit) => {
|
|
91
|
-
switch
|
|
75
|
+
switch getActiveRegistration() {
|
|
92
76
|
| None => preRegistered->Array.push(fn)
|
|
93
77
|
| Some(r) =>
|
|
94
78
|
if r.finished {
|
|
@@ -101,15 +85,13 @@ let withRegistration = (fn: activeRegistration => unit) => {
|
|
|
101
85
|
}
|
|
102
86
|
}
|
|
103
87
|
|
|
104
|
-
let startRegistration = (~
|
|
88
|
+
let startRegistration = (~config: Config.t) => {
|
|
105
89
|
let r = {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
onBlockByChainId: Dict.make(),
|
|
109
|
-
},
|
|
90
|
+
config,
|
|
91
|
+
registrationsByChainId: Dict.make(),
|
|
110
92
|
finished: false,
|
|
111
93
|
}
|
|
112
|
-
activeRegistration
|
|
94
|
+
EnvioGlobal.value.activeRegistration = Some(r->(Utils.magic: activeRegistration => unknown))
|
|
113
95
|
while preRegistered->Array.length > 0 {
|
|
114
96
|
// Loop + cleanup in one go
|
|
115
97
|
switch preRegistered->Array.pop {
|
|
@@ -119,69 +101,85 @@ let startRegistration = (~ecosystem) => {
|
|
|
119
101
|
}
|
|
120
102
|
}
|
|
121
103
|
|
|
122
|
-
let
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
r.registrations
|
|
127
|
-
}
|
|
104
|
+
let getPendingChainRegistrations = (r: activeRegistration, ~chainId: int) => {
|
|
105
|
+
let key = chainId->Int.toString
|
|
106
|
+
switch r.registrationsByChainId->Utils.Dict.dangerouslyGetNonOption(key) {
|
|
107
|
+
| Some(pending) => pending
|
|
128
108
|
| None =>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
109
|
+
let fresh = {
|
|
110
|
+
onEventRegistrations: Dict.make(),
|
|
111
|
+
onBlockRegistrations: [],
|
|
112
|
+
}
|
|
113
|
+
r.registrationsByChainId->Dict.set(key, fresh)
|
|
114
|
+
fresh
|
|
132
115
|
}
|
|
133
116
|
}
|
|
134
117
|
|
|
135
|
-
let
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
118
|
+
let buildOnEventRegistrationWith = (
|
|
119
|
+
~config: Config.t,
|
|
120
|
+
~chainId: int,
|
|
121
|
+
~eventConfig: Internal.eventConfig,
|
|
122
|
+
~isWildcard: bool,
|
|
123
|
+
~handler: option<Internal.handler>,
|
|
124
|
+
~contractRegister: option<Internal.contractRegister>,
|
|
125
|
+
~where: option<JSON.t>,
|
|
126
|
+
~startBlock=?,
|
|
127
|
+
): Internal.onEventRegistration => {
|
|
128
|
+
switch config.ecosystem.name {
|
|
129
|
+
| Fuel =>
|
|
130
|
+
(EventConfigBuilder.buildFuelOnEventRegistration(
|
|
131
|
+
~eventConfig=eventConfig->(Utils.magic: Internal.eventConfig => Internal.fuelEventConfig),
|
|
132
|
+
~isWildcard,
|
|
133
|
+
~handler,
|
|
134
|
+
~contractRegister,
|
|
135
|
+
~startBlock?,
|
|
136
|
+
) :> Internal.onEventRegistration)
|
|
137
|
+
| Svm =>
|
|
138
|
+
(EventConfigBuilder.buildSvmOnEventRegistration(
|
|
139
|
+
~eventConfig=eventConfig->(
|
|
140
|
+
Utils.magic: Internal.eventConfig => Internal.svmInstructionEventConfig
|
|
141
|
+
),
|
|
142
|
+
~isWildcard,
|
|
143
|
+
~handler,
|
|
144
|
+
~contractRegister,
|
|
145
|
+
~startBlock?,
|
|
146
|
+
) :> Internal.onEventRegistration)
|
|
147
|
+
| Evm =>
|
|
148
|
+
(EventConfigBuilder.buildEvmOnEventRegistration(
|
|
149
|
+
~eventConfig=eventConfig->(Utils.magic: Internal.eventConfig => Internal.evmEventConfig),
|
|
150
|
+
~isWildcard,
|
|
151
|
+
~handler,
|
|
152
|
+
~contractRegister,
|
|
153
|
+
~where,
|
|
154
|
+
~chainId,
|
|
155
|
+
~onEventBlockFilterSchema=config.ecosystem.onEventBlockFilterSchema,
|
|
156
|
+
~startBlock?,
|
|
157
|
+
) :> Internal.onEventRegistration)
|
|
139
158
|
}
|
|
140
159
|
}
|
|
141
160
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
let onBlockByChainId = registration.registrations.onBlockByChainId
|
|
165
|
-
let key = chainId->Int.toString
|
|
166
|
-
let index =
|
|
167
|
-
onBlockByChainId
|
|
168
|
-
->Utils.Dict.dangerouslyGetNonOption(key)
|
|
169
|
-
->Option.mapOr(0, configs => configs->Array.length)
|
|
170
|
-
onBlockByChainId->Utils.Dict.push(
|
|
171
|
-
key,
|
|
172
|
-
(
|
|
173
|
-
{
|
|
174
|
-
index,
|
|
175
|
-
name,
|
|
176
|
-
startBlock,
|
|
177
|
-
endBlock,
|
|
178
|
-
interval,
|
|
179
|
-
chainId,
|
|
180
|
-
handler,
|
|
181
|
-
}: Internal.onBlockConfig
|
|
182
|
-
),
|
|
183
|
-
)
|
|
184
|
-
})
|
|
161
|
+
// Enrich one event definition into its (event, chain) registration using
|
|
162
|
+
// whatever handler/contractRegister/where the user registered for it. Shared
|
|
163
|
+
// by the incremental per-chain sync below, `simulate`, and test helpers so
|
|
164
|
+
// they stay in sync instead of re-deriving the per-ecosystem dispatch each
|
|
165
|
+
// place.
|
|
166
|
+
let buildOnEventRegistration = (
|
|
167
|
+
~config: Config.t,
|
|
168
|
+
~chainId: int,
|
|
169
|
+
~eventConfig: Internal.eventConfig,
|
|
170
|
+
~startBlock=?,
|
|
171
|
+
): Internal.onEventRegistration => {
|
|
172
|
+
let t = get(~contractName=eventConfig.contractName, ~eventName=eventConfig.name)
|
|
173
|
+
buildOnEventRegistrationWith(
|
|
174
|
+
~config,
|
|
175
|
+
~chainId,
|
|
176
|
+
~eventConfig,
|
|
177
|
+
~isWildcard=t.eventOptions->Option.flatMap(v => v.wildcard)->Option.getOr(false),
|
|
178
|
+
~handler=t.handler,
|
|
179
|
+
~contractRegister=t.contractRegister,
|
|
180
|
+
~where=t.eventOptions->Option.flatMap(v => v.where),
|
|
181
|
+
~startBlock?,
|
|
182
|
+
)
|
|
185
183
|
}
|
|
186
184
|
|
|
187
185
|
let getHandler = (~contractName, ~eventName) => get(~contractName, ~eventName).handler
|
|
@@ -189,9 +187,6 @@ let getHandler = (~contractName, ~eventName) => get(~contractName, ~eventName).h
|
|
|
189
187
|
let getContractRegister = (~contractName, ~eventName) =>
|
|
190
188
|
get(~contractName, ~eventName).contractRegister
|
|
191
189
|
|
|
192
|
-
let getOnEventWhere = (~contractName, ~eventName) =>
|
|
193
|
-
get(~contractName, ~eventName).eventOptions->Option.flatMap(value => value.where)
|
|
194
|
-
|
|
195
190
|
let isWildcard = (~contractName, ~eventName) =>
|
|
196
191
|
get(~contractName, ~eventName).eventOptions
|
|
197
192
|
->Option.flatMap(value => value.wildcard)
|
|
@@ -210,35 +205,79 @@ let raiseDuplicateRegistration = (~contractName, ~eventName, ~msg, ~logger) => {
|
|
|
210
205
|
JsError.throwWithMessage(fullMsg)
|
|
211
206
|
}
|
|
212
207
|
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
// back to referential equality on the function reference.
|
|
218
|
-
let whereMatch = (a: option<JSON.t>, b: option<JSON.t>) => {
|
|
219
|
-
switch (a, b) {
|
|
220
|
-
| (None, None) => true
|
|
221
|
-
| (Some(a), Some(b)) =>
|
|
222
|
-
if typeof(a) === #function || typeof(b) === #function {
|
|
223
|
-
a === b
|
|
224
|
-
} else {
|
|
225
|
-
a == b
|
|
226
|
-
}
|
|
227
|
-
| _ => false
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
208
|
+
// `where` equality is checked per chain on the resolved structure (see
|
|
209
|
+
// `syncOnEventRegistrations`), so registration options only need to agree on
|
|
210
|
+
// `wildcard` here — two callbacks that resolve to identical filters count as
|
|
211
|
+
// identical options even when the function references differ.
|
|
231
212
|
let eventOptionsMatch = (
|
|
232
213
|
existing: option<Internal.eventOptions<JSON.t>>,
|
|
233
214
|
incoming: option<Internal.eventOptions<JSON.t>>,
|
|
234
215
|
) => {
|
|
235
216
|
switch (existing, incoming) {
|
|
236
217
|
| (None, None) => true
|
|
237
|
-
| (Some(a), Some(b)) => a.wildcard === b.wildcard
|
|
218
|
+
| (Some(a), Some(b)) => a.wildcard === b.wildcard
|
|
238
219
|
| _ => false
|
|
239
220
|
}
|
|
240
221
|
}
|
|
241
222
|
|
|
223
|
+
let getResolvedWhere = (reg: Internal.onEventRegistration) =>
|
|
224
|
+
(
|
|
225
|
+
reg->(Utils.magic: Internal.onEventRegistration => Internal.evmOnEventRegistration)
|
|
226
|
+
).resolvedWhere
|
|
227
|
+
|
|
228
|
+
// Resolve the registration for every configured chain that defines the event
|
|
229
|
+
// and store it in the pending per-chain registry. When the chain already
|
|
230
|
+
// holds a registration for this event, the resolved `where` structures must
|
|
231
|
+
// deep-compare equal (`Values` by hex arrays, `ContractAddresses` by contract
|
|
232
|
+
// name, plus `startBlock`) — otherwise it's a conflicting duplicate
|
|
233
|
+
// registration. Both live registrations and `preRegistered` callbacks
|
|
234
|
+
// replayed by `startRegistration` run through this single code path.
|
|
235
|
+
let syncOnEventRegistrations = (
|
|
236
|
+
r: activeRegistration,
|
|
237
|
+
~contractName,
|
|
238
|
+
~eventName,
|
|
239
|
+
~where: option<JSON.t>,
|
|
240
|
+
~duplicateMsg,
|
|
241
|
+
~logger,
|
|
242
|
+
) => {
|
|
243
|
+
let config = r.config
|
|
244
|
+
let t = get(~contractName, ~eventName)
|
|
245
|
+
let isWildcard = t.eventOptions->Option.flatMap(v => v.wildcard)->Option.getOr(false)
|
|
246
|
+
let key = getKey(~contractName, ~eventName)
|
|
247
|
+
|
|
248
|
+
config.chainMap
|
|
249
|
+
->ChainMap.values
|
|
250
|
+
->Array.forEach(chainConfig => {
|
|
251
|
+
chainConfig.contracts->Array.forEach(contract => {
|
|
252
|
+
if contract.name === contractName {
|
|
253
|
+
switch contract.events->Array.find(e => e.name === eventName) {
|
|
254
|
+
| None => ()
|
|
255
|
+
| Some(eventConfig) =>
|
|
256
|
+
let newRegistration = buildOnEventRegistrationWith(
|
|
257
|
+
~config,
|
|
258
|
+
~chainId=chainConfig.id,
|
|
259
|
+
~eventConfig,
|
|
260
|
+
~isWildcard,
|
|
261
|
+
~handler=t.handler,
|
|
262
|
+
~contractRegister=t.contractRegister,
|
|
263
|
+
~where,
|
|
264
|
+
~startBlock=?contract.startBlock,
|
|
265
|
+
)
|
|
266
|
+
let pending = r->getPendingChainRegistrations(~chainId=chainConfig.id)
|
|
267
|
+
switch pending.onEventRegistrations->Utils.Dict.dangerouslyGetNonOption(key) {
|
|
268
|
+
| Some(existing) if config.ecosystem.name === Evm =>
|
|
269
|
+
if !(existing->getResolvedWhere == newRegistration->getResolvedWhere) {
|
|
270
|
+
raiseDuplicateRegistration(~contractName, ~eventName, ~msg=duplicateMsg, ~logger)
|
|
271
|
+
}
|
|
272
|
+
| _ => ()
|
|
273
|
+
}
|
|
274
|
+
pending.onEventRegistrations->Dict.set(key, newRegistration)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
242
281
|
let setEventOptions = (~contractName, ~eventName, ~eventOptions, ~logger=Logging.getLogger()) => {
|
|
243
282
|
switch eventOptions {
|
|
244
283
|
| Some(value) =>
|
|
@@ -267,9 +306,13 @@ let setHandler = (
|
|
|
267
306
|
~eventOptions,
|
|
268
307
|
~logger=Logging.getLogger(),
|
|
269
308
|
) => {
|
|
270
|
-
withRegistration(
|
|
309
|
+
withRegistration(registration => {
|
|
271
310
|
let t = get(~contractName, ~eventName)
|
|
272
311
|
let newHandler = handler->(Utils.magic: Internal.genericHandler<'args> => Internal.handler)
|
|
312
|
+
let incomingEventOptions =
|
|
313
|
+
eventOptions->Option.map(v =>
|
|
314
|
+
v->(Utils.magic: Internal.eventOptions<'where> => Internal.eventOptions<JSON.t>)
|
|
315
|
+
)
|
|
273
316
|
switch t.handler {
|
|
274
317
|
| None =>
|
|
275
318
|
setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger)
|
|
@@ -283,10 +326,6 @@ let setHandler = (
|
|
|
283
326
|
},
|
|
284
327
|
)
|
|
285
328
|
| Some(prevHandler) =>
|
|
286
|
-
let incomingEventOptions =
|
|
287
|
-
eventOptions->Option.map(v =>
|
|
288
|
-
v->(Utils.magic: Internal.eventOptions<'where> => Internal.eventOptions<JSON.t>)
|
|
289
|
-
)
|
|
290
329
|
if eventOptionsMatch(t.eventOptions, incomingEventOptions) {
|
|
291
330
|
let composedHandler: Internal.handler = async args => {
|
|
292
331
|
await prevHandler(args)
|
|
@@ -309,6 +348,13 @@ let setHandler = (
|
|
|
309
348
|
)
|
|
310
349
|
}
|
|
311
350
|
}
|
|
351
|
+
registration->syncOnEventRegistrations(
|
|
352
|
+
~contractName,
|
|
353
|
+
~eventName,
|
|
354
|
+
~where=incomingEventOptions->Option.flatMap(v => v.where),
|
|
355
|
+
~duplicateMsg="Cannot register a second handler with different options. Make sure all handlers for the same event use identical options (wildcard, where)",
|
|
356
|
+
~logger,
|
|
357
|
+
)
|
|
312
358
|
})
|
|
313
359
|
}
|
|
314
360
|
|
|
@@ -319,7 +365,7 @@ let setContractRegister = (
|
|
|
319
365
|
~eventOptions,
|
|
320
366
|
~logger=Logging.getLogger(),
|
|
321
367
|
) => {
|
|
322
|
-
withRegistration(
|
|
368
|
+
withRegistration(registration => {
|
|
323
369
|
let t = get(~contractName, ~eventName)
|
|
324
370
|
let newContractRegister =
|
|
325
371
|
contractRegister->(
|
|
@@ -327,6 +373,10 @@ let setContractRegister = (
|
|
|
327
373
|
Internal.genericContractRegisterArgs<'event, 'context>,
|
|
328
374
|
> => Internal.contractRegister
|
|
329
375
|
)
|
|
376
|
+
let incomingEventOptions =
|
|
377
|
+
eventOptions->Option.map(v =>
|
|
378
|
+
v->(Utils.magic: Internal.eventOptions<'where> => Internal.eventOptions<JSON.t>)
|
|
379
|
+
)
|
|
330
380
|
switch t.contractRegister {
|
|
331
381
|
| None =>
|
|
332
382
|
setEventOptions(~contractName, ~eventName, ~eventOptions, ~logger)
|
|
@@ -340,10 +390,6 @@ let setContractRegister = (
|
|
|
340
390
|
},
|
|
341
391
|
)
|
|
342
392
|
| Some(prevContractRegister) =>
|
|
343
|
-
let incomingEventOptions =
|
|
344
|
-
eventOptions->Option.map(v =>
|
|
345
|
-
v->(Utils.magic: Internal.eventOptions<'where> => Internal.eventOptions<JSON.t>)
|
|
346
|
-
)
|
|
347
393
|
if eventOptionsMatch(t.eventOptions, incomingEventOptions) {
|
|
348
394
|
let composedContractRegister: Internal.contractRegister = async args => {
|
|
349
395
|
await prevContractRegister(args)
|
|
@@ -366,5 +412,333 @@ let setContractRegister = (
|
|
|
366
412
|
)
|
|
367
413
|
}
|
|
368
414
|
}
|
|
415
|
+
registration->syncOnEventRegistrations(
|
|
416
|
+
~contractName,
|
|
417
|
+
~eventName,
|
|
418
|
+
~where=incomingEventOptions->Option.flatMap(v => v.where),
|
|
419
|
+
~duplicateMsg="Cannot register a second contractRegister with different options. Make sure all handlers for the same event use identical options (wildcard, where)",
|
|
420
|
+
~logger,
|
|
421
|
+
)
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Shape of the user-returned `{_gte?, _lte?, _every?}` filter chunk after
|
|
426
|
+
// the ecosystem-specific wrapper is stripped. Shared across all ecosystems —
|
|
427
|
+
// the outer `block.number` / `block.height` / `slot` unwrap lives on each
|
|
428
|
+
// ecosystem's `onBlockFilterSchema`, and the inner range fields are the
|
|
429
|
+
// same everywhere.
|
|
430
|
+
type blockRange = {
|
|
431
|
+
_gte: option<int>,
|
|
432
|
+
_lte: option<int>,
|
|
433
|
+
_every: int,
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// `S.strict` rejects unknown fields so typos like `_gt` / `_evry` surface
|
|
437
|
+
// with a readable schema error pointing at the offending key, instead of
|
|
438
|
+
// silently registering a broken filter. `_every` defaults to 1 inside the
|
|
439
|
+
// schema so the caller always sees a plain `int`, and `intMin(1)` rejects
|
|
440
|
+
// zero/negative strides — `(blockNumber - startBlock) % 0` would crash and
|
|
441
|
+
// any negative stride would never match.
|
|
442
|
+
let blockRangeSchema: S.t<blockRange> = S.object(s => {
|
|
443
|
+
_gte: s.field("_gte", S.option(S.int)),
|
|
444
|
+
_lte: s.field("_lte", S.option(S.int)),
|
|
445
|
+
_every: s.field("_every", S.option(S.int->S.intMin(1))->S.Option.getOr(1)),
|
|
446
|
+
})->S.strict
|
|
447
|
+
|
|
448
|
+
let defaultBlockRange: blockRange = {_gte: None, _lte: None, _every: 1}
|
|
449
|
+
|
|
450
|
+
// Two-stage parse: first the ecosystem-specific outer schema unwraps the
|
|
451
|
+
// wrapper (`block.number` / `block.height` / `slot`) and surfaces the
|
|
452
|
+
// inner chunk as raw `unknown`; then the shared `blockRangeSchema`
|
|
453
|
+
// validates the `{_gte?, _lte?, _every?}` fields. Keeping the inner
|
|
454
|
+
// validation in one place means typos and shape mismatches surface with
|
|
455
|
+
// the same user-friendly error regardless of ecosystem.
|
|
456
|
+
let extractRange = (filter: unknown, ~name, ~ecosystem: Ecosystem.t): blockRange =>
|
|
457
|
+
try {
|
|
458
|
+
switch filter->S.parseOrThrow(ecosystem.onBlockFilterSchema) {
|
|
459
|
+
| None => defaultBlockRange
|
|
460
|
+
| Some(inner) => inner->S.parseOrThrow(blockRangeSchema)
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
| S.Raised(exn) =>
|
|
464
|
+
JsError.throwWithMessage(
|
|
465
|
+
`\`indexer.${ecosystem.onBlockMethodName}("${name}")\` \`where\` returned an invalid filter: ${exn
|
|
466
|
+
->Utils.prettifyExn
|
|
467
|
+
->(Utils.magic: exn => string)}`,
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Mirrors `Envio.onBlockWhereArgs` without depending on the module.
|
|
472
|
+
type onBlockWhereArgs = {chain: unknown}
|
|
473
|
+
|
|
474
|
+
// `where` is evaluated once per configured chain at registration time.
|
|
475
|
+
// Decoded ranges/stride feed directly into the per-chain registration store
|
|
476
|
+
// so the fetcher's `(blockNumber - handlerStartBlock) % interval === 0`
|
|
477
|
+
// math in `FetchState` stays untouched. Deferred via `withRegistration` so
|
|
478
|
+
// the per-chain loop sees the registration's config, which may be a narrowed
|
|
479
|
+
// version of the generated one (TestIndexer runs with a per-test chain
|
|
480
|
+
// subset). `where` arrives unvalidated (`unknown`) straight from the user's
|
|
481
|
+
// options object.
|
|
482
|
+
let registerOnBlock = (
|
|
483
|
+
~name: string,
|
|
484
|
+
~where: unknown,
|
|
485
|
+
~handler: Internal.onBlockArgs => promise<unit>,
|
|
486
|
+
~getChainsObject: Config.t => dict<unknown>,
|
|
487
|
+
) => {
|
|
488
|
+
withRegistration(registration => {
|
|
489
|
+
let config = registration.config
|
|
490
|
+
let ecosystem = config.ecosystem
|
|
491
|
+
let chainsDict = getChainsObject(config)
|
|
492
|
+
let logger = Logging.createChild(~params={"onBlock": name})
|
|
493
|
+
|
|
494
|
+
// `where` must be a function (unlike onEvent, which also accepts a static
|
|
495
|
+
// value). A static value would have to be evaluated against every chain
|
|
496
|
+
// independently, which has no useful semantic for block handlers.
|
|
497
|
+
// Normalize undefined/null to None up front so the per-chain loop below
|
|
498
|
+
// can't accidentally call `null` as a predicate.
|
|
499
|
+
let where = switch where {
|
|
500
|
+
| w if w === %raw(`undefined`) || w === %raw(`null`) => None
|
|
501
|
+
| w if typeof(w) === #function => Some(w->(Utils.magic: unknown => onBlockWhereArgs => unknown))
|
|
502
|
+
| w =>
|
|
503
|
+
JsError.throwWithMessage(
|
|
504
|
+
`\`indexer.${ecosystem.onBlockMethodName}("${name}")\` expected \`where\` to be a function or omitted, but got ${(typeof(
|
|
505
|
+
w,
|
|
506
|
+
) :> string)}.`,
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let matchedAny = ref(false)
|
|
511
|
+
|
|
512
|
+
config.chainMap
|
|
513
|
+
->ChainMap.values
|
|
514
|
+
->Array.forEach(chainConfig => {
|
|
515
|
+
let chainId = chainConfig.id
|
|
516
|
+
let chainObj = chainsDict->Dict.getUnsafe(chainId->Int.toString)
|
|
517
|
+
|
|
518
|
+
// Predicate returns `true` → match with no filter; `false` → skip;
|
|
519
|
+
// any plain object → structured filter. `undefined`/`null` returns
|
|
520
|
+
// are rejected — the TS type excludes `void`, so a missing return is
|
|
521
|
+
// a user bug we surface early rather than silently match-all.
|
|
522
|
+
let result = switch where {
|
|
523
|
+
| None => %raw(`true`)
|
|
524
|
+
| Some(predicate) => predicate({chain: chainObj})
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let (shouldRegister, range) = if result === %raw(`true`) {
|
|
528
|
+
(true, defaultBlockRange)
|
|
529
|
+
} else if result === %raw(`false`) {
|
|
530
|
+
(false, defaultBlockRange)
|
|
531
|
+
} else if typeof(result) === #object && !(result->Array.isArray) && result !== %raw(`null`) {
|
|
532
|
+
(true, extractRange(result, ~name, ~ecosystem))
|
|
533
|
+
} else {
|
|
534
|
+
// Reject numbers, strings, functions, arrays, undefined, null —
|
|
535
|
+
// anything that isn't bool or a plain object would silently
|
|
536
|
+
// misregister.
|
|
537
|
+
JsError.throwWithMessage(
|
|
538
|
+
`\`indexer.${ecosystem.onBlockMethodName}("${name}")\` \`where\` predicate returned an invalid value of type ${(typeof(
|
|
539
|
+
result,
|
|
540
|
+
) :> string)}. Expected boolean or a filter object.`,
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if shouldRegister {
|
|
545
|
+
matchedAny := true
|
|
546
|
+
if range._gte->Option.getOr(chainConfig.startBlock) < chainConfig.startBlock {
|
|
547
|
+
JsError.throwWithMessage(
|
|
548
|
+
`The start block for onBlock handler "${name}" is less than the chain start block (${chainConfig.startBlock->Int.toString}). This is not supported yet.`,
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
switch chainConfig.endBlock {
|
|
552
|
+
| Some(chainEndBlock) =>
|
|
553
|
+
if range._lte->Option.getOr(chainEndBlock) > chainEndBlock {
|
|
554
|
+
JsError.throwWithMessage(
|
|
555
|
+
`The end block for onBlock handler "${name}" is greater than the chain end block (${chainEndBlock->Int.toString}). This is not supported yet.`,
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
| None => ()
|
|
559
|
+
}
|
|
560
|
+
let pending = registration->getPendingChainRegistrations(~chainId)
|
|
561
|
+
pending.onBlockRegistrations
|
|
562
|
+
->Array.push(
|
|
563
|
+
(
|
|
564
|
+
{
|
|
565
|
+
index: pending.onBlockRegistrations->Array.length,
|
|
566
|
+
name,
|
|
567
|
+
startBlock: range._gte,
|
|
568
|
+
endBlock: range._lte,
|
|
569
|
+
interval: range._every,
|
|
570
|
+
chainId,
|
|
571
|
+
handler,
|
|
572
|
+
}: Internal.onBlockRegistration
|
|
573
|
+
),
|
|
574
|
+
)
|
|
575
|
+
->ignore
|
|
576
|
+
}
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
// Catches misconfigured `where` predicates that return `false` for every
|
|
580
|
+
// configured chain — the handler would otherwise never fire with no hint.
|
|
581
|
+
// Includes the ecosystem-specific method name so SVM users see "onSlot"
|
|
582
|
+
// and don't get confused looking for a "Block handler" they never wrote.
|
|
583
|
+
if !matchedAny.contents {
|
|
584
|
+
logger->Logging.childWarn(
|
|
585
|
+
`\`indexer.${ecosystem.onBlockMethodName}\` matched 0 chains. Check the \`where\` predicate.`,
|
|
586
|
+
)
|
|
587
|
+
}
|
|
369
588
|
})
|
|
370
589
|
}
|
|
590
|
+
|
|
591
|
+
let finishRegistration = (~config: Config.t): registrationsByChainId => {
|
|
592
|
+
switch getActiveRegistration() {
|
|
593
|
+
| Some(r) => {
|
|
594
|
+
r.finished = true
|
|
595
|
+
let notRegisteredEventsByContract: dict<Utils.Set.t<string>> = Dict.make()
|
|
596
|
+
let registrationsByChainId: registrationsByChainId = Dict.make()
|
|
597
|
+
config.chainMap
|
|
598
|
+
->ChainMap.values
|
|
599
|
+
->Array.forEach(chainConfig => {
|
|
600
|
+
let key = chainConfig.id->Int.toString
|
|
601
|
+
let pending = r.registrationsByChainId->Utils.Dict.dangerouslyGetNonOption(key)
|
|
602
|
+
|
|
603
|
+
// We don't need the router itself, but only validation logic,
|
|
604
|
+
// since now event router is created for selection of events
|
|
605
|
+
// and validation doesn't work correctly in routers.
|
|
606
|
+
// Ideally to split it into two different parts.
|
|
607
|
+
let eventRouter = EventRouter.empty()
|
|
608
|
+
|
|
609
|
+
let onEventRegistrations: array<Internal.onEventRegistration> = []
|
|
610
|
+
|
|
611
|
+
chainConfig.contracts->Array.forEach(contract => {
|
|
612
|
+
let contractName = contract.name
|
|
613
|
+
|
|
614
|
+
contract.events->Array.forEach(
|
|
615
|
+
eventConfig => {
|
|
616
|
+
let eventName = eventConfig.name
|
|
617
|
+
let registration =
|
|
618
|
+
pending->Option.flatMap(
|
|
619
|
+
pending =>
|
|
620
|
+
pending.onEventRegistrations->Utils.Dict.dangerouslyGetNonOption(
|
|
621
|
+
getKey(~contractName, ~eventName),
|
|
622
|
+
),
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
// Should validate the events
|
|
626
|
+
eventRouter->EventRouter.addOrThrow(
|
|
627
|
+
eventConfig.id,
|
|
628
|
+
(),
|
|
629
|
+
~contractName,
|
|
630
|
+
~chain=ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id),
|
|
631
|
+
~eventName,
|
|
632
|
+
~isWildcard=switch registration {
|
|
633
|
+
| Some(registration) => registration.isWildcard
|
|
634
|
+
| None => isWildcard(~contractName, ~eventName)
|
|
635
|
+
},
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
let registration = switch registration {
|
|
639
|
+
| Some(_) as registration => registration
|
|
640
|
+
| None =>
|
|
641
|
+
// No entry in the incremental store, but the persistent dict
|
|
642
|
+
// may still hold a handler: handler modules are import-cached,
|
|
643
|
+
// so a repeated registration cycle in the same process (tests
|
|
644
|
+
// restarting the indexer) never re-runs the `indexer.onEvent`
|
|
645
|
+
// calls. Rebuild from the dict in that case. Events without a
|
|
646
|
+
// handler/contractRegister aren't fetched or dispatched
|
|
647
|
+
// (unless raw events are enabled).
|
|
648
|
+
if hasRegistration(~contractName, ~eventName) || config.enableRawEvents {
|
|
649
|
+
Some(
|
|
650
|
+
buildOnEventRegistration(
|
|
651
|
+
~config,
|
|
652
|
+
~chainId=chainConfig.id,
|
|
653
|
+
~eventConfig,
|
|
654
|
+
~startBlock=?contract.startBlock,
|
|
655
|
+
),
|
|
656
|
+
)
|
|
657
|
+
} else {
|
|
658
|
+
let eventNames = switch notRegisteredEventsByContract->Utils.Dict.dangerouslyGetNonOption(
|
|
659
|
+
contractName,
|
|
660
|
+
) {
|
|
661
|
+
| Some(set) => set
|
|
662
|
+
| None => {
|
|
663
|
+
let set = Utils.Set.make()
|
|
664
|
+
notRegisteredEventsByContract->Dict.set(contractName, set)
|
|
665
|
+
set
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
eventNames->Utils.Set.add(eventName)->ignore
|
|
669
|
+
None
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
switch registration {
|
|
674
|
+
| Some(registration) =>
|
|
675
|
+
// A `where` that resolved to no topic selections (`false` for
|
|
676
|
+
// this chain) drops the chain's registration entirely — the
|
|
677
|
+
// event should never be fetched here.
|
|
678
|
+
let isDroppedByWhere =
|
|
679
|
+
config.ecosystem.name === Evm &&
|
|
680
|
+
(registration->getResolvedWhere).topicSelections->Utils.Array.isEmpty
|
|
681
|
+
if !isDroppedByWhere {
|
|
682
|
+
onEventRegistrations->Array.push(registration)
|
|
683
|
+
}
|
|
684
|
+
| None => ()
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
)
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
registrationsByChainId->Dict.set(
|
|
691
|
+
key,
|
|
692
|
+
{
|
|
693
|
+
onEventRegistrations,
|
|
694
|
+
onBlockRegistrations: switch pending {
|
|
695
|
+
| Some(pending) => pending.onBlockRegistrations
|
|
696
|
+
| None => []
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
)
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
// Reported once for the whole indexer (a shared contract on multiple
|
|
703
|
+
// chains would otherwise repeat the same message per chain).
|
|
704
|
+
let notRegisteredEntries = notRegisteredEventsByContract->Dict.toArray
|
|
705
|
+
if notRegisteredEntries->Utils.Array.notEmpty {
|
|
706
|
+
let groups =
|
|
707
|
+
notRegisteredEntries
|
|
708
|
+
->Array.map(((contractName, eventNames)) =>
|
|
709
|
+
`${contractName} (${eventNames->Utils.Set.toArray->Array.joinUnsafe(", ")})`
|
|
710
|
+
)
|
|
711
|
+
->Array.joinUnsafe(", ")
|
|
712
|
+
Logging.getLogger()->Logging.childInfo(
|
|
713
|
+
`Events without a handler, skipped for indexing: ${groups}`,
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
registrationsByChainId
|
|
718
|
+
}
|
|
719
|
+
| None =>
|
|
720
|
+
JsError.throwWithMessage(
|
|
721
|
+
"The indexer has not started registering handlers, so can't finish it.",
|
|
722
|
+
)
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let isPendingRegistration = () => {
|
|
727
|
+
switch getActiveRegistration() {
|
|
728
|
+
| Some(r) => !r.finished
|
|
729
|
+
| None => false
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Early guard called from `indexer.onEvent` / `.contractRegister` / `.onBlock` /
|
|
734
|
+
// `.onSlot` so the user sees a method-specific error at the call site, instead
|
|
735
|
+
// of hitting the generic `withRegistration` throw deep inside `setHandler` etc.
|
|
736
|
+
let throwIfFinishedRegistration = (~methodName) => {
|
|
737
|
+
switch getActiveRegistration() {
|
|
738
|
+
| Some({finished: true}) =>
|
|
739
|
+
JsError.throwWithMessage(
|
|
740
|
+
`Cannot call \`indexer.${methodName}\` after the indexer has started. Make sure all handlers are registered at the top level of your handler module.`,
|
|
741
|
+
)
|
|
742
|
+
| _ => ()
|
|
743
|
+
}
|
|
744
|
+
}
|