envio 3.3.0-alpha.1 → 3.3.0-alpha.11
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/evm.schema.json +10 -0
- package/index.d.ts +45 -25
- package/licenses/CLA.md +35 -0
- package/licenses/EULA.md +67 -0
- package/licenses/LICENSE.md +45 -0
- package/licenses/README.md +35 -0
- package/package.json +10 -9
- package/src/Address.res +5 -2
- package/src/Address.res.mjs +3 -1
- package/src/Api.res +1 -1
- package/src/Api.res.mjs +1 -1
- package/src/Batch.res +0 -6
- package/src/Batch.res.mjs +1 -12
- package/src/BatchProcessing.res +7 -12
- package/src/BatchProcessing.res.mjs +8 -7
- package/src/Bin.res +3 -0
- package/src/Bin.res.mjs +4 -0
- package/src/ChainFetching.res +53 -16
- package/src/ChainFetching.res.mjs +37 -19
- package/src/ChainMetadata.res +1 -11
- package/src/ChainMetadata.res.mjs +1 -10
- package/src/ChainState.res +630 -161
- package/src/ChainState.res.mjs +438 -101
- package/src/ChainState.resi +77 -4
- package/src/Config.res +70 -38
- package/src/Config.res.mjs +52 -12
- package/src/ContractRegisterContext.res +2 -12
- package/src/ContractRegisterContext.res.mjs +3 -5
- package/src/Core.res +34 -3
- package/src/Core.res.mjs +12 -0
- package/src/CrossChainState.res +130 -89
- package/src/CrossChainState.res.mjs +66 -61
- package/src/CrossChainState.resi +1 -1
- package/src/Ecosystem.res +5 -6
- package/src/Ecosystem.res.mjs +3 -3
- package/src/Envio.res +17 -11
- package/src/EnvioGlobal.res +53 -0
- package/src/EnvioGlobal.res.mjs +31 -0
- package/src/EventConfigBuilder.res +191 -127
- package/src/EventConfigBuilder.res.mjs +116 -66
- package/src/EventProcessing.res +77 -28
- package/src/EventProcessing.res.mjs +56 -29
- package/src/ExitOnCaughtUp.res +10 -2
- package/src/ExitOnCaughtUp.res.mjs +10 -1
- package/src/FetchState.res +684 -415
- package/src/FetchState.res.mjs +505 -486
- package/src/HandlerLoader.res +8 -104
- package/src/HandlerLoader.res.mjs +2 -81
- package/src/HandlerRegister.res +520 -144
- package/src/HandlerRegister.res.mjs +286 -133
- package/src/HandlerRegister.resi +24 -8
- package/src/IndexerState.res +10 -3
- package/src/IndexerState.res.mjs +11 -4
- package/src/IndexerState.resi +2 -1
- package/src/IndexingAddresses.res +108 -0
- package/src/IndexingAddresses.res.mjs +101 -0
- package/src/IndexingAddresses.resi +34 -0
- package/src/Internal.res +166 -48
- package/src/Internal.res.mjs +30 -2
- package/src/LoadLayer.res +5 -5
- package/src/LoadLayer.res.mjs +6 -6
- package/src/LogSelection.res +92 -217
- package/src/LogSelection.res.mjs +95 -184
- package/src/Main.res +71 -236
- package/src/Main.res.mjs +60 -145
- package/src/Metrics.res +74 -0
- package/src/Metrics.res.mjs +75 -0
- package/src/PgStorage.res +4 -4
- package/src/PgStorage.res.mjs +5 -5
- package/src/Prometheus.res +10 -63
- package/src/Prometheus.res.mjs +124 -171
- package/src/PruneStaleHistory.res +2 -2
- package/src/PruneStaleHistory.res.mjs +3 -3
- package/src/RawEvent.res +7 -2
- package/src/RawEvent.res.mjs +4 -4
- package/src/Rollback.res +3 -3
- package/src/Rollback.res.mjs +4 -4
- package/src/RollbackCommit.res +4 -1
- package/src/RollbackCommit.res.mjs +3 -2
- package/src/SimulateDeadInputTracker.res +85 -0
- package/src/SimulateDeadInputTracker.res.mjs +73 -0
- package/src/SimulateDeadInputTracker.resi +12 -0
- package/src/SimulateItems.res +128 -28
- package/src/SimulateItems.res.mjs +82 -40
- package/src/TestIndexer.res +55 -41
- package/src/TestIndexer.res.mjs +39 -32
- package/src/TopicFilter.res +1 -25
- package/src/TopicFilter.res.mjs +0 -74
- package/src/bindings/Performance.res +7 -0
- package/src/bindings/Performance.res.mjs +21 -0
- package/src/bindings/Performance.resi +7 -0
- package/src/bindings/Viem.res +0 -46
- package/src/bindings/Viem.res.mjs +1 -43
- package/src/sources/BlockStore.res +46 -0
- package/src/sources/BlockStore.res.mjs +24 -0
- package/src/sources/EventRouter.res +31 -49
- package/src/sources/EventRouter.res.mjs +12 -25
- package/src/sources/Evm.res +83 -72
- package/src/sources/Evm.res.mjs +53 -69
- package/src/sources/EvmChain.res +7 -34
- package/src/sources/EvmChain.res.mjs +6 -27
- package/src/sources/EvmRpcClient.res +71 -5
- package/src/sources/EvmRpcClient.res.mjs +13 -4
- package/src/sources/FieldMask.res +39 -0
- package/src/sources/FieldMask.res.mjs +42 -0
- package/src/sources/Fuel.res +9 -6
- package/src/sources/Fuel.res.mjs +5 -10
- package/src/sources/HyperFuelSource.res +46 -50
- package/src/sources/HyperFuelSource.res.mjs +64 -54
- package/src/sources/HyperSync.res +45 -42
- package/src/sources/HyperSync.res.mjs +68 -60
- package/src/sources/HyperSync.resi +14 -4
- package/src/sources/HyperSyncClient.res +107 -119
- package/src/sources/HyperSyncClient.res.mjs +36 -31
- package/src/sources/HyperSyncHeightStream.res +1 -8
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
- package/src/sources/HyperSyncSource.res +60 -254
- package/src/sources/HyperSyncSource.res.mjs +57 -180
- package/src/sources/Rpc.res +15 -79
- package/src/sources/Rpc.res.mjs +25 -101
- package/src/sources/RpcSource.res +221 -514
- package/src/sources/RpcSource.res.mjs +216 -411
- package/src/sources/RpcWebSocketHeightStream.res +0 -5
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
- package/src/sources/SimulateSource.res +44 -20
- package/src/sources/SimulateSource.res.mjs +41 -14
- package/src/sources/Source.res +34 -6
- package/src/sources/SourceManager.res +77 -19
- package/src/sources/SourceManager.res.mjs +63 -21
- package/src/sources/SourceManager.resi +10 -0
- package/src/sources/Svm.res +31 -16
- package/src/sources/Svm.res.mjs +36 -13
- package/src/sources/SvmHyperSyncClient.res +5 -32
- package/src/sources/SvmHyperSyncSource.res +159 -134
- package/src/sources/SvmHyperSyncSource.res.mjs +164 -129
- package/src/sources/TransactionStore.res +49 -0
- package/src/sources/TransactionStore.res.mjs +30 -0
- package/src/tui/Tui.res +13 -16
- package/src/tui/Tui.res.mjs +12 -14
- package/svm.schema.json +49 -37
- package/src/bindings/Hrtime.res +0 -58
- package/src/bindings/Hrtime.res.mjs +0 -90
- package/src/bindings/Hrtime.resi +0 -30
package/src/ChainState.res
CHANGED
|
@@ -4,19 +4,59 @@
|
|
|
4
4
|
|
|
5
5
|
type t = {
|
|
6
6
|
logger: Pino.t,
|
|
7
|
+
// The registrations used to build this chain's sources and route native items.
|
|
8
|
+
onEventRegistrations: array<Internal.onEventRegistration>,
|
|
7
9
|
mutable fetchState: FetchState.t,
|
|
10
|
+
// The chain-wide address index. Not `mutable`: the dict is mutated in place by
|
|
11
|
+
// register/rollback, so the reference is stable across fetchState versions.
|
|
12
|
+
indexingAddresses: IndexingAddresses.t,
|
|
8
13
|
sourceManager: SourceManager.t,
|
|
9
14
|
chainConfig: Config.chain,
|
|
10
15
|
mutable isProgressAtHead: bool,
|
|
11
16
|
mutable timestampCaughtUpToHeadOrEndblock: option<Date.t>,
|
|
12
17
|
mutable committedProgressBlockNumber: int,
|
|
18
|
+
// Progress of the batch currently being processed. The buffer is consumed at
|
|
19
|
+
// batch creation (see advanceAfterBatch), so this runs ahead of
|
|
20
|
+
// committedProgressBlockNumber until the batch commits — it's the true lower
|
|
21
|
+
// boundary of the remaining buffer's block span.
|
|
22
|
+
mutable processingBlockNumber: int,
|
|
13
23
|
mutable numEventsProcessed: float,
|
|
14
|
-
// Running sum of in-flight queries'
|
|
24
|
+
// Running sum of in-flight queries' itemsEst, kept here so the
|
|
15
25
|
// scheduler doesn't re-sum pending queries on every tick. Incremented when
|
|
16
26
|
// queries are dispatched, decremented as their responses land.
|
|
17
27
|
mutable pendingBudget: float,
|
|
28
|
+
// Chain-wide events/block, used to turn a chain's item budget into a target
|
|
29
|
+
// block for query sizing. Seeded from cumulative progress on construction,
|
|
30
|
+
// then smoothed with an EMA on every batch (see applyBatchProgress). None
|
|
31
|
+
// until the chain has processed at least one event.
|
|
32
|
+
mutable chainDensity: option<float>,
|
|
18
33
|
mutable reorgDetection: ReorgDetection.t,
|
|
19
34
|
mutable safeCheckpointTracking: option<SafeCheckpointTracking.t>,
|
|
35
|
+
// Holds this chain's transactions (kept in Rust) keyed by (blockNumber,
|
|
36
|
+
// transactionIndex). Fetch responses merge their page in; entries are pruned
|
|
37
|
+
// as the chain progresses and dropped above the target on rollback.
|
|
38
|
+
transactionStore: TransactionStore.t,
|
|
39
|
+
// Holds this chain's blocks (kept in Rust) keyed by block number. Same merge /
|
|
40
|
+
// prune / rollback lifecycle as the transaction store.
|
|
41
|
+
blockStore: BlockStore.t,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Per-chain shape returned by the status API.
|
|
45
|
+
type chainData = {
|
|
46
|
+
chainId: float,
|
|
47
|
+
poweredByHyperSync: bool,
|
|
48
|
+
firstEventBlockNumber: option<int>,
|
|
49
|
+
latestProcessedBlock: option<int>,
|
|
50
|
+
timestampCaughtUpToHeadOrEndblock: option<Date.t>,
|
|
51
|
+
numEventsProcessed: float,
|
|
52
|
+
latestFetchedBlockNumber: int,
|
|
53
|
+
// Need this for API backwards compatibility
|
|
54
|
+
@as("currentBlockHeight")
|
|
55
|
+
knownHeight: int,
|
|
56
|
+
numBatchesFetched: int,
|
|
57
|
+
startBlock: int,
|
|
58
|
+
endBlock: option<int>,
|
|
59
|
+
numAddresses: int,
|
|
20
60
|
}
|
|
21
61
|
|
|
22
62
|
let configAddresses = (chainConfig: Config.chain): array<Internal.indexingAddress> => {
|
|
@@ -33,9 +73,23 @@ let configAddresses = (chainConfig: Config.chain): array<Internal.indexingAddres
|
|
|
33
73
|
addresses
|
|
34
74
|
}
|
|
35
75
|
|
|
76
|
+
let validateOnEventRegistrations = (
|
|
77
|
+
~chainId: int,
|
|
78
|
+
registrations: array<Internal.onEventRegistration>,
|
|
79
|
+
) =>
|
|
80
|
+
registrations->Array.forEachWithIndex((registration, expectedIndex) => {
|
|
81
|
+
if registration.index !== expectedIndex {
|
|
82
|
+
JsError.throwWithMessage(
|
|
83
|
+
`Invalid onEvent registration index for chain ${chainId->Int.toString}: ${registration.eventConfig.contractName}.${registration.eventConfig.name} has index ${registration.index->Int.toString}, but its ChainState position is ${expectedIndex->Int.toString}.`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
36
88
|
let make = (
|
|
37
89
|
~chainConfig: Config.chain,
|
|
38
90
|
~fetchState: FetchState.t,
|
|
91
|
+
~onEventRegistrations=[],
|
|
92
|
+
~indexingAddresses: IndexingAddresses.t,
|
|
39
93
|
~sourceManager: SourceManager.t,
|
|
40
94
|
~reorgDetection: ReorgDetection.t,
|
|
41
95
|
~committedProgressBlockNumber: int,
|
|
@@ -43,19 +97,31 @@ let make = (
|
|
|
43
97
|
~numEventsProcessed=0.,
|
|
44
98
|
~timestampCaughtUpToHeadOrEndblock=None,
|
|
45
99
|
~isProgressAtHead=false,
|
|
100
|
+
~transactionStore=TransactionStore.make(~ecosystem=Ecosystem.Evm, ~shouldChecksum=false),
|
|
101
|
+
~chainDensity=None,
|
|
102
|
+
~blockStore=BlockStore.make(~ecosystem=Ecosystem.Evm, ~shouldChecksum=false),
|
|
46
103
|
~logger: Pino.t,
|
|
47
104
|
): t => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
105
|
+
validateOnEventRegistrations(~chainId=chainConfig.id, onEventRegistrations)
|
|
106
|
+
{
|
|
107
|
+
logger,
|
|
108
|
+
onEventRegistrations,
|
|
109
|
+
fetchState,
|
|
110
|
+
indexingAddresses,
|
|
111
|
+
sourceManager,
|
|
112
|
+
chainConfig,
|
|
113
|
+
isProgressAtHead,
|
|
114
|
+
timestampCaughtUpToHeadOrEndblock,
|
|
115
|
+
committedProgressBlockNumber,
|
|
116
|
+
processingBlockNumber: committedProgressBlockNumber,
|
|
117
|
+
numEventsProcessed,
|
|
118
|
+
pendingBudget: 0.,
|
|
119
|
+
chainDensity,
|
|
120
|
+
reorgDetection,
|
|
121
|
+
safeCheckpointTracking,
|
|
122
|
+
transactionStore,
|
|
123
|
+
blockStore,
|
|
124
|
+
}
|
|
59
125
|
}
|
|
60
126
|
|
|
61
127
|
let makeInternal = (
|
|
@@ -66,7 +132,7 @@ let makeInternal = (
|
|
|
66
132
|
~firstEventBlock=None,
|
|
67
133
|
~progressBlockNumber,
|
|
68
134
|
~config: Config.t,
|
|
69
|
-
~
|
|
135
|
+
~registrationsByChainId: HandlerRegister.registrationsByChainId,
|
|
70
136
|
~logger,
|
|
71
137
|
~timestampCaughtUpToHeadOrEndblock,
|
|
72
138
|
~numEventsProcessed,
|
|
@@ -77,129 +143,35 @@ let makeInternal = (
|
|
|
77
143
|
~knownHeight=0,
|
|
78
144
|
~reducedPollingInterval=?,
|
|
79
145
|
): t => {
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let eventConfigs: array<Internal.eventConfig> = []
|
|
88
|
-
|
|
89
|
-
let notRegisteredEvents = []
|
|
146
|
+
// Handler binding + `where`-derived fetch state, and onBlock registrations,
|
|
147
|
+
// are already collected by `HandlerRegister.finishRegistration`, keyed by
|
|
148
|
+
// chain - this just looks up this chain's slice.
|
|
149
|
+
let {onEventRegistrations, onBlockRegistrations} =
|
|
150
|
+
registrationsByChainId
|
|
151
|
+
->Utils.Dict.dangerouslyGetNonOption(chainConfig.id->Int.toString)
|
|
152
|
+
->Option.getOr({onEventRegistrations: [], onBlockRegistrations: []})
|
|
90
153
|
|
|
91
154
|
chainConfig.contracts->Array.forEach(contract => {
|
|
92
|
-
let contractName = contract.name
|
|
93
|
-
|
|
94
|
-
contract.events->Array.forEach(eventConfig => {
|
|
95
|
-
let {isWildcard} = eventConfig
|
|
96
|
-
let hasContractRegister = eventConfig.contractRegister->Option.isSome
|
|
97
|
-
|
|
98
|
-
// Should validate the events
|
|
99
|
-
eventRouter->EventRouter.addOrThrow(
|
|
100
|
-
eventConfig.id,
|
|
101
|
-
(),
|
|
102
|
-
~contractName,
|
|
103
|
-
~chain=ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id),
|
|
104
|
-
~eventName=eventConfig.name,
|
|
105
|
-
~isWildcard,
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
// Filter out non-preRegistration events on preRegistration phase
|
|
109
|
-
// so we don't care about it in fetch state and workers anymore
|
|
110
|
-
let shouldBeIncluded = if config.enableRawEvents {
|
|
111
|
-
true
|
|
112
|
-
} else {
|
|
113
|
-
let isRegistered = hasContractRegister || eventConfig.handler->Option.isSome
|
|
114
|
-
if !isRegistered {
|
|
115
|
-
notRegisteredEvents->Array.push(eventConfig)
|
|
116
|
-
}
|
|
117
|
-
isRegistered
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Check if event has Static([]) filters (from a dynamic where
|
|
121
|
-
// callback returning `false` / SkipAll for this chain).
|
|
122
|
-
// If so, skip it entirely - it should never be fetched
|
|
123
|
-
let shouldSkip = try {
|
|
124
|
-
let getEventFiltersOrThrow = (
|
|
125
|
-
eventConfig->(Utils.magic: Internal.eventConfig => Internal.evmEventConfig)
|
|
126
|
-
).getEventFiltersOrThrow
|
|
127
|
-
|
|
128
|
-
// Check for non-evm chains
|
|
129
|
-
if (
|
|
130
|
-
getEventFiltersOrThrow->(Utils.magic: (ChainMap.Chain.t => Internal.eventFilters) => bool)
|
|
131
|
-
) {
|
|
132
|
-
switch getEventFiltersOrThrow(ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id)) {
|
|
133
|
-
| Static([]) => true
|
|
134
|
-
| _ => false
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
false
|
|
138
|
-
}
|
|
139
|
-
} catch {
|
|
140
|
-
// Can throw when filter is invalid
|
|
141
|
-
// Don't skip an event in this case. Let it throw in a better place - source code
|
|
142
|
-
| _ => false
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if shouldBeIncluded && !shouldSkip {
|
|
146
|
-
eventConfigs->Array.push(eventConfig)
|
|
147
|
-
}
|
|
148
|
-
})
|
|
149
|
-
|
|
150
155
|
switch contract.startBlock {
|
|
151
156
|
| Some(startBlock) if startBlock < chainConfig.startBlock =>
|
|
152
157
|
JsError.throwWithMessage(
|
|
153
|
-
`The start block for contract "${
|
|
158
|
+
`The start block for contract "${contract.name}" is less than the chain start block. This is not supported yet.`,
|
|
154
159
|
)
|
|
155
160
|
| _ => ()
|
|
156
161
|
}
|
|
157
162
|
})
|
|
158
163
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
`The event${if notRegisteredEvents->Array.length > 1 {
|
|
162
|
-
"s"
|
|
163
|
-
} else {
|
|
164
|
-
""
|
|
165
|
-
}} ${notRegisteredEvents
|
|
166
|
-
->Array.map(eventConfig => `${eventConfig.contractName}.${eventConfig.name}`)
|
|
167
|
-
->Array.joinUnsafe(", ")} don't have an event handler and skipped for indexing.`,
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
let onBlockConfigs =
|
|
172
|
-
registrations.onBlockByChainId->Utils.Dict.dangerouslyGetNonOption(chainConfig.id->Int.toString)
|
|
173
|
-
switch onBlockConfigs {
|
|
174
|
-
| Some(onBlockConfigs) =>
|
|
175
|
-
// TODO: Move it to the HandlerRegister module
|
|
176
|
-
// so the error is thrown with better stack trace
|
|
177
|
-
onBlockConfigs->Array.forEach(onBlockConfig => {
|
|
178
|
-
if onBlockConfig.startBlock->Option.getOr(startBlock) < startBlock {
|
|
179
|
-
JsError.throwWithMessage(
|
|
180
|
-
`The start block for onBlock handler "${onBlockConfig.name}" is less than the chain start block (${startBlock->Int.toString}). This is not supported yet.`,
|
|
181
|
-
)
|
|
182
|
-
}
|
|
183
|
-
switch endBlock {
|
|
184
|
-
| Some(chainEndBlock) =>
|
|
185
|
-
if onBlockConfig.endBlock->Option.getOr(chainEndBlock) > chainEndBlock {
|
|
186
|
-
JsError.throwWithMessage(
|
|
187
|
-
`The end block for onBlock handler "${onBlockConfig.name}" is greater than the chain end block (${chainEndBlock->Int.toString}). This is not supported yet.`,
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
| None => ()
|
|
191
|
-
}
|
|
192
|
-
})
|
|
193
|
-
| None => ()
|
|
194
|
-
}
|
|
164
|
+
let contractConfigs = IndexingAddresses.makeContractConfigs(~onEventRegistrations)
|
|
165
|
+
let indexingAddressIndex = IndexingAddresses.make(~contractConfigs, ~addresses=indexingAddresses)
|
|
195
166
|
|
|
196
167
|
let fetchState = FetchState.make(
|
|
197
168
|
~maxAddrInPartition=config.maxAddrInPartition,
|
|
169
|
+
~contractConfigs,
|
|
198
170
|
~addresses=indexingAddresses,
|
|
199
171
|
~progressBlockNumber,
|
|
200
172
|
~startBlock,
|
|
201
173
|
~endBlock,
|
|
202
|
-
~
|
|
174
|
+
~onEventRegistrations,
|
|
203
175
|
~maxOnBlockBufferSize=2 * config.batchSize,
|
|
204
176
|
~knownHeight,
|
|
205
177
|
~chainId=chainConfig.id,
|
|
@@ -208,7 +180,7 @@ let makeInternal = (
|
|
|
208
180
|
!config.shouldRollbackOnReorg || isInReorgThreshold ? 0 : chainConfig.maxReorgDepth,
|
|
209
181
|
chainConfig.blockLag,
|
|
210
182
|
),
|
|
211
|
-
~
|
|
183
|
+
~onBlockRegistrations,
|
|
212
184
|
~firstEventBlock,
|
|
213
185
|
)
|
|
214
186
|
|
|
@@ -225,29 +197,23 @@ let makeInternal = (
|
|
|
225
197
|
let lowercaseAddresses = config.lowercaseAddresses
|
|
226
198
|
let sources = switch chainConfig.sourceConfig {
|
|
227
199
|
| Config.EvmSourceConfig({hypersync, rpcs}) =>
|
|
228
|
-
// Build Internal.evmContractConfig from contracts for EvmChain.makeSources
|
|
229
|
-
let evmContracts: array<Internal.evmContractConfig> = chainConfig.contracts->Array.map((
|
|
230
|
-
contract
|
|
231
|
-
): Internal.evmContractConfig => {
|
|
232
|
-
name: contract.name,
|
|
233
|
-
abi: contract.abi,
|
|
234
|
-
events: contract.events->(
|
|
235
|
-
Utils.magic: array<Internal.eventConfig> => array<Internal.evmEventConfig>
|
|
236
|
-
),
|
|
237
|
-
})
|
|
238
200
|
let evmRpcs: array<EvmChain.rpc> = rpcs->Array.map((rpc): EvmChain.rpc => {
|
|
239
201
|
let syncConfig = rpc.syncConfig
|
|
240
202
|
let ws = rpc.ws
|
|
203
|
+
let headers = rpc.headers
|
|
241
204
|
{
|
|
242
205
|
url: rpc.url,
|
|
243
206
|
sourceFor: rpc.sourceFor,
|
|
244
207
|
?syncConfig,
|
|
245
208
|
?ws,
|
|
209
|
+
?headers,
|
|
246
210
|
}
|
|
247
211
|
})
|
|
248
212
|
EvmChain.makeSources(
|
|
249
213
|
~chain,
|
|
250
|
-
~
|
|
214
|
+
~onEventRegistrations=onEventRegistrations->(
|
|
215
|
+
Utils.magic: array<Internal.onEventRegistration> => array<Internal.evmOnEventRegistration>
|
|
216
|
+
),
|
|
251
217
|
~hyperSync=hypersync,
|
|
252
218
|
~rpcs=evmRpcs,
|
|
253
219
|
~lowercaseAddresses,
|
|
@@ -265,17 +231,13 @@ let makeInternal = (
|
|
|
265
231
|
| (Some(hypersyncUrl), _) =>
|
|
266
232
|
// HyperSync drives instruction sync. A configured RPC is ignored for now
|
|
267
233
|
// (RPC fallback isn't wired up yet).
|
|
268
|
-
let svmEventConfigs =
|
|
269
|
-
chainConfig.contracts
|
|
270
|
-
->Array.flatMap(contract => contract.events)
|
|
271
|
-
->(Utils.magic: array<Internal.eventConfig> => array<Internal.svmInstructionEventConfig>)
|
|
272
234
|
let apiToken = Env.envioApiToken
|
|
273
235
|
[
|
|
274
236
|
SvmHyperSyncSource.make({
|
|
275
237
|
chain,
|
|
276
238
|
endpointUrl: hypersyncUrl,
|
|
277
239
|
apiToken,
|
|
278
|
-
|
|
240
|
+
onEventRegistrations,
|
|
279
241
|
clientTimeoutMillis: Env.hyperSyncClientTimeoutMillis,
|
|
280
242
|
}),
|
|
281
243
|
]
|
|
@@ -284,9 +246,19 @@ let makeInternal = (
|
|
|
284
246
|
| Config.CustomSources(sources) => sources
|
|
285
247
|
}
|
|
286
248
|
|
|
249
|
+
// Seed chain density from whatever progress this chain already has (from a
|
|
250
|
+
// resumed DB state, or 0 on a fresh chain) — refined per-batch afterwards.
|
|
251
|
+
let chainDensity = switch fetchState.firstEventBlock {
|
|
252
|
+
| Some(firstEventBlock) if progressBlockNumber > firstEventBlock && numEventsProcessed > 0. =>
|
|
253
|
+
Some(numEventsProcessed /. (progressBlockNumber - firstEventBlock)->Int.toFloat)
|
|
254
|
+
| _ => None
|
|
255
|
+
}
|
|
256
|
+
|
|
287
257
|
make(
|
|
288
258
|
~chainConfig,
|
|
289
259
|
~fetchState,
|
|
260
|
+
~onEventRegistrations,
|
|
261
|
+
~indexingAddresses=indexingAddressIndex,
|
|
290
262
|
~sourceManager=SourceManager.make(~sources, ~isRealtime, ~reducedPollingInterval?),
|
|
291
263
|
~reorgDetection=ReorgDetection.make(
|
|
292
264
|
~chainReorgCheckpoints,
|
|
@@ -301,17 +273,31 @@ let makeInternal = (
|
|
|
301
273
|
~committedProgressBlockNumber=progressBlockNumber,
|
|
302
274
|
~timestampCaughtUpToHeadOrEndblock,
|
|
303
275
|
~numEventsProcessed,
|
|
276
|
+
~transactionStore=TransactionStore.make(
|
|
277
|
+
~ecosystem=config.ecosystem.name,
|
|
278
|
+
~shouldChecksum=!lowercaseAddresses,
|
|
279
|
+
),
|
|
280
|
+
~chainDensity,
|
|
281
|
+
~blockStore=BlockStore.make(
|
|
282
|
+
~ecosystem=config.ecosystem.name,
|
|
283
|
+
~shouldChecksum=!lowercaseAddresses,
|
|
284
|
+
),
|
|
304
285
|
~logger,
|
|
305
286
|
)
|
|
306
287
|
}
|
|
307
288
|
|
|
308
|
-
let makeFromConfig = (
|
|
289
|
+
let makeFromConfig = (
|
|
290
|
+
chainConfig: Config.chain,
|
|
291
|
+
~config,
|
|
292
|
+
~registrationsByChainId,
|
|
293
|
+
~knownHeight,
|
|
294
|
+
) => {
|
|
309
295
|
let logger = Logging.createChild(~params={"chainId": chainConfig.id})
|
|
310
296
|
|
|
311
297
|
makeInternal(
|
|
312
298
|
~chainConfig,
|
|
313
299
|
~config,
|
|
314
|
-
~
|
|
300
|
+
~registrationsByChainId,
|
|
315
301
|
~startBlock=chainConfig.startBlock,
|
|
316
302
|
~endBlock=chainConfig.endBlock,
|
|
317
303
|
~reorgCheckpoints=[],
|
|
@@ -337,7 +323,7 @@ let makeFromDbState = (
|
|
|
337
323
|
~isInReorgThreshold,
|
|
338
324
|
~isRealtime,
|
|
339
325
|
~config,
|
|
340
|
-
~
|
|
326
|
+
~registrationsByChainId,
|
|
341
327
|
~reducedPollingInterval=?,
|
|
342
328
|
) => {
|
|
343
329
|
let chainId = chainConfig.id
|
|
@@ -357,7 +343,7 @@ let makeFromDbState = (
|
|
|
357
343
|
~startBlock=resumedChainState.startBlock,
|
|
358
344
|
~endBlock=resumedChainState.endBlock,
|
|
359
345
|
~config,
|
|
360
|
-
~
|
|
346
|
+
~registrationsByChainId,
|
|
361
347
|
~reorgCheckpoints,
|
|
362
348
|
~maxReorgDepth=resumedChainState.maxReorgDepth,
|
|
363
349
|
~firstEventBlock=resumedChainState.firstEventBlockNumber,
|
|
@@ -377,7 +363,6 @@ let makeFromDbState = (
|
|
|
377
363
|
// --- Read accessors. ---
|
|
378
364
|
|
|
379
365
|
let logger = (cs: t) => cs.logger
|
|
380
|
-
let fetchState = (cs: t) => cs.fetchState
|
|
381
366
|
let sourceManager = (cs: t) => cs.sourceManager
|
|
382
367
|
let chainConfig = (cs: t) => cs.chainConfig
|
|
383
368
|
let reorgDetection = (cs: t) => cs.reorgDetection
|
|
@@ -388,13 +373,27 @@ let numEventsProcessed = (cs: t) => cs.numEventsProcessed
|
|
|
388
373
|
let pendingBudget = (cs: t) => cs.pendingBudget
|
|
389
374
|
let timestampCaughtUpToHeadOrEndblock = (cs: t) => cs.timestampCaughtUpToHeadOrEndblock
|
|
390
375
|
|
|
376
|
+
// Fetch-frontier reads. The FetchState is owned here; callers go through these
|
|
377
|
+
// rather than reaching into it.
|
|
378
|
+
let knownHeight = (cs: t) => cs.fetchState.knownHeight
|
|
379
|
+
let contractAddresses = (cs: t, ~contractName) =>
|
|
380
|
+
cs.indexingAddresses->IndexingAddresses.getContractAddresses(~contractName)
|
|
381
|
+
let bufferSize = (cs: t) => cs.fetchState->FetchState.bufferSize
|
|
382
|
+
let bufferReadyCount = (cs: t) => cs.fetchState->FetchState.bufferReadyCount
|
|
383
|
+
let getProgressPercentage = (cs: t) => cs.fetchState->FetchState.getProgressPercentage
|
|
384
|
+
let chainDensity = (cs: t) => cs.chainDensity
|
|
385
|
+
let hasReadyItem = (cs: t) =>
|
|
386
|
+
cs.fetchState->FetchState.isActivelyIndexing && cs.fetchState->FetchState.hasReadyItem
|
|
387
|
+
let isReadyToEnterReorgThreshold = (cs: t) => cs.fetchState->FetchState.isReadyToEnterReorgThreshold
|
|
388
|
+
|
|
391
389
|
// Mark queries as in flight and reserve their estimated size against the shared
|
|
392
390
|
// buffer budget in one step, so the counter stays in sync with the pending
|
|
393
391
|
// queries it tracks.
|
|
394
392
|
let startFetchingQueries = (cs: t, ~queries: array<FetchState.query>) => {
|
|
395
393
|
cs.fetchState->FetchState.startFetchingQueries(~queries)
|
|
396
394
|
cs.pendingBudget =
|
|
397
|
-
cs.pendingBudget +.
|
|
395
|
+
cs.pendingBudget +.
|
|
396
|
+
queries->Array.reduce(0., (acc, query) => acc +. query.itemsEst->Int.toFloat)
|
|
398
397
|
}
|
|
399
398
|
|
|
400
399
|
// Drop every in-flight query and release their reservations together, keeping
|
|
@@ -404,6 +403,157 @@ let resetPendingQueries = (cs: t) => {
|
|
|
404
403
|
cs.pendingBudget = 0.
|
|
405
404
|
}
|
|
406
405
|
|
|
406
|
+
let isReady = (cs: t) => cs.timestampCaughtUpToHeadOrEndblock !== None
|
|
407
|
+
|
|
408
|
+
// Block span over which a batch fully replaces the chain density estimate;
|
|
409
|
+
// smaller batches blend in proportionally, so a few sparse/dense blocks only
|
|
410
|
+
// nudge it.
|
|
411
|
+
let densityBlendWindow = 100.
|
|
412
|
+
|
|
413
|
+
// The last block this chain can fetch right now: the head, or endBlock when
|
|
414
|
+
// it's below the head.
|
|
415
|
+
let fetchCeiling = (cs: t) => {
|
|
416
|
+
let fetchState = cs.fetchState
|
|
417
|
+
switch fetchState.endBlock {
|
|
418
|
+
| Some(endBlock) => Pervasives.min(endBlock, fetchState.knownHeight)
|
|
419
|
+
| None => fetchState.knownHeight
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Events/block over the ready part of the buffer — a live signal that reacts
|
|
424
|
+
// to what fetching just found, unlike the processing EMA which only moves as
|
|
425
|
+
// batches commit.
|
|
426
|
+
let readyBufferDensity = (cs: t) => {
|
|
427
|
+
let readyCount = cs.fetchState->FetchState.bufferReadyCount
|
|
428
|
+
let span = cs.fetchState->FetchState.bufferBlockNumber - cs.processingBlockNumber
|
|
429
|
+
if readyCount > 0 && span > 0 {
|
|
430
|
+
Some(readyCount->Int.toFloat /. span->Int.toFloat)
|
|
431
|
+
} else {
|
|
432
|
+
None
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Density used for query sizing: the higher of the processing EMA and the
|
|
437
|
+
// ready-buffer density, so a stale-low EMA can't undersize queries while the
|
|
438
|
+
// buffer proves the range is dense. None means the chain is cold — no density
|
|
439
|
+
// signal at all.
|
|
440
|
+
let effectiveDensity = (cs: t) =>
|
|
441
|
+
switch (cs.chainDensity, cs->readyBufferDensity) {
|
|
442
|
+
| (Some(ema), Some(buffer)) => Some(Pervasives.max(ema, buffer))
|
|
443
|
+
| (Some(_) as density, None) | (None, Some(_) as density) => density
|
|
444
|
+
| (None, None) => None
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// How far past the frontier a chain with no density signal targets while it
|
|
448
|
+
// takes its first measurements.
|
|
449
|
+
let coldTargetRange = 20_000
|
|
450
|
+
|
|
451
|
+
// This chain's share of the indexer-wide buffer budget turned into a soft
|
|
452
|
+
// target block: via density, or frontier + coldTargetRange when there's no
|
|
453
|
+
// density signal yet.
|
|
454
|
+
let targetBlock = (cs: t, ~chainTargetItems: float) => {
|
|
455
|
+
let fetchState = cs.fetchState
|
|
456
|
+
let fetchCeiling = cs->fetchCeiling
|
|
457
|
+
let bufferBlockNumber = fetchState->FetchState.bufferBlockNumber
|
|
458
|
+
switch cs->effectiveDensity {
|
|
459
|
+
| Some(density) if density > 0. =>
|
|
460
|
+
Pervasives.min(
|
|
461
|
+
fetchCeiling,
|
|
462
|
+
bufferBlockNumber + Math.ceil(chainTargetItems /. density)->Float.toInt,
|
|
463
|
+
)
|
|
464
|
+
| _ => Pervasives.min(bufferBlockNumber + coldTargetRange, fetchCeiling)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Block range that cross-chain progress alignment maps fractions over: from
|
|
469
|
+
// the first block that can hold this chain's events to the last block it can
|
|
470
|
+
// fetch right now.
|
|
471
|
+
let progressRange = (cs: t) => {
|
|
472
|
+
let fetchState = cs.fetchState
|
|
473
|
+
let lower = fetchState.firstEventBlock->Option.getOr(fetchState.startBlock)
|
|
474
|
+
(lower, cs->fetchCeiling)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// A degenerate range (chain already at or past its last block) maps to 1 so it
|
|
478
|
+
// never constrains the other chains. Clamped at 0 for the initial -1 fetch
|
|
479
|
+
// frontier — the only possible blockNumber below the range's lower bound.
|
|
480
|
+
let progressAtBlock = (cs: t, ~blockNumber) => {
|
|
481
|
+
let (lower, upper) = cs->progressRange
|
|
482
|
+
upper <= lower
|
|
483
|
+
? 1.
|
|
484
|
+
: Pervasives.max(
|
|
485
|
+
0.,
|
|
486
|
+
Pervasives.min(1., (blockNumber - lower)->Int.toFloat /. (upper - lower)->Int.toFloat),
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let blockAtProgress = (cs: t, ~progress) => {
|
|
491
|
+
let (lower, upper) = cs->progressRange
|
|
492
|
+
lower + Math.ceil(progress *. (upper - lower)->Int.toFloat)->Float.toInt
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Propose queries sized against this chain's target block. Called by
|
|
496
|
+
// CrossChainState's waterfall, furthest-behind chain first, with
|
|
497
|
+
// chainTargetItems set to whatever budget remains at that point and
|
|
498
|
+
// maxTargetBlock set to the most-behind chain's progress mapped onto this
|
|
499
|
+
// chain, so a chain with budget can't run further ahead than the chain the
|
|
500
|
+
// whole pool is prioritizing.
|
|
501
|
+
let getNextQuery = (
|
|
502
|
+
cs: t,
|
|
503
|
+
~chainTargetItems: float,
|
|
504
|
+
~chunkItemsMultiplier=1.,
|
|
505
|
+
~maxTargetBlock=?,
|
|
506
|
+
) => {
|
|
507
|
+
let chainTargetBlock = cs->targetBlock(~chainTargetItems)
|
|
508
|
+
let chainTargetBlock = switch maxTargetBlock {
|
|
509
|
+
| Some(maxTargetBlock) => Pervasives.min(chainTargetBlock, maxTargetBlock)
|
|
510
|
+
| None => chainTargetBlock
|
|
511
|
+
}
|
|
512
|
+
// When the target block is clamped (head/endBlock/cross-chain alignment) a
|
|
513
|
+
// known-density chain can't use the whole handed budget — cap the fresh part
|
|
514
|
+
// at what the clamped range actually costs (in-flight reservations stay on
|
|
515
|
+
// top: they're already accounted and shouldn't crowd out new partitions), so
|
|
516
|
+
// the waterfall's leftover flows to the next chain in the same tick instead
|
|
517
|
+
// of being held by an oversized probe.
|
|
518
|
+
let chainTargetItems = switch cs->effectiveDensity {
|
|
519
|
+
| Some(density) if density > 0. =>
|
|
520
|
+
// No extra headroom here: the budget is reserved in honest itemsEst units,
|
|
521
|
+
// and truncation safety lives in the itemsTarget server cap (sized with
|
|
522
|
+
// chunkItemsMultiplier at query creation) — multiplying the budget cap too
|
|
523
|
+
// would compound the two and hold budget away from other chains.
|
|
524
|
+
let rangeCost =
|
|
525
|
+
density *. (chainTargetBlock - cs.fetchState->FetchState.bufferBlockNumber)->Int.toFloat
|
|
526
|
+
Pervasives.min(chainTargetItems, Math.ceil(rangeCost) +. cs.pendingBudget)
|
|
527
|
+
// No density signal: the cross-chain waterfall already clamped the handed
|
|
528
|
+
// budget to the cold-chain cap, so it's used as-is.
|
|
529
|
+
| _ => chainTargetItems
|
|
530
|
+
}
|
|
531
|
+
cs.fetchState->FetchState.getNextQuery(
|
|
532
|
+
~chainTargetBlock,
|
|
533
|
+
~chainTargetItems,
|
|
534
|
+
~chunkItemsMultiplier,
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Run a fetch tick for this chain against its sources, feeding the owned fetch
|
|
539
|
+
// frontier to the source manager.
|
|
540
|
+
let dispatch = (
|
|
541
|
+
cs: t,
|
|
542
|
+
~executeQuery,
|
|
543
|
+
~waitForNewBlock,
|
|
544
|
+
~onNewBlock,
|
|
545
|
+
~action: FetchState.nextQuery,
|
|
546
|
+
~stateId,
|
|
547
|
+
) =>
|
|
548
|
+
cs.sourceManager->SourceManager.dispatch(
|
|
549
|
+
~fetchState=cs.fetchState,
|
|
550
|
+
~executeQuery,
|
|
551
|
+
~waitForNewBlock,
|
|
552
|
+
~onNewBlock,
|
|
553
|
+
~action,
|
|
554
|
+
~stateId,
|
|
555
|
+
)
|
|
556
|
+
|
|
407
557
|
// --- Derived (pure). ---
|
|
408
558
|
|
|
409
559
|
let hasProcessedToEndblock = (cs: t) => {
|
|
@@ -421,16 +571,204 @@ let getHighestBlockBelowThreshold = (cs: t): int => {
|
|
|
421
571
|
|
|
422
572
|
let isActivelyIndexing = (cs: t) => cs.fetchState->FetchState.isActivelyIndexing
|
|
423
573
|
|
|
424
|
-
let isReady = (cs: t) => cs.timestampCaughtUpToHeadOrEndblock !== None
|
|
425
|
-
|
|
426
574
|
// True once the fetch frontier has reached the head/endBlock for this chain.
|
|
427
575
|
let isFetchingAtHead = (cs: t) => cs.fetchState->FetchState.isFetchingAtHead
|
|
428
576
|
|
|
577
|
+
// Reached head on a chain with no configured endBlock — used by auto-exit to
|
|
578
|
+
// detect that no events were found in the start..head range.
|
|
579
|
+
let isAtHeadWithoutEndBlock = (cs: t) =>
|
|
580
|
+
cs.isProgressAtHead && cs.fetchState.endBlock->Option.isNone
|
|
581
|
+
|
|
429
582
|
// --- State transitions. The chain state is mutated only through these; each
|
|
430
583
|
// owns a cohesive update so callers don't juggle individual fields. ---
|
|
431
584
|
|
|
432
|
-
//
|
|
433
|
-
//
|
|
585
|
+
// Per-store grouping data a single pass over `items` produces (see
|
|
586
|
+
// `groupBatchItems`), fed into `TransactionStore`/`BlockStore`'s `materialize`.
|
|
587
|
+
type transactionGroups = {
|
|
588
|
+
txBlockNumbers: array<int>,
|
|
589
|
+
transactionIndices: array<int>,
|
|
590
|
+
transactionMasks: array<float>,
|
|
591
|
+
payloadGroups: array<array<Internal.eventPayload>>,
|
|
592
|
+
anyTransactionFieldSelected: bool,
|
|
593
|
+
}
|
|
594
|
+
type blockGroups = {
|
|
595
|
+
blockBlockNumbers: array<int>,
|
|
596
|
+
blockMasks: array<float>,
|
|
597
|
+
blockItemGroups: array<array<Internal.eventItem>>,
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Walk `items` once to build both the transaction-store and block-store
|
|
601
|
+
// grouping data, instead of `TransactionStore`/`BlockStore` each re-walking the
|
|
602
|
+
// same batch independently. Items already arrive in (blockNumber, logIndex)
|
|
603
|
+
// order, so a (blockNumber, transactionIndex) run and a blockNumber-only run
|
|
604
|
+
// each stay adjacent; every item is checked against both, in the same pass.
|
|
605
|
+
// `includeBlocks` skips the block side entirely for Fuel, which carries the
|
|
606
|
+
// block inline and has no store.
|
|
607
|
+
let groupBatchItems = (items: array<Internal.item>, ~includeBlocks: bool): (
|
|
608
|
+
transactionGroups,
|
|
609
|
+
blockGroups,
|
|
610
|
+
) => {
|
|
611
|
+
let txBlockNumbers = []
|
|
612
|
+
let transactionIndices = []
|
|
613
|
+
let transactionMasks = []
|
|
614
|
+
let payloadGroups = []
|
|
615
|
+
let anyTransactionFieldSelected = ref(false)
|
|
616
|
+
|
|
617
|
+
let blockBlockNumbers = []
|
|
618
|
+
let blockMasks = []
|
|
619
|
+
let blockItemGroups = []
|
|
620
|
+
|
|
621
|
+
items->Array.forEach(item =>
|
|
622
|
+
switch item {
|
|
623
|
+
| Internal.Event(_) =>
|
|
624
|
+
let eventItem = item->Internal.castUnsafeEventItem
|
|
625
|
+
let {blockNumber} = eventItem
|
|
626
|
+
|
|
627
|
+
switch eventItem.payload->Internal.getPayloadTransaction->Nullable.toOption {
|
|
628
|
+
| Some(_) => () // RPC/simulate/Fuel carry the transaction inline.
|
|
629
|
+
| None =>
|
|
630
|
+
let {transactionIndex} = eventItem
|
|
631
|
+
let mask = eventItem.onEventRegistration.eventConfig.transactionFieldMask
|
|
632
|
+
if mask != 0. {
|
|
633
|
+
anyTransactionFieldSelected := true
|
|
634
|
+
}
|
|
635
|
+
let last = payloadGroups->Array.length - 1
|
|
636
|
+
if (
|
|
637
|
+
last >= 0 &&
|
|
638
|
+
txBlockNumbers->Array.getUnsafe(last) == blockNumber &&
|
|
639
|
+
transactionIndices->Array.getUnsafe(last) == transactionIndex
|
|
640
|
+
) {
|
|
641
|
+
payloadGroups->Array.getUnsafe(last)->Array.push(eventItem.payload)
|
|
642
|
+
transactionMasks->Array.setUnsafe(
|
|
643
|
+
last,
|
|
644
|
+
FieldMask.orMask(transactionMasks->Array.getUnsafe(last), mask),
|
|
645
|
+
)
|
|
646
|
+
} else {
|
|
647
|
+
txBlockNumbers->Array.push(blockNumber)
|
|
648
|
+
transactionIndices->Array.push(transactionIndex)
|
|
649
|
+
transactionMasks->Array.push(mask)
|
|
650
|
+
payloadGroups->Array.push([eventItem.payload])
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if includeBlocks {
|
|
655
|
+
switch eventItem.payload->Internal.getPayloadBlock->Nullable.toOption {
|
|
656
|
+
| Some(_) => () // RPC/simulate/Fuel carry the block inline.
|
|
657
|
+
| None =>
|
|
658
|
+
let mask = eventItem.onEventRegistration.eventConfig.blockFieldMask
|
|
659
|
+
let last = blockItemGroups->Array.length - 1
|
|
660
|
+
if last >= 0 && blockBlockNumbers->Array.getUnsafe(last) == blockNumber {
|
|
661
|
+
blockItemGroups->Array.getUnsafe(last)->Array.push(eventItem)
|
|
662
|
+
blockMasks->Array.setUnsafe(
|
|
663
|
+
last,
|
|
664
|
+
FieldMask.orMask(blockMasks->Array.getUnsafe(last), mask),
|
|
665
|
+
)
|
|
666
|
+
} else {
|
|
667
|
+
blockBlockNumbers->Array.push(blockNumber)
|
|
668
|
+
blockMasks->Array.push(mask)
|
|
669
|
+
blockItemGroups->Array.push([eventItem])
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
| Internal.Block(_) => ()
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
(
|
|
678
|
+
{
|
|
679
|
+
txBlockNumbers,
|
|
680
|
+
transactionIndices,
|
|
681
|
+
transactionMasks,
|
|
682
|
+
payloadGroups,
|
|
683
|
+
anyTransactionFieldSelected: anyTransactionFieldSelected.contents,
|
|
684
|
+
},
|
|
685
|
+
{blockBlockNumbers, blockMasks, blockItemGroups},
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Materialise a `TransactionStore` against precomputed groups (see
|
|
690
|
+
// `groupBatchItems`). Store-backed items always get a transaction object — the
|
|
691
|
+
// selected fields, or `{}` when nothing was selected — so `event.transaction`
|
|
692
|
+
// is never `undefined` (matching the inline sources).
|
|
693
|
+
let applyTransactionGroups = async (store: TransactionStore.t, g: transactionGroups) => {
|
|
694
|
+
if g.payloadGroups->Utils.Array.notEmpty {
|
|
695
|
+
if g.anyTransactionFieldSelected {
|
|
696
|
+
let txs = await store->TransactionStore.materialize(
|
|
697
|
+
~blockNumbers=g.txBlockNumbers,
|
|
698
|
+
~transactionIndices=g.transactionIndices,
|
|
699
|
+
~masks=g.transactionMasks,
|
|
700
|
+
)
|
|
701
|
+
g.payloadGroups->Array.forEachWithIndex((payloads, i) => {
|
|
702
|
+
let tx = txs->Array.getUnsafe(i)
|
|
703
|
+
payloads->Array.forEach(payload => payload->Internal.setPayloadTransaction(tx))
|
|
704
|
+
})
|
|
705
|
+
} else {
|
|
706
|
+
g.payloadGroups->Array.forEach(payloads =>
|
|
707
|
+
payloads->Array.forEach(payload => payload->Internal.setPayloadTransaction(%raw(`{}`)))
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Materialise a `BlockStore` against precomputed groups (see `groupBatchItems`).
|
|
714
|
+
let applyBlockGroups = async (store: BlockStore.t, g: blockGroups) => {
|
|
715
|
+
if g.blockItemGroups->Utils.Array.notEmpty {
|
|
716
|
+
let blocks = await store->BlockStore.materialize(
|
|
717
|
+
~blockNumbers=g.blockBlockNumbers,
|
|
718
|
+
~masks=g.blockMasks,
|
|
719
|
+
)
|
|
720
|
+
g.blockItemGroups->Array.forEachWithIndex((group, i) => {
|
|
721
|
+
let block = blocks->Array.getUnsafe(i)
|
|
722
|
+
group->Array.forEach(ei => ei.payload->Internal.setPayloadBlock(block))
|
|
723
|
+
})
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
let includeBlocksForEcosystem = (ecosystem: Ecosystem.name) =>
|
|
728
|
+
switch ecosystem {
|
|
729
|
+
| Evm | Svm => true
|
|
730
|
+
| Fuel => false
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Materialise the chain stores' selected transaction and block fields onto a
|
|
734
|
+
// batch's items at batch prep (the persistent-store path). A single pass over
|
|
735
|
+
// `items` (`groupBatchItems`) builds both stores' selection masks before the
|
|
736
|
+
// two independent materialize calls run concurrently.
|
|
737
|
+
let materializeBatchItems = async (cs: t, ~items: array<Internal.item>, ~ecosystem) => {
|
|
738
|
+
let (txGroups, blockGroups) =
|
|
739
|
+
items->groupBatchItems(~includeBlocks=ecosystem->includeBlocksForEcosystem)
|
|
740
|
+
let _ = await Promise.all2((
|
|
741
|
+
cs.transactionStore->applyTransactionGroups(txGroups),
|
|
742
|
+
cs.blockStore->applyBlockGroups(blockGroups),
|
|
743
|
+
))
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Materialise a fetch-response page's transactions and blocks onto its items
|
|
747
|
+
// before contract-register handlers read them. `None` pages (RPC/Fuel/Simulate
|
|
748
|
+
// keep them inline) are a no-op.
|
|
749
|
+
let materializePageItems = async (
|
|
750
|
+
~items: array<Internal.item>,
|
|
751
|
+
~transactionStore: option<TransactionStore.t>,
|
|
752
|
+
~blockStore: option<BlockStore.t>,
|
|
753
|
+
~ecosystem,
|
|
754
|
+
) => {
|
|
755
|
+
let (txGroups, blockGroups) =
|
|
756
|
+
items->groupBatchItems(~includeBlocks=ecosystem->includeBlocksForEcosystem)
|
|
757
|
+
let _ = await Promise.all2((
|
|
758
|
+
switch transactionStore {
|
|
759
|
+
| Some(store) => store->applyTransactionGroups(txGroups)
|
|
760
|
+
| None => Promise.resolve()
|
|
761
|
+
},
|
|
762
|
+
switch blockStore {
|
|
763
|
+
| Some(store) => store->applyBlockGroups(blockGroups)
|
|
764
|
+
| None => Promise.resolve()
|
|
765
|
+
},
|
|
766
|
+
))
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
let filterByClientAddress = (cs: t, items: array<Internal.item>): array<Internal.item> =>
|
|
770
|
+
items->FetchState.filterByClientAddress(~indexingAddresses=cs.indexingAddresses)
|
|
771
|
+
|
|
434
772
|
let handleQueryResult = (
|
|
435
773
|
cs: t,
|
|
436
774
|
~query: FetchState.query,
|
|
@@ -438,10 +776,27 @@ let handleQueryResult = (
|
|
|
438
776
|
~newItemsWithDcs,
|
|
439
777
|
~latestFetchedBlock,
|
|
440
778
|
~knownHeight,
|
|
779
|
+
~transactionStore as txPage: option<TransactionStore.t>,
|
|
780
|
+
~blockStore as blockPage: option<BlockStore.t>,
|
|
441
781
|
) => {
|
|
782
|
+
// Merge this response's pages into the chain stores in lockstep with appending
|
|
783
|
+
// its items to the buffer. Inline sources contribute no page.
|
|
784
|
+
switch txPage {
|
|
785
|
+
| Some(page) => cs.transactionStore->TransactionStore.merge(page)
|
|
786
|
+
| None => ()
|
|
787
|
+
}
|
|
788
|
+
switch blockPage {
|
|
789
|
+
| Some(page) => cs.blockStore->BlockStore.merge(page)
|
|
790
|
+
| None => ()
|
|
791
|
+
}
|
|
792
|
+
|
|
442
793
|
let fs = switch newItemsWithDcs {
|
|
443
794
|
| [] => cs.fetchState
|
|
444
|
-
| _ =>
|
|
795
|
+
| _ =>
|
|
796
|
+
cs.fetchState->FetchState.registerDynamicContracts(
|
|
797
|
+
~indexingAddresses=cs.indexingAddresses,
|
|
798
|
+
newItemsWithDcs,
|
|
799
|
+
)
|
|
445
800
|
}
|
|
446
801
|
|
|
447
802
|
cs.fetchState =
|
|
@@ -450,7 +805,7 @@ let handleQueryResult = (
|
|
|
450
805
|
->FetchState.updateKnownHeight(~knownHeight)
|
|
451
806
|
|
|
452
807
|
// The query is no longer in flight, so release its reservation.
|
|
453
|
-
cs.pendingBudget = Pervasives.max(0., cs.pendingBudget -. query.
|
|
808
|
+
cs.pendingBudget = Pervasives.max(0., cs.pendingBudget -. query.itemsEst->Int.toFloat)
|
|
454
809
|
}
|
|
455
810
|
|
|
456
811
|
// Run reorg detection against a fetch response and commit the updated guard.
|
|
@@ -491,6 +846,56 @@ let setEndBlockToFirstEvent = (cs: t, ~blockNumber) =>
|
|
|
491
846
|
let enterReorgThreshold = (cs: t) =>
|
|
492
847
|
cs.fetchState = cs.fetchState->FetchState.updateInternal(~blockLag=cs.chainConfig.blockLag)
|
|
493
848
|
|
|
849
|
+
// Snapshot the chain's metadata fields for staging into the chains table.
|
|
850
|
+
let toChainMetadata = (cs: t): InternalTable.Chains.metaFields => {
|
|
851
|
+
firstEventBlockNumber: cs.fetchState.firstEventBlock->Null.fromOption,
|
|
852
|
+
isHyperSync: (cs.sourceManager->SourceManager.getActiveSource).poweredByHyperSync,
|
|
853
|
+
latestFetchedBlockNumber: cs.fetchState->FetchState.bufferBlockNumber,
|
|
854
|
+
timestampCaughtUpToHeadOrEndblock: cs.timestampCaughtUpToHeadOrEndblock->Null.fromOption,
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Snapshot the chain's view for the status API.
|
|
858
|
+
let toChainData = (cs: t): chainData => {
|
|
859
|
+
chainId: cs.chainConfig.id->Int.toFloat,
|
|
860
|
+
poweredByHyperSync: (cs.sourceManager->SourceManager.getActiveSource).poweredByHyperSync,
|
|
861
|
+
firstEventBlockNumber: cs.fetchState.firstEventBlock,
|
|
862
|
+
latestProcessedBlock: cs.committedProgressBlockNumber === -1
|
|
863
|
+
? None
|
|
864
|
+
: Some(cs.committedProgressBlockNumber),
|
|
865
|
+
timestampCaughtUpToHeadOrEndblock: cs.timestampCaughtUpToHeadOrEndblock,
|
|
866
|
+
numEventsProcessed: cs.numEventsProcessed,
|
|
867
|
+
latestFetchedBlockNumber: Pervasives.max(cs.fetchState->FetchState.bufferBlockNumber, 0),
|
|
868
|
+
knownHeight: cs->hasProcessedToEndblock
|
|
869
|
+
? cs.fetchState.endBlock->Option.getOr(cs.fetchState.knownHeight)
|
|
870
|
+
: cs.fetchState.knownHeight,
|
|
871
|
+
numBatchesFetched: 0,
|
|
872
|
+
startBlock: cs.fetchState.startBlock,
|
|
873
|
+
endBlock: cs.fetchState.endBlock,
|
|
874
|
+
numAddresses: cs.indexingAddresses->IndexingAddresses.size,
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Snapshot the inputs a batch build needs from this chain.
|
|
878
|
+
let toChainBeforeBatch = (cs: t): Batch.chainBeforeBatch => {
|
|
879
|
+
fetchState: cs.fetchState,
|
|
880
|
+
progressBlockNumber: cs.committedProgressBlockNumber,
|
|
881
|
+
totalEventsProcessed: cs.numEventsProcessed,
|
|
882
|
+
sourceBlockNumber: cs.fetchState.knownHeight,
|
|
883
|
+
reorgDetection: cs.reorgDetection,
|
|
884
|
+
chainConfig: cs.chainConfig,
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Whether the chain's post-batch fetch frontier is ready to cross into the reorg
|
|
888
|
+
// threshold, using the batch's progressed frontier when this chain advanced.
|
|
889
|
+
let isReadyToEnterReorgThresholdAfterBatch = (cs: t, ~batch: Batch.t) => {
|
|
890
|
+
let fetchState = switch batch.progressedChainsById->Utils.Dict.dangerouslyGetByIntNonOption(
|
|
891
|
+
cs.fetchState.chainId,
|
|
892
|
+
) {
|
|
893
|
+
| Some(chainAfterBatch) => chainAfterBatch.fetchState
|
|
894
|
+
| None => cs.fetchState
|
|
895
|
+
}
|
|
896
|
+
fetchState->FetchState.isReadyToEnterReorgThreshold
|
|
897
|
+
}
|
|
898
|
+
|
|
494
899
|
// Commit the post-batch fetch frontier for a chain that progressed in the batch,
|
|
495
900
|
// applying blockLag when this batch also crosses into the reorg threshold.
|
|
496
901
|
let advanceAfterBatch = (cs: t, ~batch: Batch.t, ~enteringReorgThreshold) =>
|
|
@@ -501,14 +906,19 @@ let advanceAfterBatch = (cs: t, ~batch: Batch.t, ~enteringReorgThreshold) =>
|
|
|
501
906
|
cs.fetchState = enteringReorgThreshold
|
|
502
907
|
? chainAfterBatch.fetchState->FetchState.updateInternal(~blockLag=cs.chainConfig.blockLag)
|
|
503
908
|
: chainAfterBatch.fetchState
|
|
909
|
+
|
|
910
|
+
// The batch's items just left the buffer, so the remaining buffer's span
|
|
911
|
+
// starts at the batch's progress.
|
|
912
|
+
cs.processingBlockNumber = chainAfterBatch.progressBlockNumber
|
|
504
913
|
| None => ()
|
|
505
914
|
}
|
|
506
915
|
|
|
507
916
|
// Commit a processed batch's progress for this chain (progress block, events
|
|
508
917
|
// processed, head/safe-checkpoint tracking, first event block). Emits the
|
|
509
918
|
// per-chain progress metrics. Readiness is decided by CrossChainState once every
|
|
510
|
-
// chain is caught up (see markReady).
|
|
511
|
-
|
|
919
|
+
// chain is caught up (see markReady). `blockTimestampName` is the ecosystem's
|
|
920
|
+
// block-timestamp field, read off the payload block for the latency metric.
|
|
921
|
+
let applyBatchProgress = (cs: t, ~batch: Batch.t, ~blockTimestampName: string) => {
|
|
512
922
|
let chainId = cs.chainConfig.id
|
|
513
923
|
|
|
514
924
|
switch batch.progressedChainsById->Utils.Dict.dangerouslyGetByIntNonOption(chainId) {
|
|
@@ -526,15 +936,27 @@ let applyBatchProgress = (cs: t, ~batch: Batch.t) => {
|
|
|
526
936
|
)
|
|
527
937
|
}
|
|
528
938
|
|
|
529
|
-
// Calculate and set latency metrics
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
939
|
+
// Calculate and set latency metrics. The payload block is materialised or
|
|
940
|
+
// inline by processing time; its timestamp may still be absent (e.g. an
|
|
941
|
+
// SVM slot with no block row) — the metric is skipped then.
|
|
942
|
+
switch batch
|
|
943
|
+
->Batch.findLastEventItem(~chainId)
|
|
944
|
+
->Option.flatMap(eventItem =>
|
|
945
|
+
eventItem.payload
|
|
946
|
+
->Internal.getPayloadBlock
|
|
947
|
+
->Nullable.toOption
|
|
948
|
+
)
|
|
949
|
+
->Option.flatMap(block =>
|
|
950
|
+
block
|
|
951
|
+
->(Utils.magic: Internal.eventBlock => dict<unknown>)
|
|
952
|
+
->Utils.Dict.dangerouslyGetNonOption(blockTimestampName)
|
|
953
|
+
) {
|
|
954
|
+
| Some(blockTimestamp) =>
|
|
955
|
+
let blockTimestampMs = blockTimestamp->(Utils.magic: unknown => int) * 1000
|
|
956
|
+
Prometheus.ProgressLatency.set(
|
|
957
|
+
~latencyMs=Date.now()->Float.toInt - blockTimestampMs,
|
|
958
|
+
~chainId,
|
|
959
|
+
)
|
|
538
960
|
| None => ()
|
|
539
961
|
}
|
|
540
962
|
|
|
@@ -549,8 +971,42 @@ let applyBatchProgress = (cs: t, ~batch: Batch.t) => {
|
|
|
549
971
|
}
|
|
550
972
|
}
|
|
551
973
|
|
|
974
|
+
// Chain-wide density update: seed with the batch's own events/block on
|
|
975
|
+
// the first update, then blend weighted by the batch's block span — a
|
|
976
|
+
// few sparse/dense blocks barely nudge the estimate, while a
|
|
977
|
+
// window-sized batch replaces it.
|
|
978
|
+
let deltaBlocks = chainAfterBatch.progressBlockNumber - cs.committedProgressBlockNumber
|
|
979
|
+
if deltaBlocks > 0 {
|
|
980
|
+
let deltaEvents = chainAfterBatch.totalEventsProcessed -. cs.numEventsProcessed
|
|
981
|
+
// Don't seed a density before the first event is seen — a progress-only
|
|
982
|
+
// batch would otherwise set it to 0, matching the resume-seed guard.
|
|
983
|
+
switch (cs.chainDensity, deltaEvents > 0.) {
|
|
984
|
+
| (None, false) => ()
|
|
985
|
+
| _ =>
|
|
986
|
+
let batchDensity = deltaEvents /. deltaBlocks->Int.toFloat
|
|
987
|
+
cs.chainDensity = Some(
|
|
988
|
+
switch cs.chainDensity {
|
|
989
|
+
| None => batchDensity
|
|
990
|
+
| Some(oldDensity) =>
|
|
991
|
+
let alpha = Pervasives.min(1., deltaBlocks->Int.toFloat /. densityBlendWindow)
|
|
992
|
+
oldDensity *. (1. -. alpha) +. batchDensity *. alpha
|
|
993
|
+
},
|
|
994
|
+
)
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
552
998
|
cs.committedProgressBlockNumber = chainAfterBatch.progressBlockNumber
|
|
999
|
+
|
|
1000
|
+
// Normally already set by advanceAfterBatch at batch creation; catch up
|
|
1001
|
+
// here for paths that commit progress without it.
|
|
1002
|
+
cs.processingBlockNumber = Pervasives.max(
|
|
1003
|
+
cs.processingBlockNumber,
|
|
1004
|
+
chainAfterBatch.progressBlockNumber,
|
|
1005
|
+
)
|
|
553
1006
|
cs.numEventsProcessed = chainAfterBatch.totalEventsProcessed
|
|
1007
|
+
// Processed blocks' transactions and blocks are no longer needed.
|
|
1008
|
+
cs.transactionStore->TransactionStore.prune(chainAfterBatch.progressBlockNumber)
|
|
1009
|
+
cs.blockStore->BlockStore.prune(chainAfterBatch.progressBlockNumber)
|
|
554
1010
|
cs.isProgressAtHead = cs.isProgressAtHead || chainAfterBatch.isProgressAtHeadWhenBatchCreated
|
|
555
1011
|
switch cs.safeCheckpointTracking {
|
|
556
1012
|
| Some(safeCheckpointTracking) =>
|
|
@@ -623,8 +1079,15 @@ let rollback = (
|
|
|
623
1079
|
)
|
|
624
1080
|
| None => ()
|
|
625
1081
|
}
|
|
626
|
-
cs.fetchState =
|
|
1082
|
+
cs.fetchState =
|
|
1083
|
+
cs.fetchState->FetchState.rollback(
|
|
1084
|
+
~indexingAddresses=cs.indexingAddresses,
|
|
1085
|
+
~targetBlockNumber=newProgressBlockNumber,
|
|
1086
|
+
)
|
|
1087
|
+
cs.transactionStore->TransactionStore.rollback(newProgressBlockNumber)
|
|
1088
|
+
cs.blockStore->BlockStore.rollback(newProgressBlockNumber)
|
|
627
1089
|
cs.committedProgressBlockNumber = newProgressBlockNumber
|
|
1090
|
+
cs.processingBlockNumber = newProgressBlockNumber
|
|
628
1091
|
cs.numEventsProcessed = newTotalEventsProcessed
|
|
629
1092
|
| None =>
|
|
630
1093
|
if isReorgChain {
|
|
@@ -633,11 +1096,17 @@ let rollback = (
|
|
|
633
1096
|
~blockNumber=rollbackTargetBlockNumber,
|
|
634
1097
|
)
|
|
635
1098
|
cs.fetchState =
|
|
636
|
-
cs.fetchState->FetchState.rollback(
|
|
1099
|
+
cs.fetchState->FetchState.rollback(
|
|
1100
|
+
~indexingAddresses=cs.indexingAddresses,
|
|
1101
|
+
~targetBlockNumber=rollbackTargetBlockNumber,
|
|
1102
|
+
)
|
|
1103
|
+
cs.transactionStore->TransactionStore.rollback(rollbackTargetBlockNumber)
|
|
1104
|
+
cs.blockStore->BlockStore.rollback(rollbackTargetBlockNumber)
|
|
637
1105
|
cs.committedProgressBlockNumber = Pervasives.min(
|
|
638
1106
|
cs.committedProgressBlockNumber,
|
|
639
1107
|
rollbackTargetBlockNumber,
|
|
640
1108
|
)
|
|
1109
|
+
cs.processingBlockNumber = Pervasives.min(cs.processingBlockNumber, rollbackTargetBlockNumber)
|
|
641
1110
|
}
|
|
642
1111
|
}
|
|
643
1112
|
}
|