envio 3.3.0-alpha.8 → 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/package.json +6 -6
- package/src/ChainState.res +104 -37
- package/src/ChainState.res.mjs +80 -19
- package/src/ChainState.resi +4 -1
- package/src/CrossChainState.res +56 -29
- package/src/CrossChainState.res.mjs +11 -2
- package/src/EnvioGlobal.res +53 -0
- package/src/EnvioGlobal.res.mjs +31 -0
- package/src/EventConfigBuilder.res +16 -17
- package/src/EventConfigBuilder.res.mjs +7 -6
- package/src/FetchState.res +151 -130
- package/src/FetchState.res.mjs +105 -88
- package/src/HandlerLoader.res +1 -1
- package/src/HandlerLoader.res.mjs +1 -1
- package/src/HandlerRegister.res +476 -305
- package/src/HandlerRegister.res.mjs +276 -209
- package/src/HandlerRegister.resi +11 -6
- package/src/Internal.res +23 -4
- package/src/LogSelection.res +102 -165
- package/src/LogSelection.res.mjs +101 -116
- package/src/Main.res +29 -143
- package/src/Main.res.mjs +25 -88
- package/src/RollbackCommit.res +4 -1
- package/src/RollbackCommit.res.mjs +3 -2
- package/src/SimulateItems.res +1 -1
- package/src/sources/Evm.res +4 -1
- package/src/sources/Evm.res.mjs +1 -1
- package/src/sources/Fuel.res +3 -1
- package/src/sources/Fuel.res.mjs +1 -1
- package/src/sources/HyperSyncSource.res +22 -33
- package/src/sources/HyperSyncSource.res.mjs +17 -26
- package/src/sources/RpcSource.res +28 -39
- package/src/sources/RpcSource.res.mjs +18 -31
- package/src/sources/SourceManager.res +1 -7
- package/src/sources/SourceManager.res.mjs +1 -2
package/src/Main.res
CHANGED
|
@@ -39,40 +39,24 @@ let stateSchema = S.union([
|
|
|
39
39
|
})),
|
|
40
40
|
])
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
_lte: option<int>,
|
|
50
|
-
_every: int,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// `S.strict` rejects unknown fields so typos like `_gt` / `_evry` surface
|
|
54
|
-
// with a readable schema error pointing at the offending key, instead of
|
|
55
|
-
// silently registering a broken filter. `_every` defaults to 1 inside the
|
|
56
|
-
// schema so the caller always sees a plain `int`, and `intMin(1)` rejects
|
|
57
|
-
// zero/negative strides — `(blockNumber - startBlock) % 0` would crash and
|
|
58
|
-
// any negative stride would never match.
|
|
59
|
-
let blockRangeSchema: S.t<blockRange> = S.object(s => {
|
|
60
|
-
_gte: s.field("_gte", S.option(S.int)),
|
|
61
|
-
_lte: s.field("_lte", S.option(S.int)),
|
|
62
|
-
_every: s.field("_every", S.option(S.int->S.intMin(1))->S.Option.getOr(1)),
|
|
63
|
-
})->S.strict
|
|
64
|
-
|
|
65
|
-
let defaultBlockRange: blockRange = {_gte: None, _lte: None, _every: 1}
|
|
66
|
-
|
|
67
|
-
let indexerStateRef: ref<option<IndexerState.t>> = ref(None)
|
|
42
|
+
// Runtime state lives in the process-wide `EnvioGlobal` record (shared
|
|
43
|
+
// across duplicate envio module instances); the slots are opaque there, so
|
|
44
|
+
// cast them to the real types here.
|
|
45
|
+
let getIndexerState = () =>
|
|
46
|
+
EnvioGlobal.value.indexerState->(Utils.magic: option<unknown> => option<IndexerState.t>)
|
|
47
|
+
let setIndexerState = (state: IndexerState.t) =>
|
|
48
|
+
EnvioGlobal.value.indexerState = Some(state->(Utils.magic: IndexerState.t => unknown))
|
|
68
49
|
|
|
69
50
|
// Persistence is set by Main.start before handler modules load, so that
|
|
70
51
|
// the exported indexer value can lazily expose DB state (startBlock,
|
|
71
52
|
// endBlock, isRealtime, dynamic contract addresses) once it's ready.
|
|
72
|
-
let
|
|
53
|
+
let getGlobalPersistence = () =>
|
|
54
|
+
EnvioGlobal.value.persistence->(Utils.magic: option<unknown> => option<Persistence.t>)
|
|
55
|
+
let setGlobalPersistence = (persistence: Persistence.t) =>
|
|
56
|
+
EnvioGlobal.value.persistence = Some(persistence->(Utils.magic: Persistence.t => unknown))
|
|
73
57
|
|
|
74
58
|
let getInitialChainState = (~chainId: int): option<Persistence.initialChainState> => {
|
|
75
|
-
switch
|
|
59
|
+
switch getGlobalPersistence() {
|
|
76
60
|
| Some(persistence) =>
|
|
77
61
|
switch persistence.storageStatus {
|
|
78
62
|
| Ready(initialState) => initialState.chains->Array.find(c => c.id === chainId)
|
|
@@ -130,7 +114,7 @@ let buildChainsObject = (~config: Config.t) => {
|
|
|
130
114
|
{
|
|
131
115
|
enumerable: true,
|
|
132
116
|
get: () => {
|
|
133
|
-
switch
|
|
117
|
+
switch getIndexerState() {
|
|
134
118
|
| Some(state) => state->IndexerState.isRealtime
|
|
135
119
|
// Before the global state is available (eg during handler
|
|
136
120
|
// module load after resume), derive from persistence: every chain
|
|
@@ -164,7 +148,7 @@ let buildChainsObject = (~config: Config.t) => {
|
|
|
164
148
|
{
|
|
165
149
|
enumerable: true,
|
|
166
150
|
get: () => {
|
|
167
|
-
switch
|
|
151
|
+
switch getIndexerState() {
|
|
168
152
|
| Some(state) => {
|
|
169
153
|
let chain = ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id)
|
|
170
154
|
let chainState = state->IndexerState.getChainState(~chain)
|
|
@@ -345,121 +329,24 @@ let getGlobalIndexer = (): 'indexer => {
|
|
|
345
329
|
let _ = RollbackCommit.register(callback->(Utils.magic: 'a => RollbackCommit.callback))
|
|
346
330
|
}
|
|
347
331
|
|
|
348
|
-
// Two-stage parse: first the ecosystem-specific outer schema unwraps the
|
|
349
|
-
// wrapper (`block.number` / `block.height` / `slot`) and surfaces the
|
|
350
|
-
// inner chunk as raw `unknown`; then the shared `blockRangeSchema`
|
|
351
|
-
// validates the `{_gte?, _lte?, _every?}` fields. Keeping the inner
|
|
352
|
-
// validation in one place means typos and shape mismatches surface with
|
|
353
|
-
// the same user-friendly error regardless of ecosystem.
|
|
354
|
-
let extractRange = (filter: unknown, ~name, ~ecosystem: Ecosystem.t): blockRange =>
|
|
355
|
-
try {
|
|
356
|
-
switch filter->S.parseOrThrow(ecosystem.onBlockFilterSchema) {
|
|
357
|
-
| None => defaultBlockRange
|
|
358
|
-
| Some(inner) => inner->S.parseOrThrow(blockRangeSchema)
|
|
359
|
-
}
|
|
360
|
-
} catch {
|
|
361
|
-
| S.Raised(exn) =>
|
|
362
|
-
JsError.throwWithMessage(
|
|
363
|
-
`\`indexer.${ecosystem.onBlockMethodName}("${name}")\` \`where\` returned an invalid filter: ${exn
|
|
364
|
-
->Utils.prettifyExn
|
|
365
|
-
->(Utils.magic: exn => string)}`,
|
|
366
|
-
)
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// `where` is evaluated once per configured chain at registration time.
|
|
370
|
-
// Decoded ranges/stride feed directly into `HandlerRegister.registerOnBlock`
|
|
371
|
-
// so the fetcher's `(blockNumber - handlerStartBlock) % interval === 0`
|
|
372
|
-
// math at `FetchState.res:619` stays untouched.
|
|
373
332
|
let onBlockFn = (rawOptions: 'a, handler: 'b) => {
|
|
374
333
|
HandlerRegister.throwIfFinishedRegistration(~methodName="onBlock")
|
|
375
|
-
let config = Config.load()
|
|
376
|
-
let ecosystem = config.ecosystem
|
|
377
334
|
let raw =
|
|
378
335
|
rawOptions->(
|
|
379
336
|
Utils.magic: 'a => {
|
|
380
337
|
"name": string,
|
|
381
|
-
"where":
|
|
338
|
+
"where": unknown,
|
|
382
339
|
}
|
|
383
340
|
)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
// Normalize undefined/null to None up front so the per-chain loop below
|
|
394
|
-
// can't accidentally call `null` as a predicate (ReScript treats a JS
|
|
395
|
-
// `null` value as `Some(null)` when the field is typed as option).
|
|
396
|
-
let where = switch raw["where"]->(Utils.magic: option<'a> => unknown) {
|
|
397
|
-
| w if w === %raw(`undefined`) || w === %raw(`null`) => None
|
|
398
|
-
| w if typeof(w) === #function => Some(raw["where"]->Option.getUnsafe)
|
|
399
|
-
| w =>
|
|
400
|
-
JsError.throwWithMessage(
|
|
401
|
-
`\`indexer.${ecosystem.onBlockMethodName}("${name}")\` expected \`where\` to be a function or omitted, but got ${(typeof(
|
|
402
|
-
w,
|
|
403
|
-
) :> string)}.`,
|
|
404
|
-
)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
let matchedAny = ref(false)
|
|
408
|
-
|
|
409
|
-
config.chainMap
|
|
410
|
-
->ChainMap.values
|
|
411
|
-
->Array.forEach(chainConfig => {
|
|
412
|
-
let chainId = chainConfig.id
|
|
413
|
-
let chainObj = chainsDict->Dict.getUnsafe(chainId->Int.toString)
|
|
414
|
-
|
|
415
|
-
// Predicate returns `true` → match with no filter; `false` → skip;
|
|
416
|
-
// any plain object → structured filter. `undefined`/`null` returns
|
|
417
|
-
// are rejected — the TS type excludes `void`, so a missing return is
|
|
418
|
-
// a user bug we surface early rather than silently match-all.
|
|
419
|
-
let result = switch where {
|
|
420
|
-
| None => %raw(`true`)
|
|
421
|
-
| Some(predicate) => predicate({chain: chainObj})
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
let (shouldRegister, range) = if result === %raw(`true`) {
|
|
425
|
-
(true, defaultBlockRange)
|
|
426
|
-
} else if result === %raw(`false`) {
|
|
427
|
-
(false, defaultBlockRange)
|
|
428
|
-
} else if typeof(result) === #object && !(result->Array.isArray) && result !== %raw(`null`) {
|
|
429
|
-
(true, extractRange(result, ~name, ~ecosystem))
|
|
430
|
-
} else {
|
|
431
|
-
// Reject numbers, strings, functions, arrays, undefined, null —
|
|
432
|
-
// anything that isn't bool or a plain object would silently
|
|
433
|
-
// misregister.
|
|
434
|
-
JsError.throwWithMessage(
|
|
435
|
-
`\`indexer.${ecosystem.onBlockMethodName}("${name}")\` \`where\` predicate returned an invalid value of type ${(typeof(
|
|
436
|
-
result,
|
|
437
|
-
) :> string)}. Expected boolean or a filter object.`,
|
|
438
|
-
)
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if shouldRegister {
|
|
442
|
-
matchedAny := true
|
|
443
|
-
HandlerRegister.registerOnBlock(
|
|
444
|
-
~name,
|
|
445
|
-
~chainId,
|
|
446
|
-
~interval=range._every,
|
|
447
|
-
~startBlock=range._gte,
|
|
448
|
-
~endBlock=range._lte,
|
|
449
|
-
~handler=typedHandler,
|
|
450
|
-
)
|
|
451
|
-
}
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
// Catches misconfigured `where` predicates that return `false` for every
|
|
455
|
-
// configured chain — the handler would otherwise never fire with no hint.
|
|
456
|
-
// Includes the ecosystem-specific method name so SVM users see "onSlot"
|
|
457
|
-
// and don't get confused looking for a "Block handler" they never wrote.
|
|
458
|
-
if !matchedAny.contents {
|
|
459
|
-
logger->Logging.childWarn(
|
|
460
|
-
`\`indexer.${ecosystem.onBlockMethodName}\` matched 0 chains. Check the \`where\` predicate.`,
|
|
461
|
-
)
|
|
462
|
-
}
|
|
341
|
+
HandlerRegister.registerOnBlock(
|
|
342
|
+
~name=raw["name"],
|
|
343
|
+
~where=raw["where"],
|
|
344
|
+
~handler=handler->(Utils.magic: 'b => Internal.onBlockArgs => promise<unit>),
|
|
345
|
+
~getChainsObject=config => {
|
|
346
|
+
let (chains, _) = buildChainsObject(~config)
|
|
347
|
+
chains->(Utils.magic: {..} => dict<unknown>)
|
|
348
|
+
},
|
|
349
|
+
)
|
|
463
350
|
}
|
|
464
351
|
|
|
465
352
|
// Ecosystem-specific surface: EVM/Fuel expose event + block handlers; SVM
|
|
@@ -506,8 +393,7 @@ let getGlobalIndexer = (): 'indexer => {
|
|
|
506
393
|
let get = (~prop: string) =>
|
|
507
394
|
switch prop {
|
|
508
395
|
| "name" => Config.load().name->(Utils.magic: string => unknown)
|
|
509
|
-
| "description" =>
|
|
510
|
-
Config.load().description->(Utils.magic: option<string> => unknown)
|
|
396
|
+
| "description" => Config.load().description->(Utils.magic: option<string> => unknown)
|
|
511
397
|
| "chainIds" => {
|
|
512
398
|
let (_, chainIds) = buildChainsObject(~config=Config.load())
|
|
513
399
|
chainIds->(Utils.magic: array<int> => unknown)
|
|
@@ -616,7 +502,7 @@ let startServer = (~getState, ~persistence: Persistence.t, ~isDevelopmentMode: b
|
|
|
616
502
|
app->get("/metrics", (_req, res) => {
|
|
617
503
|
res->set("Content-Type", PromClient.defaultRegister->PromClient.getContentType)
|
|
618
504
|
let _ =
|
|
619
|
-
Metrics.collect(~state=
|
|
505
|
+
Metrics.collect(~state=getIndexerState())->Promise.thenResolve(metrics =>
|
|
620
506
|
res->endWithData(metrics)
|
|
621
507
|
)
|
|
622
508
|
})
|
|
@@ -709,7 +595,7 @@ let start = async (
|
|
|
709
595
|
| Some(p) => p
|
|
710
596
|
| None => PgStorage.makePersistenceFromConfig(~config)
|
|
711
597
|
}
|
|
712
|
-
|
|
598
|
+
setGlobalPersistence(persistence)
|
|
713
599
|
await persistence->Persistence.init(
|
|
714
600
|
~reset,
|
|
715
601
|
~chainConfigs=config.chainMap->ChainMap.values,
|
|
@@ -756,7 +642,7 @@ let start = async (
|
|
|
756
642
|
|
|
757
643
|
if !isTest {
|
|
758
644
|
startServer(~persistence, ~isDevelopmentMode, ~getState=() =>
|
|
759
|
-
switch
|
|
645
|
+
switch getIndexerState() {
|
|
760
646
|
| None => Initializing({})
|
|
761
647
|
| Some(state) => {
|
|
762
648
|
let chains =
|
|
@@ -786,7 +672,7 @@ let start = async (
|
|
|
786
672
|
if shouldUseTui {
|
|
787
673
|
let _rerender = Tui.start(~getState=() => state)
|
|
788
674
|
}
|
|
789
|
-
|
|
675
|
+
setIndexerState(state)
|
|
790
676
|
state->IndexerLoop.start
|
|
791
677
|
await runUntilFatalError
|
|
792
678
|
}
|
package/src/Main.res.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import * as ChainMap from "./ChainMap.res.mjs";
|
|
|
13
13
|
import * as PgStorage from "./PgStorage.res.mjs";
|
|
14
14
|
import * as ChainState from "./ChainState.res.mjs";
|
|
15
15
|
import * as Prometheus from "./Prometheus.res.mjs";
|
|
16
|
+
import * as EnvioGlobal from "./EnvioGlobal.res.mjs";
|
|
16
17
|
import * as IndexerLoop from "./IndexerLoop.res.mjs";
|
|
17
18
|
import * as Persistence from "./Persistence.res.mjs";
|
|
18
19
|
import * as PromClient from "prom-client";
|
|
@@ -61,28 +62,24 @@ let stateSchema = S$RescriptSchema.union([
|
|
|
61
62
|
}))
|
|
62
63
|
]);
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
_every: s.f("_every", S$RescriptSchema.Option.getOr(S$RescriptSchema.option(S$RescriptSchema.intMin(S$RescriptSchema.int, 1, undefined)), 1))
|
|
68
|
-
})));
|
|
65
|
+
function getIndexerState() {
|
|
66
|
+
return EnvioGlobal.value.indexerState;
|
|
67
|
+
}
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
_every: 1
|
|
74
|
-
};
|
|
69
|
+
function setIndexerState(state) {
|
|
70
|
+
EnvioGlobal.value.indexerState = Primitive_option.some(state);
|
|
71
|
+
}
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
73
|
+
function getGlobalPersistence() {
|
|
74
|
+
return EnvioGlobal.value.persistence;
|
|
75
|
+
}
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
77
|
+
function setGlobalPersistence(persistence) {
|
|
78
|
+
EnvioGlobal.value.persistence = Primitive_option.some(persistence);
|
|
79
|
+
}
|
|
83
80
|
|
|
84
81
|
function getInitialChainState(chainId) {
|
|
85
|
-
let persistence =
|
|
82
|
+
let persistence = EnvioGlobal.value.persistence;
|
|
86
83
|
if (persistence === undefined) {
|
|
87
84
|
return;
|
|
88
85
|
}
|
|
@@ -134,7 +131,7 @@ function buildChainsObject(config) {
|
|
|
134
131
|
}), "isRealtime", {
|
|
135
132
|
enumerable: true,
|
|
136
133
|
get: () => {
|
|
137
|
-
let state =
|
|
134
|
+
let state = EnvioGlobal.value.indexerState;
|
|
138
135
|
if (state !== undefined) {
|
|
139
136
|
return IndexerState.isRealtime(Primitive_option.valFromOption(state));
|
|
140
137
|
} else if (Env.updateSyncTimeOnRestart) {
|
|
@@ -162,7 +159,7 @@ function buildChainsObject(config) {
|
|
|
162
159
|
}), "addresses", {
|
|
163
160
|
enumerable: true,
|
|
164
161
|
get: () => {
|
|
165
|
-
let state =
|
|
162
|
+
let state = EnvioGlobal.value.indexerState;
|
|
166
163
|
if (state !== undefined) {
|
|
167
164
|
let chain = ChainMap.Chain.makeUnsafe(chainConfig.id);
|
|
168
165
|
let chainState = IndexerState.getChainState(Primitive_option.valFromOption(state), chain);
|
|
@@ -278,69 +275,9 @@ function getGlobalIndexer() {
|
|
|
278
275
|
HandlerRegister.throwIfFinishedRegistration("~internalAndWillBeRemovedSoon_onRollbackCommit");
|
|
279
276
|
RollbackCommit.register(callback);
|
|
280
277
|
};
|
|
281
|
-
let extractRange = (filter, name, ecosystem) => {
|
|
282
|
-
try {
|
|
283
|
-
let inner = S$RescriptSchema.parseOrThrow(filter, ecosystem.onBlockFilterSchema);
|
|
284
|
-
if (inner !== undefined) {
|
|
285
|
-
return S$RescriptSchema.parseOrThrow(Primitive_option.valFromOption(inner), blockRangeSchema);
|
|
286
|
-
} else {
|
|
287
|
-
return defaultBlockRange;
|
|
288
|
-
}
|
|
289
|
-
} catch (raw_exn) {
|
|
290
|
-
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
291
|
-
if (exn.RE_EXN_ID === S$RescriptSchema.Raised) {
|
|
292
|
-
return Stdlib_JsError.throwWithMessage(`\`indexer.` + ecosystem.onBlockMethodName + `("` + name + `")\` \`where\` returned an invalid filter: ` + Utils.prettifyExn(exn._1));
|
|
293
|
-
}
|
|
294
|
-
throw exn;
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
278
|
let onBlockFn = (rawOptions, handler) => {
|
|
298
279
|
HandlerRegister.throwIfFinishedRegistration("onBlock");
|
|
299
|
-
|
|
300
|
-
let ecosystem = config.ecosystem;
|
|
301
|
-
let match = buildChainsObject(config);
|
|
302
|
-
let chains = match[0];
|
|
303
|
-
let name = rawOptions.name;
|
|
304
|
-
let logger = Logging.createChild({
|
|
305
|
-
onBlock: name
|
|
306
|
-
});
|
|
307
|
-
let w = rawOptions.where;
|
|
308
|
-
let where = w === undefined || w === null ? undefined : (
|
|
309
|
-
typeof w === "function" ? rawOptions.where : Stdlib_JsError.throwWithMessage(`\`indexer.` + ecosystem.onBlockMethodName + `("` + name + `")\` expected \`where\` to be a function or omitted, but got ` + typeof w + `.`)
|
|
310
|
-
);
|
|
311
|
-
let matchedAny = {
|
|
312
|
-
contents: false
|
|
313
|
-
};
|
|
314
|
-
ChainMap.values(config.chainMap).forEach(chainConfig => {
|
|
315
|
-
let chainId = chainConfig.id;
|
|
316
|
-
let chainObj = chains[chainId.toString()];
|
|
317
|
-
let result = where !== undefined ? where({
|
|
318
|
-
chain: chainObj
|
|
319
|
-
}) : true;
|
|
320
|
-
let match = result === true ? [
|
|
321
|
-
true,
|
|
322
|
-
defaultBlockRange
|
|
323
|
-
] : (
|
|
324
|
-
result === false ? [
|
|
325
|
-
false,
|
|
326
|
-
defaultBlockRange
|
|
327
|
-
] : (
|
|
328
|
-
typeof result === "object" && !Array.isArray(result) && result !== null ? [
|
|
329
|
-
true,
|
|
330
|
-
extractRange(result, name, ecosystem)
|
|
331
|
-
] : Stdlib_JsError.throwWithMessage(`\`indexer.` + ecosystem.onBlockMethodName + `("` + name + `")\` \`where\` predicate returned an invalid value of type ` + typeof result + `. Expected boolean or a filter object.`)
|
|
332
|
-
)
|
|
333
|
-
);
|
|
334
|
-
if (!match[0]) {
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
let range = match[1];
|
|
338
|
-
matchedAny.contents = true;
|
|
339
|
-
HandlerRegister.registerOnBlock(name, chainId, range._every, range._gte, range._lte, handler);
|
|
340
|
-
});
|
|
341
|
-
if (!matchedAny.contents) {
|
|
342
|
-
return Logging.childWarn(logger, `\`indexer.` + ecosystem.onBlockMethodName + `\` matched 0 chains. Check the \`where\` predicate.`);
|
|
343
|
-
}
|
|
280
|
+
HandlerRegister.registerOnBlock(rawOptions.name, rawOptions.where, handler, config => buildChainsObject(config)[0]);
|
|
344
281
|
};
|
|
345
282
|
let keysMemo = {
|
|
346
283
|
contents: undefined
|
|
@@ -478,7 +415,7 @@ function startServer(getState, persistence, isDevelopmentMode) {
|
|
|
478
415
|
});
|
|
479
416
|
app.get("/metrics", (_req, res) => {
|
|
480
417
|
res.set("Content-Type", PromClient.register.contentType);
|
|
481
|
-
Metrics.collect(
|
|
418
|
+
Metrics.collect(EnvioGlobal.value.indexerState).then(metrics => res.end(metrics));
|
|
482
419
|
});
|
|
483
420
|
app.get("/metrics/runtime", (_req, res) => {
|
|
484
421
|
res.set("Content-Type", runtimeRegistry.contentType);
|
|
@@ -529,7 +466,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
|
|
|
529
466
|
let config = Config.load();
|
|
530
467
|
let isDevelopmentMode = !isTest && config.isDev;
|
|
531
468
|
let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(config, undefined);
|
|
532
|
-
|
|
469
|
+
EnvioGlobal.value.persistence = Primitive_option.some(persistence$1);
|
|
533
470
|
await Persistence.init(persistence$1, ChainMap.values(config.chainMap), Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset);
|
|
534
471
|
let registrationsByChainId = await HandlerLoader.registerAllHandlers(config);
|
|
535
472
|
let config$1 = isTest ? ({
|
|
@@ -573,7 +510,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
|
|
|
573
510
|
Prometheus.RollbackEnabled.set(config$2.shouldRollbackOnReorg);
|
|
574
511
|
if (!isTest) {
|
|
575
512
|
startServer(() => {
|
|
576
|
-
let state =
|
|
513
|
+
let state = EnvioGlobal.value.indexerState;
|
|
577
514
|
if (state === undefined) {
|
|
578
515
|
return {
|
|
579
516
|
status: "initializing"
|
|
@@ -595,7 +532,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
|
|
|
595
532
|
if (shouldUseTui) {
|
|
596
533
|
Tui.start(() => state);
|
|
597
534
|
}
|
|
598
|
-
|
|
535
|
+
EnvioGlobal.value.indexerState = Primitive_option.some(state);
|
|
599
536
|
IndexerLoop.start(state);
|
|
600
537
|
return await runUntilFatalError;
|
|
601
538
|
}
|
|
@@ -603,10 +540,10 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
|
|
|
603
540
|
export {
|
|
604
541
|
chainDataSchema,
|
|
605
542
|
stateSchema,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
543
|
+
getIndexerState,
|
|
544
|
+
setIndexerState,
|
|
545
|
+
getGlobalPersistence,
|
|
546
|
+
setGlobalPersistence,
|
|
610
547
|
getInitialChainState,
|
|
611
548
|
buildChainsObject,
|
|
612
549
|
getGlobalIndexer,
|
package/src/RollbackCommit.res
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
type args = {chainId: int, rollbackToBlock: int}
|
|
7
7
|
type callback = args => promise<unit>
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// Lives in the process-wide `EnvioGlobal` record so callbacks registered
|
|
10
|
+
// through a duplicate envio module instance still fire.
|
|
11
|
+
let callbacks =
|
|
12
|
+
EnvioGlobal.value.rollbackCommitCallbacks->(Utils.magic: array<unknown> => array<callback>)
|
|
10
13
|
|
|
11
14
|
let register = (callback: callback) => {
|
|
12
15
|
callbacks->Array.push(callback)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
4
|
+
import * as EnvioGlobal from "./EnvioGlobal.res.mjs";
|
|
4
5
|
|
|
5
|
-
let callbacks =
|
|
6
|
+
let callbacks = EnvioGlobal.value.rollbackCommitCallbacks;
|
|
6
7
|
|
|
7
8
|
function register(callback) {
|
|
8
9
|
callbacks.push(callback);
|
|
@@ -32,4 +33,4 @@ export {
|
|
|
32
33
|
register,
|
|
33
34
|
fire,
|
|
34
35
|
}
|
|
35
|
-
/*
|
|
36
|
+
/* EnvioGlobal Not a pure module */
|
package/src/SimulateItems.res
CHANGED
|
@@ -336,7 +336,7 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
|
|
|
336
336
|
| None => seenCoordinates->Dict.set(coordinate, itemIndex)
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
// Build a real registration the same way `HandlerRegister.
|
|
339
|
+
// Build a real registration the same way `HandlerRegister.finishRegistration`
|
|
340
340
|
// does at startup (not a stub), so the address filter and `where`
|
|
341
341
|
// behave identically to real indexing — the dead-input tracker relies
|
|
342
342
|
// on `clientAddressFilter` actually gating unrouted items.
|
package/src/sources/Evm.res
CHANGED
|
@@ -90,8 +90,11 @@ let make = (~logger: Pino.t): Ecosystem.t => {
|
|
|
90
90
|
// range chunk is validated by `eventBlockRangeSchema` in
|
|
91
91
|
// `LogSelection.res` which rejects `_lte`/`_every` (use `onBlock` for
|
|
92
92
|
// stride- and endBlock-based block handlers).
|
|
93
|
+
// `S.strict` on the inner object rejects unknown `block` fields (e.g. a
|
|
94
|
+
// `numbre` typo or `block: {timestamp: ...}`) instead of silently
|
|
95
|
+
// ignoring them.
|
|
93
96
|
onEventBlockFilterSchema: S.object(s =>
|
|
94
|
-
s.field("block", S.option(S.object(s2 => s2.field("number", S.unknown))))
|
|
97
|
+
s.field("block", S.option(S.object(s2 => s2.field("number", S.unknown))->S.strict))
|
|
95
98
|
),
|
|
96
99
|
logger,
|
|
97
100
|
// The payload carries `transaction` by batch prep (HyperSync) or inline
|
package/src/sources/Evm.res.mjs
CHANGED
|
@@ -58,7 +58,7 @@ function make(logger) {
|
|
|
58
58
|
cleanUpRawEventFieldsInPlace: cleanUpRawEventFieldsInPlace,
|
|
59
59
|
onBlockMethodName: "onBlock",
|
|
60
60
|
onBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown))))),
|
|
61
|
-
onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown))))),
|
|
61
|
+
onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.strict(S$RescriptSchema.object(s2 => s2.f("number", S$RescriptSchema.unknown)))))),
|
|
62
62
|
logger: logger,
|
|
63
63
|
toEvent: eventItem => eventItem.payload,
|
|
64
64
|
toEventLogger: eventItem => Logging.createChildFrom(logger, {
|
package/src/sources/Fuel.res
CHANGED
|
@@ -35,8 +35,10 @@ let make = (~logger: Pino.t): Ecosystem.t => {
|
|
|
35
35
|
// Analogous to EVM, but keyed by `block.height` instead of
|
|
36
36
|
// `block.number`. See `Evm.res` for the rationale on the two-stage
|
|
37
37
|
// parse and the `_lte`/`_every` rejection.
|
|
38
|
+
// `S.strict` on the inner object rejects unknown `block` fields — including
|
|
39
|
+
// `block.number`, which is EVM-only; Fuel filters by `block.height`.
|
|
38
40
|
onEventBlockFilterSchema: S.object(s =>
|
|
39
|
-
s.field("block", S.option(S.object(s2 => s2.field("height", S.unknown))))
|
|
41
|
+
s.field("block", S.option(S.object(s2 => s2.field("height", S.unknown))->S.strict))
|
|
40
42
|
),
|
|
41
43
|
logger,
|
|
42
44
|
toEvent: eventItem => eventItem.payload->(Utils.magic: Internal.eventPayload => Internal.event),
|
package/src/sources/Fuel.res.mjs
CHANGED
|
@@ -19,7 +19,7 @@ function make(logger) {
|
|
|
19
19
|
cleanUpRawEventFieldsInPlace: cleanUpRawEventFieldsInPlace,
|
|
20
20
|
onBlockMethodName: "onBlock",
|
|
21
21
|
onBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("height", S$RescriptSchema.unknown))))),
|
|
22
|
-
onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.object(s2 => s2.f("height", S$RescriptSchema.unknown))))),
|
|
22
|
+
onEventBlockFilterSchema: S$RescriptSchema.object(s => s.f("block", S$RescriptSchema.option(S$RescriptSchema.strict(S$RescriptSchema.object(s2 => s2.f("height", S$RescriptSchema.unknown)))))),
|
|
23
23
|
logger: logger,
|
|
24
24
|
toEvent: eventItem => eventItem.payload,
|
|
25
25
|
toEventLogger: eventItem => Logging.createChildFrom(logger, {
|
|
@@ -7,13 +7,12 @@ type selectionConfig = {
|
|
|
7
7
|
fieldSelection: HyperSyncClient.QueryTypes.fieldSelection,
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
let getSelectionConfig = (selection: FetchState.selection
|
|
10
|
+
let getSelectionConfig = (selection: FetchState.selection) => {
|
|
11
11
|
let capitalizedBlockFields = Utils.Set.make()
|
|
12
12
|
let capitalizedTransactionFields = Utils.Set.make()
|
|
13
13
|
|
|
14
|
-
let
|
|
15
|
-
let
|
|
16
|
-
let dynamicWildcardEventFiltersByContract = Dict.make()
|
|
14
|
+
let topicSelectionsByContract = Dict.make()
|
|
15
|
+
let wildcardTopicSelectionsByContract = Dict.make()
|
|
17
16
|
let noAddressesTopicSelections = []
|
|
18
17
|
let contractNames = Utils.Set.make()
|
|
19
18
|
|
|
@@ -24,7 +23,7 @@ let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
|
|
|
24
23
|
reg.eventConfig->(Utils.magic: Internal.eventConfig => Internal.evmEventConfig)
|
|
25
24
|
let contractName = eventConfig.contractName
|
|
26
25
|
let {selectedBlockFields, selectedTransactionFields} = eventConfig
|
|
27
|
-
let {dependsOnAddresses,
|
|
26
|
+
let {dependsOnAddresses, resolvedWhere, isWildcard} = reg
|
|
28
27
|
selectedBlockFields
|
|
29
28
|
->Utils.Set.toArray
|
|
30
29
|
->Array.forEach(name =>
|
|
@@ -44,24 +43,16 @@ let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
|
|
|
44
43
|
}
|
|
45
44
|
})
|
|
46
45
|
|
|
47
|
-
let eventFilters = getEventFiltersOrThrow(chain)
|
|
48
46
|
if dependsOnAddresses {
|
|
49
47
|
let _ = contractNames->Utils.Set.add(contractName)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
(
|
|
55
|
-
isWildcard ? dynamicWildcardEventFiltersByContract : dynamicEventFiltersByContract
|
|
56
|
-
)->Utils.Dict.push(contractName, fn)
|
|
57
|
-
}
|
|
48
|
+
|
|
49
|
+
(
|
|
50
|
+
isWildcard ? wildcardTopicSelectionsByContract : topicSelectionsByContract
|
|
51
|
+
)->Utils.Dict.pushMany(contractName, resolvedWhere.topicSelections)
|
|
58
52
|
} else {
|
|
59
53
|
noAddressesTopicSelections
|
|
60
54
|
->Array.pushMany(
|
|
61
|
-
|
|
62
|
-
| Static(s) => s
|
|
63
|
-
| Dynamic(fn) => fn([])
|
|
64
|
-
},
|
|
55
|
+
resolvedWhere.topicSelections->LogSelection.materializeTopicSelections(~addresses=[]),
|
|
65
56
|
)
|
|
66
57
|
->ignore
|
|
67
58
|
}
|
|
@@ -92,27 +83,26 @@ let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
|
|
|
92
83
|
| None
|
|
93
84
|
| Some([]) => ()
|
|
94
85
|
| Some(addresses) =>
|
|
95
|
-
switch
|
|
86
|
+
switch topicSelectionsByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
|
|
96
87
|
| None => ()
|
|
97
88
|
| Some(topicSelections) =>
|
|
98
|
-
logSelections->Array.push(LogSelection.make(~addresses, ~topicSelections))
|
|
99
|
-
}
|
|
100
|
-
switch dynamicEventFiltersByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
|
|
101
|
-
| None => ()
|
|
102
|
-
| Some(fns) =>
|
|
103
89
|
logSelections->Array.push(
|
|
104
|
-
LogSelection.make(
|
|
90
|
+
LogSelection.make(
|
|
91
|
+
~addresses,
|
|
92
|
+
~topicSelections=topicSelections->LogSelection.materializeTopicSelections(~addresses),
|
|
93
|
+
),
|
|
105
94
|
)
|
|
106
95
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
96
|
+
// Wildcard events that filter an indexed param by registered addresses:
|
|
97
|
+
// the addresses fold into the topics, so the query itself stays
|
|
98
|
+
// address-unbound.
|
|
99
|
+
switch wildcardTopicSelectionsByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
|
|
110
100
|
| None => ()
|
|
111
|
-
| Some(
|
|
101
|
+
| Some(topicSelections) =>
|
|
112
102
|
logSelections->Array.push(
|
|
113
103
|
LogSelection.make(
|
|
114
104
|
~addresses=[],
|
|
115
|
-
~topicSelections=
|
|
105
|
+
~topicSelections=topicSelections->LogSelection.materializeTopicSelections(~addresses),
|
|
116
106
|
),
|
|
117
107
|
)
|
|
118
108
|
}
|
|
@@ -127,8 +117,7 @@ let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
|
|
|
127
117
|
}
|
|
128
118
|
}
|
|
129
119
|
|
|
130
|
-
let memoGetSelectionConfig = (
|
|
131
|
-
Utils.WeakMap.memoize(selection => selection->getSelectionConfig(~chain))
|
|
120
|
+
let memoGetSelectionConfig = () => Utils.WeakMap.memoize(getSelectionConfig)
|
|
132
121
|
|
|
133
122
|
// Surfaced by HyperSyncClient.getHeight (Rust) when HyperSync rejects the API
|
|
134
123
|
// token. The corrupted-token test feeds the real server error through this
|
|
@@ -164,7 +153,7 @@ let make = (
|
|
|
164
153
|
): t => {
|
|
165
154
|
let name = "HyperSync"
|
|
166
155
|
|
|
167
|
-
let getSelectionConfig = memoGetSelectionConfig(
|
|
156
|
+
let getSelectionConfig = memoGetSelectionConfig()
|
|
168
157
|
|
|
169
158
|
let apiToken = switch apiToken {
|
|
170
159
|
| Some(token) => token
|