envio 3.3.0-alpha.5 → 3.3.0-alpha.6
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/BatchProcessing.res +5 -0
- package/src/BatchProcessing.res.mjs +6 -0
- package/src/ExitOnCaughtUp.res +10 -2
- package/src/ExitOnCaughtUp.res.mjs +10 -1
- package/src/IndexerState.res +4 -0
- package/src/IndexerState.res.mjs +8 -1
- package/src/IndexerState.resi +1 -0
- package/src/SimulateDeadInputTracker.res +85 -0
- package/src/SimulateDeadInputTracker.res.mjs +73 -0
- package/src/SimulateDeadInputTracker.resi +12 -0
- package/src/SimulateItems.res +23 -42
- package/src/SimulateItems.res.mjs +12 -30
- package/src/TestIndexer.res +3 -27
- package/src/TestIndexer.res.mjs +2 -9
- package/src/sources/EvmChain.res +1 -1
- package/src/sources/EvmChain.res.mjs +1 -1
- package/src/sources/RpcSource.res +274 -146
- package/src/sources/RpcSource.res.mjs +285 -246
- package/src/sources/SimulateSource.res +1 -0
- package/src/sources/SimulateSource.res.mjs +2 -1
- package/src/sources/Source.res +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envio",
|
|
3
|
-
"version": "3.3.0-alpha.
|
|
3
|
+
"version": "3.3.0-alpha.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
|
|
6
6
|
"bin": "./bin.mjs",
|
|
@@ -69,10 +69,10 @@
|
|
|
69
69
|
"tsx": "4.21.0"
|
|
70
70
|
},
|
|
71
71
|
"optionalDependencies": {
|
|
72
|
-
"envio-linux-x64": "3.3.0-alpha.
|
|
73
|
-
"envio-linux-x64-musl": "3.3.0-alpha.
|
|
74
|
-
"envio-linux-arm64": "3.3.0-alpha.
|
|
75
|
-
"envio-darwin-x64": "3.3.0-alpha.
|
|
76
|
-
"envio-darwin-arm64": "3.3.0-alpha.
|
|
72
|
+
"envio-linux-x64": "3.3.0-alpha.6",
|
|
73
|
+
"envio-linux-x64-musl": "3.3.0-alpha.6",
|
|
74
|
+
"envio-linux-arm64": "3.3.0-alpha.6",
|
|
75
|
+
"envio-darwin-x64": "3.3.0-alpha.6",
|
|
76
|
+
"envio-darwin-arm64": "3.3.0-alpha.6"
|
|
77
77
|
}
|
|
78
78
|
}
|
package/src/BatchProcessing.res
CHANGED
|
@@ -127,6 +127,11 @@ and processNextBatch = async (state: IndexerState.t, ~scheduleFetch): unit => {
|
|
|
127
127
|
| Ok() =>
|
|
128
128
|
state->IndexerState.recordProcessedBatch
|
|
129
129
|
|
|
130
|
+
switch state->IndexerState.simulateDeadInputTracker {
|
|
131
|
+
| Some(tracker) => tracker->SimulateDeadInputTracker.recordProcessed(~batch)
|
|
132
|
+
| None => ()
|
|
133
|
+
}
|
|
134
|
+
|
|
130
135
|
if state->IndexerState.isResolvingReorg {
|
|
131
136
|
// A reorg landed while this batch was processing. Apply its progress so
|
|
132
137
|
// the rollback diff is computed against up-to-date chain progress, but
|
|
@@ -10,8 +10,10 @@ import * as ErrorHandling from "./ErrorHandling.res.mjs";
|
|
|
10
10
|
import * as InMemoryStore from "./InMemoryStore.res.mjs";
|
|
11
11
|
import * as ExitOnCaughtUp from "./ExitOnCaughtUp.res.mjs";
|
|
12
12
|
import * as EventProcessing from "./EventProcessing.res.mjs";
|
|
13
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
13
14
|
import * as PruneStaleHistory from "./PruneStaleHistory.res.mjs";
|
|
14
15
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
16
|
+
import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
|
|
15
17
|
|
|
16
18
|
function yieldTick() {
|
|
17
19
|
return new Promise((resolve, param) => {
|
|
@@ -55,6 +57,10 @@ async function processNextBatch(state, scheduleFetch) {
|
|
|
55
57
|
return IndexerState.errorExit(state, errHandler._0);
|
|
56
58
|
}
|
|
57
59
|
IndexerState.recordProcessedBatch(state);
|
|
60
|
+
let tracker = IndexerState.simulateDeadInputTracker(state);
|
|
61
|
+
if (tracker !== undefined) {
|
|
62
|
+
SimulateDeadInputTracker.recordProcessed(Primitive_option.valFromOption(tracker), batch);
|
|
63
|
+
}
|
|
58
64
|
if (IndexerState.isResolvingReorg(state)) {
|
|
59
65
|
return IndexerState.applyBatchProgress(state, batch);
|
|
60
66
|
}
|
package/src/ExitOnCaughtUp.res
CHANGED
|
@@ -4,7 +4,15 @@ let run = async (state: IndexerState.t) => {
|
|
|
4
4
|
ChainMetadata.stage(state)
|
|
5
5
|
await state->Writing.flush
|
|
6
6
|
if !(state->IndexerState.isStopped) && !(state->IndexerState.isResolvingReorg) {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// A simulate run fails here when a provided item never reached a handler —
|
|
8
|
+
// dead test code. The tracker is None (a no-op) off the simulate path.
|
|
9
|
+
switch state
|
|
10
|
+
->IndexerState.simulateDeadInputTracker
|
|
11
|
+
->Option.flatMap(SimulateDeadInputTracker.failureMessage) {
|
|
12
|
+
| None =>
|
|
13
|
+
Logging.info("Exiting with success")
|
|
14
|
+
NodeJs.process->NodeJs.exitWithCode(Success)
|
|
15
|
+
| Some(message) => state->IndexerState.errorExit(ErrorHandling.make(Utils.Error.make(message)))
|
|
16
|
+
}
|
|
9
17
|
}
|
|
10
18
|
}
|
|
@@ -5,11 +5,20 @@ import * as Writing from "./Writing.res.mjs";
|
|
|
5
5
|
import * as Process from "process";
|
|
6
6
|
import * as IndexerState from "./IndexerState.res.mjs";
|
|
7
7
|
import * as ChainMetadata from "./ChainMetadata.res.mjs";
|
|
8
|
+
import * as ErrorHandling from "./ErrorHandling.res.mjs";
|
|
9
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
10
|
+
import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
|
|
8
11
|
|
|
9
12
|
async function run(state) {
|
|
10
13
|
ChainMetadata.stage(state);
|
|
11
14
|
await Writing.flush(state);
|
|
12
|
-
if (
|
|
15
|
+
if (IndexerState.isStopped(state) || IndexerState.isResolvingReorg(state)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let message = Stdlib_Option.flatMap(IndexerState.simulateDeadInputTracker(state), SimulateDeadInputTracker.failureMessage);
|
|
19
|
+
if (message !== undefined) {
|
|
20
|
+
return IndexerState.errorExit(state, ErrorHandling.make(new Error(message), undefined, undefined));
|
|
21
|
+
} else {
|
|
13
22
|
Logging.info("Exiting with success");
|
|
14
23
|
Process.exit(0);
|
|
15
24
|
return;
|
package/src/IndexerState.res
CHANGED
|
@@ -108,6 +108,8 @@ type t = {
|
|
|
108
108
|
// waitForNewBlock waiter is bound to the old, pre-realtime source). A fetch
|
|
109
109
|
// response or waiter carrying an older epoch than this is discarded.
|
|
110
110
|
mutable epoch: int,
|
|
111
|
+
// None off the simulate path.
|
|
112
|
+
simulateDeadInputTracker: option<SimulateDeadInputTracker.t>,
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
let make = (
|
|
@@ -169,6 +171,7 @@ let make = (
|
|
|
169
171
|
onError,
|
|
170
172
|
isStopped: false,
|
|
171
173
|
epoch: 0,
|
|
174
|
+
simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config),
|
|
172
175
|
}
|
|
173
176
|
}
|
|
174
177
|
|
|
@@ -400,6 +403,7 @@ let exitAfterFirstEventBlock = (state: t) => state.exitAfterFirstEventBlock
|
|
|
400
403
|
let isStopped = (state: t) => state.isStopped
|
|
401
404
|
let epoch = (state: t) => state.epoch
|
|
402
405
|
let pruneStaleEntityHistoryThrottler = (state: t) => state.writeThrottlers.pruneStaleEntityHistory
|
|
406
|
+
let simulateDeadInputTracker = (state: t) => state.simulateDeadInputTracker
|
|
403
407
|
|
|
404
408
|
// --- Store domain operations. ---
|
|
405
409
|
|
package/src/IndexerState.res.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import * as CrossChainState from "./CrossChainState.res.mjs";
|
|
|
18
18
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
19
19
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
20
20
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
21
|
+
import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
|
|
21
22
|
|
|
22
23
|
function make() {
|
|
23
24
|
let intervalMillis = Env.ThrottleWrites.pruneStaleDataIntervalMillis;
|
|
@@ -91,7 +92,8 @@ function make$2(config, persistence, chainStates, isInReorgThreshold, isRealtime
|
|
|
91
92
|
exitAfterFirstEventBlock: exitAfterFirstEventBlock,
|
|
92
93
|
onError: onError,
|
|
93
94
|
isStopped: false,
|
|
94
|
-
epoch: 0
|
|
95
|
+
epoch: 0,
|
|
96
|
+
simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config)
|
|
95
97
|
};
|
|
96
98
|
}
|
|
97
99
|
|
|
@@ -348,6 +350,10 @@ function pruneStaleEntityHistoryThrottler(state) {
|
|
|
348
350
|
return state.writeThrottlers.pruneStaleEntityHistory;
|
|
349
351
|
}
|
|
350
352
|
|
|
353
|
+
function simulateDeadInputTracker(state) {
|
|
354
|
+
return state.simulateDeadInputTracker;
|
|
355
|
+
}
|
|
356
|
+
|
|
351
357
|
function queueProcessedBatch(state, batch) {
|
|
352
358
|
state.processedBatches.push(batch);
|
|
353
359
|
let checkpointId = Utils.$$Array.last(batch.checkpointIds);
|
|
@@ -527,6 +533,7 @@ export {
|
|
|
527
533
|
isStopped,
|
|
528
534
|
epoch,
|
|
529
535
|
pruneStaleEntityHistoryThrottler,
|
|
536
|
+
simulateDeadInputTracker,
|
|
530
537
|
queueProcessedBatch,
|
|
531
538
|
drainBatchRun,
|
|
532
539
|
takeRollback,
|
package/src/IndexerState.resi
CHANGED
|
@@ -107,6 +107,7 @@ let exitAfterFirstEventBlock: t => bool
|
|
|
107
107
|
let isStopped: t => bool
|
|
108
108
|
let epoch: t => int
|
|
109
109
|
let pruneStaleEntityHistoryThrottler: t => Throttler.t
|
|
110
|
+
let simulateDeadInputTracker: t => option<SimulateDeadInputTracker.t>
|
|
110
111
|
|
|
111
112
|
// Store domain operations.
|
|
112
113
|
let queueProcessedBatch: (t, ~batch: Batch.t) => unit
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Lives on IndexerState and is fed each processed batch, so ChainState and the
|
|
2
|
+
// fetch loop carry no simulate-specific state.
|
|
3
|
+
|
|
4
|
+
// Match a provided item to a processed one by its (chain, block, logIndex)
|
|
5
|
+
// coordinate rather than object identity, so matching survives any copy or
|
|
6
|
+
// transform of the item between the source and the batch.
|
|
7
|
+
let itemKey = (item: Internal.item): string =>
|
|
8
|
+
switch item {
|
|
9
|
+
| Internal.Event({chain, blockNumber, logIndex}) =>
|
|
10
|
+
`${chain
|
|
11
|
+
->ChainMap.Chain.toChainId
|
|
12
|
+
->Int.toString}:${blockNumber->Int.toString}:${logIndex->Int.toString}`
|
|
13
|
+
| _ => "" // non-event items are never a provided simulate input
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// `index` is the item's position in its chain's `simulate` array, reported back
|
|
17
|
+
// so a user finds it without echoing its fields.
|
|
18
|
+
type entry = {chainId: int, index: int, key: string}
|
|
19
|
+
|
|
20
|
+
type t = {mutable unprocessed: array<entry>}
|
|
21
|
+
|
|
22
|
+
let makeFromConfig = (config: Config.t): option<t> => {
|
|
23
|
+
let entries =
|
|
24
|
+
config.chainMap
|
|
25
|
+
->ChainMap.values
|
|
26
|
+
->Array.flatMap(chainConfig =>
|
|
27
|
+
switch chainConfig.sourceConfig {
|
|
28
|
+
| Config.CustomSources(sources) =>
|
|
29
|
+
sources->Array.flatMap(source =>
|
|
30
|
+
source.simulateItems
|
|
31
|
+
->Option.getOr([])
|
|
32
|
+
->Array.mapWithIndex(
|
|
33
|
+
(item, index) => {
|
|
34
|
+
chainId: chainConfig.id,
|
|
35
|
+
index,
|
|
36
|
+
key: itemKey(item),
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
| _ => []
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
switch entries {
|
|
44
|
+
| [] => None
|
|
45
|
+
| _ => Some({unprocessed: entries})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let recordProcessed = (t: t, ~batch: Batch.t) => {
|
|
50
|
+
let processedKeys = batch.items->Array.map(itemKey)->Utils.Set.fromArray
|
|
51
|
+
t.unprocessed = t.unprocessed->Array.filter(entry => !(processedKeys->Utils.Set.has(entry.key)))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Unrouted item indices grouped by chain, in the order chains were first seen.
|
|
55
|
+
let unroutedByChain = (t: t): array<(int, array<int>)> => {
|
|
56
|
+
let indicesByChain = Dict.make()
|
|
57
|
+
let chainOrder = []
|
|
58
|
+
t.unprocessed->Array.forEach(entry => {
|
|
59
|
+
let key = entry.chainId->Int.toString
|
|
60
|
+
if indicesByChain->Dict.get(key)->Option.isNone {
|
|
61
|
+
chainOrder->Array.push(entry.chainId)->ignore
|
|
62
|
+
}
|
|
63
|
+
indicesByChain->Utils.Dict.push(key, entry.index)
|
|
64
|
+
})
|
|
65
|
+
chainOrder->Array.map(chainId => (chainId, indicesByChain->Dict.getUnsafe(chainId->Int.toString)))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let failureMessage = (t: t): option<string> =>
|
|
69
|
+
switch t->unroutedByChain {
|
|
70
|
+
| [] => None
|
|
71
|
+
| byChain =>
|
|
72
|
+
let count = byChain->Array.reduce(0, (acc, (_chainId, indices)) => acc + indices->Array.length)
|
|
73
|
+
let itemWord = count === 1 ? "item" : "items"
|
|
74
|
+
let lines =
|
|
75
|
+
byChain
|
|
76
|
+
->Array.map(((chainId, indices)) =>
|
|
77
|
+
` - chain ${chainId->Int.toString}: ${indices
|
|
78
|
+
->Array.map(index => index->Int.toString)
|
|
79
|
+
->Array.join(", ")}`
|
|
80
|
+
)
|
|
81
|
+
->Array.join("\n")
|
|
82
|
+
Some(
|
|
83
|
+
`simulate: ${count->Int.toString} ${itemWord} you passed to simulate never reached a handler, so nothing ran for them. Each was filtered out before the handler — usually a non-wildcard srcAddress that isn't indexed for the contract, or a where/block filter that excluded the event. Unrouted items, by index in each chain's simulate array:\n${lines}`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Utils from "./Utils.res.mjs";
|
|
4
|
+
import * as ChainMap from "./ChainMap.res.mjs";
|
|
5
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
|
|
8
|
+
function itemKey(item) {
|
|
9
|
+
if (item.kind === 0) {
|
|
10
|
+
return item.chain.toString() + `:` + item.blockNumber.toString() + `:` + item.logIndex.toString();
|
|
11
|
+
} else {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeFromConfig(config) {
|
|
17
|
+
let entries = ChainMap.values(config.chainMap).flatMap(chainConfig => {
|
|
18
|
+
let sources = chainConfig.sourceConfig;
|
|
19
|
+
if (sources.TAG === "CustomSources") {
|
|
20
|
+
return sources._0.flatMap(source => Stdlib_Option.getOr(source.simulateItems, []).map((item, index) => ({
|
|
21
|
+
chainId: chainConfig.id,
|
|
22
|
+
index: index,
|
|
23
|
+
key: itemKey(item)
|
|
24
|
+
})));
|
|
25
|
+
} else {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
if (entries.length !== 0) {
|
|
30
|
+
return {
|
|
31
|
+
unprocessed: entries
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function recordProcessed(t, batch) {
|
|
37
|
+
let processedKeys = new Set(batch.items.map(itemKey));
|
|
38
|
+
t.unprocessed = t.unprocessed.filter(entry => !processedKeys.has(entry.key));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function unroutedByChain(t) {
|
|
42
|
+
let indicesByChain = {};
|
|
43
|
+
let chainOrder = [];
|
|
44
|
+
t.unprocessed.forEach(entry => {
|
|
45
|
+
let key = entry.chainId.toString();
|
|
46
|
+
if (Stdlib_Option.isNone(indicesByChain[key])) {
|
|
47
|
+
chainOrder.push(entry.chainId);
|
|
48
|
+
}
|
|
49
|
+
Utils.Dict.push(indicesByChain, key, entry.index);
|
|
50
|
+
});
|
|
51
|
+
return chainOrder.map(chainId => [
|
|
52
|
+
chainId,
|
|
53
|
+
indicesByChain[chainId.toString()]
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function failureMessage(t) {
|
|
58
|
+
let byChain = unroutedByChain(t);
|
|
59
|
+
if (byChain.length === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let count = Stdlib_Array.reduce(byChain, 0, (acc, param) => acc + param[1].length | 0);
|
|
63
|
+
let itemWord = count === 1 ? "item" : "items";
|
|
64
|
+
let lines = byChain.map(param => ` - chain ` + param[0].toString() + `: ` + param[1].map(index => index.toString()).join(", ")).join("\n");
|
|
65
|
+
return `simulate: ` + count.toString() + ` ` + itemWord + ` you passed to simulate never reached a handler, so nothing ran for them. Each was filtered out before the handler — usually a non-wildcard srcAddress that isn't indexed for the contract, or a where/block filter that excluded the event. Unrouted items, by index in each chain's simulate array:\n` + lines;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export {
|
|
69
|
+
makeFromConfig,
|
|
70
|
+
recordProcessed,
|
|
71
|
+
failureMessage,
|
|
72
|
+
}
|
|
73
|
+
/* Utils Not a pure module */
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type t
|
|
2
|
+
|
|
3
|
+
// Builds a tracker from the run's config, seeded with every item each simulate
|
|
4
|
+
// source provided. None when no chain has a simulate source.
|
|
5
|
+
let makeFromConfig: Config.t => option<t>
|
|
6
|
+
|
|
7
|
+
// Drop the batch's items from the not-yet-routed set, matched by event coordinate.
|
|
8
|
+
let recordProcessed: (t, ~batch: Batch.t) => unit
|
|
9
|
+
|
|
10
|
+
// The error to fail the run with when items never reached a handler, listing them
|
|
11
|
+
// by chain and index within that chain's `simulate` array. None when all routed.
|
|
12
|
+
let failureMessage: t => option<string>
|
package/src/SimulateItems.res
CHANGED
|
@@ -231,46 +231,6 @@ let deriveSrcAddress = (
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
// A non-wildcard event is gated by `clientAddressFilter` on srcAddress ownership,
|
|
235
|
-
// so a simulate item whose srcAddress isn't indexed on this chain would be
|
|
236
|
-
// silently dropped (handler never runs). Fail loudly instead.
|
|
237
|
-
//
|
|
238
|
-
// `config` must already have handler registrations applied, otherwise an event
|
|
239
|
-
// made wildcard purely through `indexer.onEvent({ wildcard: true })` reads back
|
|
240
|
-
// as non-wildcard and gets wrongly rejected.
|
|
241
|
-
let validateSrcAddresses = (
|
|
242
|
-
~simulateItems: array<JSON.t>,
|
|
243
|
-
~config: Config.t,
|
|
244
|
-
~chainConfig: Config.chain,
|
|
245
|
-
~indexingAddresses: array<Internal.indexingAddress>,
|
|
246
|
-
): unit => {
|
|
247
|
-
let known = Utils.Set.make()
|
|
248
|
-
indexingAddresses->Array.forEach(ia => known->Utils.Set.add(ia.address->Address.toString)->ignore)
|
|
249
|
-
simulateItems->Array.forEach(rawJson => {
|
|
250
|
-
let raw = rawJson->(Utils.magic: JSON.t => rawSimulateItem)
|
|
251
|
-
switch (raw->getContract, raw->getEvent) {
|
|
252
|
-
| (Some(contractName), Some(eventName)) =>
|
|
253
|
-
switch findEventConfig(~config, ~contractName, ~eventName) {
|
|
254
|
-
| Some(eventConfig) if !eventConfig.isWildcard =>
|
|
255
|
-
let item = rawJson->(Utils.magic: JSON.t => Envio.evmSimulateItem)
|
|
256
|
-
let srcAddress = deriveSrcAddress(
|
|
257
|
-
~providedSrcAddress=item.srcAddress,
|
|
258
|
-
~eventConfig,
|
|
259
|
-
~chainConfig,
|
|
260
|
-
)
|
|
261
|
-
if !(known->Utils.Set.has(srcAddress->Address.toString)) {
|
|
262
|
-
JsError.throwWithMessage(
|
|
263
|
-
`simulate: ${contractName}.${eventName} resolved to address ${srcAddress->Address.toString}, which isn't indexed on chain ${chainConfig.id->Int.toString}. ` ++
|
|
264
|
-
`Provide a "srcAddress" configured or registered for ${contractName} on this chain, or use a wildcard event.`,
|
|
265
|
-
)
|
|
266
|
-
}
|
|
267
|
-
| _ => ()
|
|
268
|
-
}
|
|
269
|
-
| _ => ()
|
|
270
|
-
}
|
|
271
|
-
})
|
|
272
|
-
}
|
|
273
|
-
|
|
274
234
|
let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Config.chain): array<
|
|
275
235
|
Internal.item,
|
|
276
236
|
> => {
|
|
@@ -281,8 +241,11 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
|
|
|
281
241
|
let currentLogIndex = ref(0)
|
|
282
242
|
|
|
283
243
|
let items = []
|
|
244
|
+
// Coordinate "block:logIndex" -> the index of the first item that claimed it,
|
|
245
|
+
// used to reject two items resolving to the same (block, logIndex).
|
|
246
|
+
let seenCoordinates = Dict.make()
|
|
284
247
|
|
|
285
|
-
simulateItems->Array.
|
|
248
|
+
simulateItems->Array.forEachWithIndex((rawJson, itemIndex) => {
|
|
286
249
|
let raw = rawJson->(Utils.magic: JSON.t => rawSimulateItem)
|
|
287
250
|
|
|
288
251
|
switch (raw->getContract, raw->getEvent) {
|
|
@@ -306,8 +269,14 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
|
|
|
306
269
|
}
|
|
307
270
|
let params = paramsJson->S.convertOrThrow(eventConfig.simulateParamsSchema)
|
|
308
271
|
|
|
272
|
+
// An explicit logIndex advances the auto-increment counter past itself, so a
|
|
273
|
+
// later item that omits logIndex picks up after it instead of colliding.
|
|
309
274
|
let logIndex = switch item.logIndex {
|
|
310
|
-
| Some(li) =>
|
|
275
|
+
| Some(li) =>
|
|
276
|
+
if li >= currentLogIndex.contents {
|
|
277
|
+
currentLogIndex := li + 1
|
|
278
|
+
}
|
|
279
|
+
li
|
|
311
280
|
| None =>
|
|
312
281
|
let li = currentLogIndex.contents
|
|
313
282
|
currentLogIndex := li + 1
|
|
@@ -345,6 +314,18 @@ let parse = (~simulateItems: array<JSON.t>, ~config: Config.t, ~chainConfig: Con
|
|
|
345
314
|
// Update currentBlock for subsequent items
|
|
346
315
|
currentBlock := blockNumber
|
|
347
316
|
|
|
317
|
+
// A simulate item must land on a distinct (block, logIndex): event ordering
|
|
318
|
+
// and the dead-input tracker both key on it. Catch explicit duplicates and
|
|
319
|
+
// explicit-vs-auto-increment collisions here, naming both offending indices.
|
|
320
|
+
let coordinate = `${blockNumber->Int.toString}:${logIndex->Int.toString}`
|
|
321
|
+
switch seenCoordinates->Dict.get(coordinate) {
|
|
322
|
+
| Some(firstIndex) =>
|
|
323
|
+
JsError.throwWithMessage(
|
|
324
|
+
`simulate: items at index ${firstIndex->Int.toString} and ${itemIndex->Int.toString} on chain ${chainId->Int.toString} both resolve to block ${blockNumber->Int.toString}, logIndex ${logIndex->Int.toString}. Give each item a distinct logIndex (or omit logIndex so they auto-increment).`,
|
|
325
|
+
)
|
|
326
|
+
| None => seenCoordinates->Dict.set(coordinate, itemIndex)
|
|
327
|
+
}
|
|
328
|
+
|
|
348
329
|
items
|
|
349
330
|
->Array.push(
|
|
350
331
|
Internal.Event({
|
|
@@ -165,34 +165,6 @@ function deriveSrcAddress(providedSrcAddress, eventConfig, chainConfig) {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
function validateSrcAddresses(simulateItems, config, chainConfig, indexingAddresses) {
|
|
169
|
-
let known = new Set();
|
|
170
|
-
indexingAddresses.forEach(ia => {
|
|
171
|
-
known.add(ia.address);
|
|
172
|
-
});
|
|
173
|
-
simulateItems.forEach(rawJson => {
|
|
174
|
-
let match = rawJson.contract;
|
|
175
|
-
let match$1 = rawJson.event;
|
|
176
|
-
if (match === undefined) {
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (match$1 === undefined) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
let eventConfig = findEventConfig(config, match, match$1);
|
|
183
|
-
if (eventConfig === undefined) {
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
if (eventConfig.isWildcard) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
let srcAddress = deriveSrcAddress(rawJson.srcAddress, eventConfig, chainConfig);
|
|
190
|
-
if (!known.has(srcAddress)) {
|
|
191
|
-
return Stdlib_JsError.throwWithMessage(`simulate: ` + match + `.` + match$1 + ` resolved to address ` + srcAddress + `, which isn't indexed on chain ` + chainConfig.id.toString() + `. ` + (`Provide a "srcAddress" configured or registered for ` + match + ` on this chain, or use a wildcard event.`));
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
168
|
function parse(simulateItems, config, chainConfig) {
|
|
197
169
|
let chain = ChainMap.Chain.makeUnsafe(chainConfig.id);
|
|
198
170
|
let chainId = chainConfig.id;
|
|
@@ -204,7 +176,8 @@ function parse(simulateItems, config, chainConfig) {
|
|
|
204
176
|
contents: 0
|
|
205
177
|
};
|
|
206
178
|
let items = [];
|
|
207
|
-
|
|
179
|
+
let seenCoordinates = {};
|
|
180
|
+
simulateItems.forEach((rawJson, itemIndex) => {
|
|
208
181
|
let match = rawJson.contract;
|
|
209
182
|
let match$1 = rawJson.event;
|
|
210
183
|
if (match === undefined) {
|
|
@@ -221,6 +194,9 @@ function parse(simulateItems, config, chainConfig) {
|
|
|
221
194
|
let li = rawJson.logIndex;
|
|
222
195
|
let logIndex;
|
|
223
196
|
if (li !== undefined) {
|
|
197
|
+
if (li >= currentLogIndex.contents) {
|
|
198
|
+
currentLogIndex.contents = li + 1 | 0;
|
|
199
|
+
}
|
|
224
200
|
logIndex = li;
|
|
225
201
|
} else {
|
|
226
202
|
let li$1 = currentLogIndex.contents;
|
|
@@ -272,6 +248,13 @@ function parse(simulateItems, config, chainConfig) {
|
|
|
272
248
|
break;
|
|
273
249
|
}
|
|
274
250
|
currentBlock.contents = blockNumber;
|
|
251
|
+
let coordinate = blockNumber.toString() + `:` + logIndex.toString();
|
|
252
|
+
let firstIndex = seenCoordinates[coordinate];
|
|
253
|
+
if (firstIndex !== undefined) {
|
|
254
|
+
Stdlib_JsError.throwWithMessage(`simulate: items at index ` + firstIndex.toString() + ` and ` + itemIndex.toString() + ` on chain ` + chainId.toString() + ` both resolve to block ` + blockNumber.toString() + `, logIndex ` + logIndex.toString() + `. Give each item a distinct logIndex (or omit logIndex so they auto-increment).`);
|
|
255
|
+
} else {
|
|
256
|
+
seenCoordinates[coordinate] = itemIndex;
|
|
257
|
+
}
|
|
275
258
|
items.push({
|
|
276
259
|
kind: 0,
|
|
277
260
|
eventConfig: eventConfig,
|
|
@@ -360,7 +343,6 @@ export {
|
|
|
360
343
|
dummySrcAddress,
|
|
361
344
|
firstContractAddress,
|
|
362
345
|
deriveSrcAddress,
|
|
363
|
-
validateSrcAddresses,
|
|
364
346
|
parse,
|
|
365
347
|
patchConfig,
|
|
366
348
|
}
|
package/src/TestIndexer.res
CHANGED
|
@@ -667,11 +667,7 @@ let makeCreateTestIndexer = (~config: Config.t, ~workerPath: string): (
|
|
|
667
667
|
Int.compare(aId, bId)
|
|
668
668
|
})
|
|
669
669
|
|
|
670
|
-
// Parse and validate the block ranges upfront before starting any
|
|
671
|
-
// workers. srcAddress validation can't run here: it depends on which
|
|
672
|
-
// events are wildcard, and `config` reflects config.yaml only —
|
|
673
|
-
// handler-level `wildcard: true` takes effect only once registrations
|
|
674
|
-
// are applied.
|
|
670
|
+
// Parse and validate the block ranges upfront before starting any workers.
|
|
675
671
|
let chainEntries = sortedChainKeys->Array.map(chainIdStr => {
|
|
676
672
|
let rawChainConfig = rawChains->Dict.getUnsafe(chainIdStr)
|
|
677
673
|
let chainId = switch chainIdStr->Int.fromString {
|
|
@@ -868,26 +864,6 @@ let initTestWorker = () => {
|
|
|
868
864
|
}
|
|
869
865
|
|
|
870
866
|
let patchConfig = (config: Config.t, _registrations) => {
|
|
871
|
-
// `config` here has handler registrations applied, so `isWildcard`
|
|
872
|
-
// reflects `indexer.onEvent({ wildcard: true })` — which the main thread
|
|
873
|
-
// can't see. Validate srcAddresses against it before patching in the
|
|
874
|
-
// simulate source.
|
|
875
|
-
switch simulate {
|
|
876
|
-
| Some(simulateItems) =>
|
|
877
|
-
let chainConfig = config.chainMap->ChainMap.get(ChainMap.Chain.makeUnsafe(~chainId))
|
|
878
|
-
let indexingAddresses =
|
|
879
|
-
initialState.chains
|
|
880
|
-
->Array.find(c => c.id == chainId)
|
|
881
|
-
->Option.mapOr([], c => c.indexingAddresses)
|
|
882
|
-
SimulateItems.validateSrcAddresses(
|
|
883
|
-
~simulateItems,
|
|
884
|
-
~config,
|
|
885
|
-
~chainConfig,
|
|
886
|
-
~indexingAddresses,
|
|
887
|
-
)
|
|
888
|
-
| None => ()
|
|
889
|
-
}
|
|
890
|
-
|
|
891
867
|
let config = SimulateItems.patchConfig(~config, ~processConfig)
|
|
892
868
|
|
|
893
869
|
// In auto-exit mode, set batchSize=1 to process one block checkpoint at a time
|
|
@@ -900,8 +876,8 @@ let initTestWorker = () => {
|
|
|
900
876
|
Main.start(~persistence, ~isTest=true, ~patchConfig, ~exitAfterFirstEventBlock)
|
|
901
877
|
->Promise.catch(exn => {
|
|
902
878
|
// `Main.start` rejects on any fatal error: a runtime failure arrives wrapped
|
|
903
|
-
// in `Main.FatalError` (already logged), a setup throw (e.g.
|
|
904
|
-
//
|
|
879
|
+
// in `Main.FatalError` (already logged), a setup throw (e.g. an invalid
|
|
880
|
+
// simulate item) arrives raw. The parent only learns of failures
|
|
905
881
|
// through the worker `error` event, which fires on an *uncaught* exception.
|
|
906
882
|
// Throwing synchronously in this catch would just reject the catch's own
|
|
907
883
|
// promise (swallowed by `ignore`); `setImmediate` re-throws outside the
|
package/src/TestIndexer.res.mjs
CHANGED
|
@@ -586,11 +586,9 @@ function initTestWorker() {
|
|
|
586
586
|
Process.exit(1);
|
|
587
587
|
return;
|
|
588
588
|
}
|
|
589
|
-
let initialState = workerData.initialState;
|
|
590
589
|
let simulate = workerData.simulate;
|
|
591
590
|
let endBlock = workerData.endBlock;
|
|
592
|
-
let
|
|
593
|
-
let chainIdStr = chainId.toString();
|
|
591
|
+
let chainIdStr = workerData.chainId.toString();
|
|
594
592
|
let exitAfterFirstEventBlock = Stdlib_Option.isNone(endBlock);
|
|
595
593
|
let resolvedChainDict = {};
|
|
596
594
|
resolvedChainDict["startBlock"] = workerData.startBlock;
|
|
@@ -605,7 +603,7 @@ function initTestWorker() {
|
|
|
605
603
|
let processConfig = {
|
|
606
604
|
chains: resolvedChainsDict
|
|
607
605
|
};
|
|
608
|
-
let proxy = TestIndexerProxyStorage.make(parentPort, initialState);
|
|
606
|
+
let proxy = TestIndexerProxyStorage.make(parentPort, workerData.initialState);
|
|
609
607
|
let storage = TestIndexerProxyStorage.makeStorage(proxy);
|
|
610
608
|
let config = Config.loadWithoutRegistrations();
|
|
611
609
|
let persistence = Persistence.make(config.userEntities, config.allEnums, storage);
|
|
@@ -615,11 +613,6 @@ function initTestWorker() {
|
|
|
615
613
|
Logging.setLogLevel("silent");
|
|
616
614
|
}
|
|
617
615
|
let patchConfig = (config, _registrations) => {
|
|
618
|
-
if (simulate !== undefined) {
|
|
619
|
-
let chainConfig = ChainMap.get(config.chainMap, ChainMap.Chain.makeUnsafe(chainId));
|
|
620
|
-
let indexingAddresses = Stdlib_Option.mapOr(initialState.chains.find(c => c.id === chainId), [], c => c.indexingAddresses);
|
|
621
|
-
SimulateItems.validateSrcAddresses(simulate, config, chainConfig, indexingAddresses);
|
|
622
|
-
}
|
|
623
616
|
let config$1 = SimulateItems.patchConfig(config, processConfig);
|
|
624
617
|
if (exitAfterFirstEventBlock) {
|
|
625
618
|
return {
|
package/src/sources/EvmChain.res
CHANGED
|
@@ -32,7 +32,7 @@ let getSyncConfig = (
|
|
|
32
32
|
intervalCeiling: Env.Configurable.SyncConfig.intervalCeiling->Option.getOr(
|
|
33
33
|
intervalCeiling->Option.getOr(10_000),
|
|
34
34
|
),
|
|
35
|
-
backoffMillis: backoffMillis->Option.getOr(
|
|
35
|
+
backoffMillis: backoffMillis->Option.getOr(2000),
|
|
36
36
|
queryTimeoutMillis,
|
|
37
37
|
fallbackStallTimeout: fallbackStallTimeout->Option.getOr(queryTimeoutMillis / 2),
|
|
38
38
|
pollingInterval: pollingInterval->Option.getOr(1000),
|
|
@@ -13,7 +13,7 @@ function getSyncConfig(param) {
|
|
|
13
13
|
backoffMultiplicative: Stdlib_Option.getOr(Env.Configurable.SyncConfig.backoffMultiplicative, Stdlib_Option.getOr(param.backoffMultiplicative, 0.8)),
|
|
14
14
|
accelerationAdditive: Stdlib_Option.getOr(Env.Configurable.SyncConfig.accelerationAdditive, Stdlib_Option.getOr(param.accelerationAdditive, 500)),
|
|
15
15
|
intervalCeiling: Stdlib_Option.getOr(Env.Configurable.SyncConfig.intervalCeiling, Stdlib_Option.getOr(param.intervalCeiling, 10000)),
|
|
16
|
-
backoffMillis: Stdlib_Option.getOr(param.backoffMillis,
|
|
16
|
+
backoffMillis: Stdlib_Option.getOr(param.backoffMillis, 2000),
|
|
17
17
|
queryTimeoutMillis: queryTimeoutMillis,
|
|
18
18
|
fallbackStallTimeout: Stdlib_Option.getOr(param.fallbackStallTimeout, queryTimeoutMillis / 2 | 0),
|
|
19
19
|
pollingInterval: Stdlib_Option.getOr(param.pollingInterval, 1000)
|