envio 3.0.0-alpha.21 → 3.0.0-alpha.23
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/README.md +3 -3
- package/bin.mjs +2 -48
- package/evm.schema.json +67 -0
- package/fuel.schema.json +67 -0
- package/index.d.ts +822 -38
- package/index.js +5 -3
- package/package.json +10 -8
- package/rescript.json +5 -9
- package/src/Address.res +4 -5
- package/src/Address.res.mjs +9 -12
- package/src/Api.res +15 -0
- package/src/Api.res.mjs +20 -0
- package/src/Batch.res +32 -34
- package/src/Batch.res.mjs +172 -187
- package/src/Bin.res +89 -0
- package/src/Bin.res.mjs +97 -0
- package/src/ChainFetcher.res +33 -57
- package/src/ChainFetcher.res.mjs +197 -227
- package/src/ChainManager.res +6 -14
- package/src/ChainManager.res.mjs +74 -85
- package/src/ChainMap.res +14 -16
- package/src/ChainMap.res.mjs +38 -38
- package/src/Config.res +193 -135
- package/src/Config.res.mjs +566 -592
- package/src/Core.res +182 -0
- package/src/Core.res.mjs +207 -0
- package/src/Ecosystem.res +25 -4
- package/src/Ecosystem.res.mjs +12 -13
- package/src/Env.res +20 -13
- package/src/Env.res.mjs +124 -113
- package/src/EnvSafe.res +269 -0
- package/src/EnvSafe.res.mjs +296 -0
- package/src/EnvSafe.resi +18 -0
- package/src/Envio.res +37 -26
- package/src/Envio.res.mjs +59 -60
- package/src/ErrorHandling.res +2 -2
- package/src/ErrorHandling.res.mjs +15 -15
- package/src/EventConfigBuilder.res +219 -81
- package/src/EventConfigBuilder.res.mjs +259 -202
- package/src/EventProcessing.res +27 -38
- package/src/EventProcessing.res.mjs +165 -183
- package/src/EventUtils.res +11 -11
- package/src/EventUtils.res.mjs +21 -22
- package/src/EvmTypes.res +0 -1
- package/src/EvmTypes.res.mjs +5 -5
- package/src/FetchState.res +360 -256
- package/src/FetchState.res.mjs +958 -914
- package/src/GlobalState.res +365 -351
- package/src/GlobalState.res.mjs +958 -992
- package/src/GlobalStateManager.res +1 -2
- package/src/GlobalStateManager.res.mjs +36 -44
- package/src/HandlerLoader.res +107 -23
- package/src/HandlerLoader.res.mjs +128 -38
- package/src/HandlerRegister.res +127 -103
- package/src/HandlerRegister.res.mjs +164 -164
- package/src/HandlerRegister.resi +12 -4
- package/src/Hasura.res +35 -22
- package/src/Hasura.res.mjs +158 -167
- package/src/InMemoryStore.res +20 -27
- package/src/InMemoryStore.res.mjs +64 -80
- package/src/InMemoryTable.res +34 -39
- package/src/InMemoryTable.res.mjs +165 -170
- package/src/Internal.res +52 -33
- package/src/Internal.res.mjs +84 -81
- package/src/LazyLoader.res.mjs +55 -61
- package/src/LoadLayer.res +77 -78
- package/src/LoadLayer.res.mjs +160 -189
- package/src/LoadManager.res +16 -21
- package/src/LoadManager.res.mjs +79 -84
- package/src/LogSelection.res +236 -68
- package/src/LogSelection.res.mjs +211 -141
- package/src/Logging.res +13 -9
- package/src/Logging.res.mjs +130 -143
- package/src/Main.res +430 -51
- package/src/Main.res.mjs +530 -271
- package/src/Persistence.res +80 -84
- package/src/Persistence.res.mjs +131 -132
- package/src/PgStorage.res +294 -167
- package/src/PgStorage.res.mjs +799 -817
- package/src/Prometheus.res +50 -58
- package/src/Prometheus.res.mjs +345 -373
- package/src/ReorgDetection.res +22 -24
- package/src/ReorgDetection.res.mjs +100 -106
- package/src/SafeCheckpointTracking.res +7 -7
- package/src/SafeCheckpointTracking.res.mjs +40 -43
- package/src/SimulateItems.res +41 -49
- package/src/SimulateItems.res.mjs +257 -272
- package/src/Sink.res +2 -2
- package/src/Sink.res.mjs +22 -26
- package/src/TableIndices.res +1 -2
- package/src/TableIndices.res.mjs +42 -48
- package/src/TestIndexer.res +196 -189
- package/src/TestIndexer.res.mjs +536 -536
- package/src/TestIndexerProxyStorage.res +16 -16
- package/src/TestIndexerProxyStorage.res.mjs +99 -122
- package/src/TestIndexerWorker.res +4 -0
- package/src/TestIndexerWorker.res.mjs +7 -0
- package/src/Throttler.res +3 -3
- package/src/Throttler.res.mjs +23 -24
- package/src/Time.res +1 -1
- package/src/Time.res.mjs +18 -21
- package/src/TopicFilter.res +3 -3
- package/src/TopicFilter.res.mjs +29 -30
- package/src/UserContext.res +93 -54
- package/src/UserContext.res.mjs +197 -182
- package/src/Utils.res +141 -86
- package/src/Utils.res.mjs +334 -295
- package/src/bindings/BigDecimal.res +0 -2
- package/src/bindings/BigDecimal.res.mjs +19 -23
- package/src/bindings/ClickHouse.res +28 -27
- package/src/bindings/ClickHouse.res.mjs +243 -240
- package/src/bindings/DateFns.res +11 -11
- package/src/bindings/DateFns.res.mjs +7 -7
- package/src/bindings/EventSource.res.mjs +2 -2
- package/src/bindings/Express.res +2 -5
- package/src/bindings/Hrtime.res +2 -2
- package/src/bindings/Hrtime.res.mjs +30 -32
- package/src/bindings/Lodash.res.mjs +1 -1
- package/src/bindings/NodeJs.res +14 -9
- package/src/bindings/NodeJs.res.mjs +20 -20
- package/src/bindings/Pino.res +8 -10
- package/src/bindings/Pino.res.mjs +40 -43
- package/src/bindings/Postgres.res +7 -5
- package/src/bindings/Postgres.res.mjs +9 -9
- package/src/bindings/PromClient.res +17 -2
- package/src/bindings/PromClient.res.mjs +30 -7
- package/src/bindings/SDSL.res.mjs +2 -2
- package/src/bindings/Viem.res +4 -4
- package/src/bindings/Viem.res.mjs +20 -22
- package/src/bindings/Vitest.res +1 -1
- package/src/bindings/Vitest.res.mjs +2 -2
- package/src/bindings/WebSocket.res +1 -1
- package/src/db/EntityHistory.res +9 -3
- package/src/db/EntityHistory.res.mjs +84 -59
- package/src/db/InternalTable.res +62 -60
- package/src/db/InternalTable.res.mjs +271 -203
- package/src/db/Schema.res +1 -2
- package/src/db/Schema.res.mjs +28 -32
- package/src/db/Table.res +28 -27
- package/src/db/Table.res.mjs +276 -292
- package/src/sources/EventRouter.res +21 -16
- package/src/sources/EventRouter.res.mjs +55 -57
- package/src/sources/Evm.res +17 -1
- package/src/sources/Evm.res.mjs +16 -8
- package/src/sources/EvmChain.res +15 -17
- package/src/sources/EvmChain.res.mjs +40 -42
- package/src/sources/Fuel.res +14 -1
- package/src/sources/Fuel.res.mjs +16 -8
- package/src/sources/FuelSDK.res +1 -1
- package/src/sources/FuelSDK.res.mjs +6 -8
- package/src/sources/HyperFuel.res +8 -10
- package/src/sources/HyperFuel.res.mjs +113 -123
- package/src/sources/HyperFuelClient.res.mjs +6 -7
- package/src/sources/HyperFuelSource.res +19 -20
- package/src/sources/HyperFuelSource.res.mjs +339 -356
- package/src/sources/HyperSync.res +11 -13
- package/src/sources/HyperSync.res.mjs +206 -220
- package/src/sources/HyperSyncClient.res +5 -7
- package/src/sources/HyperSyncClient.res.mjs +70 -75
- package/src/sources/HyperSyncHeightStream.res +8 -9
- package/src/sources/HyperSyncHeightStream.res.mjs +78 -86
- package/src/sources/HyperSyncJsonApi.res +18 -15
- package/src/sources/HyperSyncJsonApi.res.mjs +201 -231
- package/src/sources/HyperSyncSource.res +17 -21
- package/src/sources/HyperSyncSource.res.mjs +268 -290
- package/src/sources/Rpc.res +5 -5
- package/src/sources/Rpc.res.mjs +168 -192
- package/src/sources/RpcSource.res +166 -167
- package/src/sources/RpcSource.res.mjs +972 -1046
- package/src/sources/RpcWebSocketHeightStream.res +10 -11
- package/src/sources/RpcWebSocketHeightStream.res.mjs +131 -145
- package/src/sources/SimulateSource.res +1 -1
- package/src/sources/SimulateSource.res.mjs +35 -38
- package/src/sources/Source.res +1 -1
- package/src/sources/Source.res.mjs +3 -3
- package/src/sources/SourceManager.res +39 -20
- package/src/sources/SourceManager.res.mjs +340 -371
- package/src/sources/SourceManager.resi +2 -1
- package/src/sources/Svm.res +12 -5
- package/src/sources/Svm.res.mjs +44 -41
- package/src/tui/Tui.res +23 -12
- package/src/tui/Tui.res.mjs +292 -290
- package/src/tui/bindings/Ink.res +2 -4
- package/src/tui/bindings/Ink.res.mjs +35 -41
- package/src/tui/components/BufferedProgressBar.res +7 -7
- package/src/tui/components/BufferedProgressBar.res.mjs +46 -46
- package/src/tui/components/CustomHooks.res +1 -2
- package/src/tui/components/CustomHooks.res.mjs +102 -122
- package/src/tui/components/Messages.res +1 -2
- package/src/tui/components/Messages.res.mjs +38 -42
- package/src/tui/components/SyncETA.res +10 -11
- package/src/tui/components/SyncETA.res.mjs +178 -196
- package/src/tui/components/TuiData.res +1 -1
- package/src/tui/components/TuiData.res.mjs +7 -6
- package/src/vendored/Rest.res +52 -66
- package/src/vendored/Rest.res.mjs +324 -364
- package/svm.schema.json +67 -0
- package/src/Address.gen.ts +0 -8
- package/src/Config.gen.ts +0 -19
- package/src/Envio.gen.ts +0 -55
- package/src/EvmTypes.gen.ts +0 -6
- package/src/InMemoryStore.gen.ts +0 -6
- package/src/Internal.gen.ts +0 -64
- package/src/PgStorage.gen.ts +0 -10
- package/src/PgStorage.res.d.mts +0 -5
- package/src/Types.ts +0 -56
- package/src/bindings/BigDecimal.gen.ts +0 -14
- package/src/bindings/BigDecimal.res.d.mts +0 -5
- package/src/bindings/BigInt.gen.ts +0 -10
- package/src/bindings/BigInt.res +0 -70
- package/src/bindings/BigInt.res.d.mts +0 -5
- package/src/bindings/BigInt.res.mjs +0 -154
- package/src/bindings/Ethers.res.d.mts +0 -5
- package/src/bindings/Pino.gen.ts +0 -17
- package/src/bindings/Postgres.gen.ts +0 -8
- package/src/bindings/Postgres.res.d.mts +0 -5
- package/src/bindings/Promise.res +0 -67
- package/src/bindings/Promise.res.mjs +0 -26
- package/src/db/InternalTable.gen.ts +0 -36
- package/src/sources/HyperSyncClient.gen.ts +0 -19
package/src/Main.res
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
open Belt
|
|
2
|
-
|
|
3
1
|
type chainData = {
|
|
4
2
|
chainId: float,
|
|
5
3
|
poweredByHyperSync: bool,
|
|
6
4
|
firstEventBlockNumber: option<int>,
|
|
7
5
|
latestProcessedBlock: option<int>,
|
|
8
|
-
timestampCaughtUpToHeadOrEndblock: option<
|
|
6
|
+
timestampCaughtUpToHeadOrEndblock: option<Date.t>,
|
|
9
7
|
numEventsProcessed: float,
|
|
10
8
|
latestFetchedBlockNumber: int,
|
|
11
9
|
// Need this for API backwards compatibility
|
|
@@ -24,7 +22,7 @@ type state =
|
|
|
24
22
|
Active({
|
|
25
23
|
envioVersion: string,
|
|
26
24
|
chains: array<chainData>,
|
|
27
|
-
indexerStartTime:
|
|
25
|
+
indexerStartTime: Date.t,
|
|
28
26
|
isPreRegisteringDynamicContracts: bool,
|
|
29
27
|
isUnorderedMultichainMode: bool,
|
|
30
28
|
rollbackOnReorg: bool,
|
|
@@ -58,40 +56,90 @@ let stateSchema = S.union([
|
|
|
58
56
|
})),
|
|
59
57
|
])
|
|
60
58
|
|
|
61
|
-
|
|
59
|
+
// Shape of the user-returned `{_gte?, _lte?, _every?}` filter chunk after
|
|
60
|
+
// the ecosystem-specific wrapper is stripped. Shared across all ecosystems —
|
|
61
|
+
// the outer `block.number` / `block.height` / `slot` unwrap lives on each
|
|
62
|
+
// ecosystem's `onBlockFilterSchema`, and the inner range fields are the
|
|
63
|
+
// same everywhere.
|
|
64
|
+
type blockRange = {
|
|
65
|
+
_gte: option<int>,
|
|
66
|
+
_lte: option<int>,
|
|
67
|
+
_every: int,
|
|
68
|
+
}
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
// `S.strict` rejects unknown fields so typos like `_gt` / `_evry` surface
|
|
71
|
+
// with a readable schema error pointing at the offending key, instead of
|
|
72
|
+
// silently registering a broken filter. `_every` defaults to 1 inside the
|
|
73
|
+
// schema so the caller always sees a plain `int`, and `intMin(1)` rejects
|
|
74
|
+
// zero/negative strides — `(blockNumber - startBlock) % 0` would crash and
|
|
75
|
+
// any negative stride would never match.
|
|
76
|
+
let blockRangeSchema: S.t<blockRange> = S.object(s => {
|
|
77
|
+
_gte: s.field("_gte", S.option(S.int)),
|
|
78
|
+
_lte: s.field("_lte", S.option(S.int)),
|
|
79
|
+
_every: s.field("_every", S.option(S.int->S.intMin(1))->S.Option.getOr(1)),
|
|
80
|
+
})->S.strict
|
|
65
81
|
|
|
66
|
-
|
|
67
|
-
->Utils.Object.definePropertyWithValue("name", {enumerable: true, value: config.name})
|
|
68
|
-
->Utils.Object.definePropertyWithValue(
|
|
69
|
-
"description",
|
|
70
|
-
{enumerable: true, value: config.description},
|
|
71
|
-
)
|
|
72
|
-
->ignore
|
|
82
|
+
let defaultBlockRange: blockRange = {_gte: None, _lte: None, _every: 1}
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
let globalGsManagerRef: ref<option<GlobalStateManager.t>> = ref(None)
|
|
75
85
|
|
|
76
|
-
|
|
86
|
+
// Persistence is set by Main.start before handler modules load, so that
|
|
87
|
+
// the exported indexer value can lazily expose DB state (startBlock,
|
|
88
|
+
// endBlock, isLive, dynamic contract addresses) once it's ready.
|
|
89
|
+
let globalPersistenceRef: ref<option<Persistence.t>> = ref(None)
|
|
90
|
+
|
|
91
|
+
let getInitialChainState = (~chainId: int): option<Persistence.initialChainState> => {
|
|
92
|
+
switch globalPersistenceRef.contents {
|
|
93
|
+
| Some(persistence) =>
|
|
94
|
+
switch persistence.storageStatus {
|
|
95
|
+
| Ready(initialState) => initialState.chains->Array.find(c => c.id === chainId)
|
|
96
|
+
| _ => None
|
|
97
|
+
}
|
|
98
|
+
| None => None
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Importing `generated` must not trigger `Config.loadWithoutRegistrations()`,
|
|
103
|
+
// so the exported indexer calls this lazily on first `indexer.chains` access.
|
|
104
|
+
let buildChainsObject = (~config: Config.t) => {
|
|
105
|
+
let chainIds = []
|
|
77
106
|
let chains = Utils.Object.createNullObject()
|
|
78
107
|
config.chainMap
|
|
79
108
|
->ChainMap.values
|
|
80
109
|
->Array.forEach(chainConfig => {
|
|
81
110
|
let chainIdStr = chainConfig.id->Int.toString
|
|
82
111
|
|
|
83
|
-
chainIds->
|
|
112
|
+
chainIds->Array.push(chainConfig.id)->ignore
|
|
84
113
|
|
|
85
114
|
let chainObj = Utils.Object.createNullObject()
|
|
86
115
|
chainObj
|
|
87
116
|
->Utils.Object.definePropertyWithValue("id", {enumerable: true, value: chainConfig.id})
|
|
88
|
-
->Utils.Object.
|
|
117
|
+
->Utils.Object.defineProperty(
|
|
89
118
|
"startBlock",
|
|
90
|
-
{
|
|
119
|
+
{
|
|
120
|
+
enumerable: true,
|
|
121
|
+
get: () => {
|
|
122
|
+
switch getInitialChainState(~chainId=chainConfig.id) {
|
|
123
|
+
| Some(chainState) => chainState.startBlock
|
|
124
|
+
| None => chainConfig.startBlock
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
},
|
|
91
128
|
)
|
|
92
|
-
->Utils.Object.
|
|
129
|
+
->Utils.Object.defineProperty(
|
|
93
130
|
"endBlock",
|
|
94
|
-
{
|
|
131
|
+
{
|
|
132
|
+
enumerable: true,
|
|
133
|
+
get: () => {
|
|
134
|
+
// Persistence may store endBlock=None (eg the test indexer's
|
|
135
|
+
// auto-exit mode where the user didn't specify an endBlock).
|
|
136
|
+
// Only override the config when persistence has an explicit value.
|
|
137
|
+
switch getInitialChainState(~chainId=chainConfig.id) {
|
|
138
|
+
| Some({endBlock: Some(_) as eb}) => eb
|
|
139
|
+
| _ => chainConfig.endBlock
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
},
|
|
95
143
|
)
|
|
96
144
|
->Utils.Object.definePropertyWithValue("name", {enumerable: true, value: chainConfig.name})
|
|
97
145
|
->Utils.Object.defineProperty(
|
|
@@ -100,12 +148,20 @@ let getGlobalIndexer = (~config: Config.t): 'indexer => {
|
|
|
100
148
|
enumerable: true,
|
|
101
149
|
get: () => {
|
|
102
150
|
switch globalGsManagerRef.contents {
|
|
103
|
-
| None => false
|
|
104
151
|
| Some(gsManager) =>
|
|
105
152
|
let state = gsManager->GlobalStateManager.getState
|
|
106
153
|
let chain = ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id)
|
|
107
154
|
let chainFetcher = state.chainManager.chainFetchers->ChainMap.get(chain)
|
|
108
155
|
chainFetcher->ChainFetcher.isReady
|
|
156
|
+
// Before the GlobalStateManager is available (eg during handler
|
|
157
|
+
// module load after resume), derive liveness from persistence:
|
|
158
|
+
// a chain is considered live when it previously caught up to head
|
|
159
|
+
// or endBlock (timestampCaughtUpToHeadOrEndblock is set).
|
|
160
|
+
| None =>
|
|
161
|
+
switch getInitialChainState(~chainId=chainConfig.id) {
|
|
162
|
+
| Some(chainState) => chainState.timestampCaughtUpToHeadOrEndblock->Option.isSome
|
|
163
|
+
| None => false
|
|
164
|
+
}
|
|
109
165
|
}
|
|
110
166
|
},
|
|
111
167
|
},
|
|
@@ -124,24 +180,40 @@ let getGlobalIndexer = (~config: Config.t): 'indexer => {
|
|
|
124
180
|
enumerable: true,
|
|
125
181
|
get: () => {
|
|
126
182
|
switch globalGsManagerRef.contents {
|
|
127
|
-
| None => contract.addresses
|
|
128
183
|
| Some(gsManager) => {
|
|
129
184
|
let state = gsManager->GlobalStateManager.getState
|
|
130
185
|
let chain = ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id)
|
|
131
186
|
let chainFetcher = state.chainManager.chainFetchers->ChainMap.get(chain)
|
|
132
|
-
let
|
|
187
|
+
let indexingAddresses = chainFetcher.fetchState.indexingAddresses
|
|
133
188
|
|
|
134
|
-
// Collect all addresses for this contract name from
|
|
189
|
+
// Collect all addresses for this contract name from indexingAddresses
|
|
135
190
|
let addresses = []
|
|
136
|
-
let values =
|
|
191
|
+
let values = indexingAddresses->Dict.valuesToArray
|
|
137
192
|
for idx in 0 to values->Array.length - 1 {
|
|
138
|
-
let indexingContract = values->
|
|
193
|
+
let indexingContract = values->Array.getUnsafe(idx)
|
|
139
194
|
if indexingContract.contractName === contract.name {
|
|
140
195
|
addresses->Array.push(indexingContract.address)->ignore
|
|
141
196
|
}
|
|
142
197
|
}
|
|
143
198
|
addresses
|
|
144
199
|
}
|
|
200
|
+
// Before the GlobalStateManager is available (eg during handler
|
|
201
|
+
// module load after resume), combine static addresses from config
|
|
202
|
+
// with dynamic contracts persisted in the database.
|
|
203
|
+
| None =>
|
|
204
|
+
switch getInitialChainState(~chainId=chainConfig.id) {
|
|
205
|
+
| Some(chainState) =>
|
|
206
|
+
let addresses = contract.addresses->Array.copy
|
|
207
|
+
chainState.indexingAddresses->Array.forEach(
|
|
208
|
+
dc => {
|
|
209
|
+
if dc.contractName === contract.name {
|
|
210
|
+
addresses->Array.push(dc.address)->ignore
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
addresses
|
|
215
|
+
| None => contract.addresses
|
|
216
|
+
}
|
|
145
217
|
}
|
|
146
218
|
},
|
|
147
219
|
},
|
|
@@ -165,12 +237,282 @@ let getGlobalIndexer = (~config: Config.t): 'indexer => {
|
|
|
165
237
|
->ignore
|
|
166
238
|
}
|
|
167
239
|
})
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
240
|
+
(chains, chainIds)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let getGlobalIndexer = (): 'indexer => {
|
|
244
|
+
// Parse eventIdentity config to extract contractName, eventName, and options.
|
|
245
|
+
// Supports two runtime formats:
|
|
246
|
+
// - From TypeScript: { contract: "X", event: "Y", wildcard?, where? }
|
|
247
|
+
// - From ReScript GADT: { event: { contract: "X", _0: "Y" }, wildcard?, where? }
|
|
248
|
+
let parseIdentityConfig = (identityConfig: 'a) => {
|
|
249
|
+
let raw =
|
|
250
|
+
identityConfig->(
|
|
251
|
+
Utils.magic: 'a => {
|
|
252
|
+
"contract": unknown,
|
|
253
|
+
"event": unknown,
|
|
254
|
+
"wildcard": option<bool>,
|
|
255
|
+
"where": option<JSON.t>,
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
// Detect format: if "contract" is a string, it's the TS format
|
|
259
|
+
let (contractName, eventName) = if typeof(raw["contract"]) === #string {
|
|
260
|
+
// TS format: { contract: "X", event: "Y" }
|
|
261
|
+
(
|
|
262
|
+
raw["contract"]->(Utils.magic: unknown => string),
|
|
263
|
+
raw["event"]->(Utils.magic: unknown => string),
|
|
264
|
+
)
|
|
265
|
+
} else {
|
|
266
|
+
// ReScript GADT format: { event: { contract: "X", _0: "Y" } }
|
|
267
|
+
let event = raw["event"]->(Utils.magic: unknown => {"contract": string, "_0": string})
|
|
268
|
+
(event["contract"], event["_0"])
|
|
269
|
+
}
|
|
270
|
+
let wildcard = raw["wildcard"]
|
|
271
|
+
let where = raw["where"]
|
|
272
|
+
let eventOptions: option<Internal.eventOptions<_>> = switch (wildcard, where) {
|
|
273
|
+
| (None, None) => None
|
|
274
|
+
| (wildcard, where) =>
|
|
275
|
+
Some({
|
|
276
|
+
?wildcard,
|
|
277
|
+
where: ?(where->(Utils.magic: option<JSON.t> => option<_>)),
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
(contractName, eventName, eventOptions)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// onEvent: delegates to HandlerRegister.setHandler
|
|
284
|
+
let onEventFn = (identityConfig: 'a, handler: 'b) => {
|
|
285
|
+
HandlerRegister.throwIfFinishedRegistration(~methodName="onEvent")
|
|
286
|
+
let (contractName, eventName, eventOptions) = parseIdentityConfig(identityConfig)
|
|
287
|
+
HandlerRegister.setHandler(
|
|
288
|
+
~contractName,
|
|
289
|
+
~eventName,
|
|
290
|
+
handler->(
|
|
291
|
+
Utils.magic: 'b => Internal.genericHandler<
|
|
292
|
+
Internal.genericHandlerArgs<Internal.event, Internal.handlerContext>,
|
|
293
|
+
>
|
|
294
|
+
),
|
|
295
|
+
~eventOptions,
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// contractRegister: delegates to HandlerRegister.setContractRegister
|
|
300
|
+
let contractRegisterFn = (identityConfig: 'a, handler: 'b) => {
|
|
301
|
+
HandlerRegister.throwIfFinishedRegistration(~methodName="contractRegister")
|
|
302
|
+
let (contractName, eventName, eventOptions) = parseIdentityConfig(identityConfig)
|
|
303
|
+
HandlerRegister.setContractRegister(
|
|
304
|
+
~contractName,
|
|
305
|
+
~eventName,
|
|
306
|
+
handler->(
|
|
307
|
+
Utils.magic: 'b => Internal.genericContractRegister<
|
|
308
|
+
Internal.genericContractRegisterArgs<Internal.event, Internal.contractRegisterContext>,
|
|
309
|
+
>
|
|
310
|
+
),
|
|
311
|
+
~eventOptions,
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Two-stage parse: first the ecosystem-specific outer schema unwraps the
|
|
316
|
+
// wrapper (`block.number` / `block.height` / `slot`) and surfaces the
|
|
317
|
+
// inner chunk as raw `unknown`; then the shared `blockRangeSchema`
|
|
318
|
+
// validates the `{_gte?, _lte?, _every?}` fields. Keeping the inner
|
|
319
|
+
// validation in one place means typos and shape mismatches surface with
|
|
320
|
+
// the same user-friendly error regardless of ecosystem.
|
|
321
|
+
let extractRange = (filter: unknown, ~name, ~ecosystem: Ecosystem.t): blockRange =>
|
|
322
|
+
try {
|
|
323
|
+
switch filter->S.parseOrThrow(ecosystem.onBlockFilterSchema) {
|
|
324
|
+
| None => defaultBlockRange
|
|
325
|
+
| Some(inner) => inner->S.parseOrThrow(blockRangeSchema)
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
| S.Raised(exn) =>
|
|
329
|
+
JsError.throwWithMessage(
|
|
330
|
+
`\`indexer.${ecosystem.onBlockMethodName}("${name}")\` \`where\` returned an invalid filter: ${exn
|
|
331
|
+
->Utils.prettifyExn
|
|
332
|
+
->(Utils.magic: exn => string)}`,
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// `where` is evaluated once per configured chain at registration time.
|
|
337
|
+
// Decoded ranges/stride feed directly into `HandlerRegister.registerOnBlock`
|
|
338
|
+
// so the fetcher's `(blockNumber - handlerStartBlock) % interval === 0`
|
|
339
|
+
// math at `FetchState.res:619` stays untouched.
|
|
340
|
+
let onBlockFn = (rawOptions: 'a, handler: 'b) => {
|
|
341
|
+
HandlerRegister.throwIfFinishedRegistration(~methodName="onBlock")
|
|
342
|
+
let config = Config.loadWithoutRegistrations()
|
|
343
|
+
let ecosystem = config.ecosystem
|
|
344
|
+
let raw =
|
|
345
|
+
rawOptions->(
|
|
346
|
+
Utils.magic: 'a => {
|
|
347
|
+
"name": string,
|
|
348
|
+
"where": option<Envio.onBlockWhereArgs<unknown> => unknown>,
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
let typedHandler = handler->(Utils.magic: 'b => Internal.onBlockArgs => promise<unit>)
|
|
352
|
+
let (chains, _) = buildChainsObject(~config)
|
|
353
|
+
let chainsDict = chains->(Utils.magic: {..} => dict<unknown>)
|
|
354
|
+
let name = raw["name"]
|
|
355
|
+
let logger = Logging.createChild(~params={"onBlock": name})
|
|
356
|
+
|
|
357
|
+
// `where` must be a function (unlike onEvent, which also accepts a static
|
|
358
|
+
// value). A static value would have to be evaluated against every chain
|
|
359
|
+
// independently, which has no useful semantic for block handlers.
|
|
360
|
+
// Normalize undefined/null to None up front so the per-chain loop below
|
|
361
|
+
// can't accidentally call `null` as a predicate (ReScript treats a JS
|
|
362
|
+
// `null` value as `Some(null)` when the field is typed as option).
|
|
363
|
+
let where = switch raw["where"]->(Utils.magic: option<'a> => unknown) {
|
|
364
|
+
| w if w === %raw(`undefined`) || w === %raw(`null`) => None
|
|
365
|
+
| w if typeof(w) === #function => Some(raw["where"]->Option.getUnsafe)
|
|
366
|
+
| w =>
|
|
367
|
+
JsError.throwWithMessage(
|
|
368
|
+
`\`indexer.${ecosystem.onBlockMethodName}("${name}")\` expected \`where\` to be a function or omitted, but got ${(typeof(
|
|
369
|
+
w,
|
|
370
|
+
) :> string)}.`,
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let matchedAny = ref(false)
|
|
375
|
+
|
|
376
|
+
config.chainMap
|
|
377
|
+
->ChainMap.values
|
|
378
|
+
->Array.forEach(chainConfig => {
|
|
379
|
+
let chainId = chainConfig.id
|
|
380
|
+
let chainObj = chainsDict->Dict.getUnsafe(chainId->Int.toString)
|
|
381
|
+
|
|
382
|
+
// Predicate returns `true` → match with no filter; `false` → skip;
|
|
383
|
+
// any plain object → structured filter. `undefined`/`null` returns
|
|
384
|
+
// are rejected — the TS type excludes `void`, so a missing return is
|
|
385
|
+
// a user bug we surface early rather than silently match-all.
|
|
386
|
+
let result = switch where {
|
|
387
|
+
| None => %raw(`true`)
|
|
388
|
+
| Some(predicate) => predicate({chain: chainObj})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let (shouldRegister, range) = if result === %raw(`true`) {
|
|
392
|
+
(true, defaultBlockRange)
|
|
393
|
+
} else if result === %raw(`false`) {
|
|
394
|
+
(false, defaultBlockRange)
|
|
395
|
+
} else if typeof(result) === #object && !(result->Array.isArray) && result !== %raw(`null`) {
|
|
396
|
+
(true, extractRange(result, ~name, ~ecosystem))
|
|
397
|
+
} else {
|
|
398
|
+
// Reject numbers, strings, functions, arrays, undefined, null —
|
|
399
|
+
// anything that isn't bool or a plain object would silently
|
|
400
|
+
// misregister.
|
|
401
|
+
JsError.throwWithMessage(
|
|
402
|
+
`\`indexer.${ecosystem.onBlockMethodName}("${name}")\` \`where\` predicate returned an invalid value of type ${(typeof(
|
|
403
|
+
result,
|
|
404
|
+
) :> string)}. Expected boolean or a filter object.`,
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if shouldRegister {
|
|
409
|
+
matchedAny := true
|
|
410
|
+
HandlerRegister.registerOnBlock(
|
|
411
|
+
~name,
|
|
412
|
+
~chainId,
|
|
413
|
+
~interval=range._every,
|
|
414
|
+
~startBlock=range._gte,
|
|
415
|
+
~endBlock=range._lte,
|
|
416
|
+
~handler=typedHandler,
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
// Catches misconfigured `where` predicates that return `false` for every
|
|
422
|
+
// configured chain — the handler would otherwise never fire with no hint.
|
|
423
|
+
// Includes the ecosystem-specific method name so SVM users see "onSlot"
|
|
424
|
+
// and don't get confused looking for a "Block handler" they never wrote.
|
|
425
|
+
if !matchedAny.contents {
|
|
426
|
+
logger->Logging.childWarn(
|
|
427
|
+
`\`indexer.${ecosystem.onBlockMethodName}\` matched 0 chains. Check the \`where\` predicate.`,
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Ecosystem-specific surface: EVM/Fuel expose event + block handlers; SVM
|
|
433
|
+
// exposes slot handlers only. The TS `.d.ts` already models this separation
|
|
434
|
+
// — the Proxy mirrors it at runtime so `Object.keys(indexer)` reflects the
|
|
435
|
+
// actually-callable methods and typos surface via the unknown-prop throw
|
|
436
|
+
// rather than silent `undefined` returns.
|
|
437
|
+
//
|
|
438
|
+
// `Api.res` calls `getGlobalIndexer()` at envio-package load, so the keys
|
|
439
|
+
// array is memoized lazily: an early `createEffect` / `S` import that
|
|
440
|
+
// never touches the indexer must not trigger a config parse. The memo is
|
|
441
|
+
// safe because `Config.loadWithoutRegistrations` is itself pure.
|
|
442
|
+
let keysMemo: ref<option<array<string>>> = ref(None)
|
|
443
|
+
let getKeys = () =>
|
|
444
|
+
switch keysMemo.contents {
|
|
445
|
+
| Some(k) => k
|
|
446
|
+
| None => {
|
|
447
|
+
let keys = switch Config.loadWithoutRegistrations().ecosystem.name {
|
|
448
|
+
| Evm | Fuel => [
|
|
449
|
+
"name",
|
|
450
|
+
"description",
|
|
451
|
+
"chainIds",
|
|
452
|
+
"chains",
|
|
453
|
+
"onEvent",
|
|
454
|
+
"contractRegister",
|
|
455
|
+
"onBlock",
|
|
456
|
+
]
|
|
457
|
+
| Svm => ["name", "description", "chainIds", "chains", "onSlot"]
|
|
458
|
+
}
|
|
459
|
+
keysMemo := Some(keys)
|
|
460
|
+
keys
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
let get = (~prop: string) =>
|
|
465
|
+
switch prop {
|
|
466
|
+
| "name" => Config.loadWithoutRegistrations().name->(Utils.magic: string => unknown)
|
|
467
|
+
| "description" =>
|
|
468
|
+
Config.loadWithoutRegistrations().description->(Utils.magic: option<string> => unknown)
|
|
469
|
+
| "chainIds" => {
|
|
470
|
+
let (_, chainIds) = buildChainsObject(~config=Config.loadWithoutRegistrations())
|
|
471
|
+
chainIds->(Utils.magic: array<int> => unknown)
|
|
472
|
+
}
|
|
473
|
+
| "chains" => {
|
|
474
|
+
let (chains, _) = buildChainsObject(~config=Config.loadWithoutRegistrations())
|
|
475
|
+
chains->(Utils.magic: {..} => unknown)
|
|
476
|
+
}
|
|
477
|
+
| "onEvent" => onEventFn->Utils.magic
|
|
478
|
+
| "contractRegister" => contractRegisterFn->Utils.magic
|
|
479
|
+
| "onBlock" | "onSlot" => onBlockFn->Utils.magic
|
|
480
|
+
| _ =>
|
|
481
|
+
JsError.throwWithMessage(
|
|
482
|
+
`Field \`${prop}\` does not exist on \`indexer\`. Available fields: ${getKeys()->Array.join(
|
|
483
|
+
", ",
|
|
484
|
+
)}.`,
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let traps: Utils.Proxy.traps<{..}> = {
|
|
489
|
+
// Engine internals (`Symbol.toStringTag`, `Symbol.toPrimitive`, inspect
|
|
490
|
+
// hooks, etc.) read symbol-keyed properties — fall through to the
|
|
491
|
+
// underlying null-proto target so stringification / inspection of the
|
|
492
|
+
// indexer value stays well-behaved instead of throwing.
|
|
493
|
+
get: (~target, ~prop) =>
|
|
494
|
+
if typeof(prop) === #string {
|
|
495
|
+
get(~prop=prop->(Utils.magic: unknown => string))
|
|
496
|
+
} else {
|
|
497
|
+
target->(Utils.magic: {..} => dict<unknown>)->Dict.getUnsafe(prop->Utils.magic)
|
|
498
|
+
},
|
|
499
|
+
ownKeys: (~target as _) => getKeys(),
|
|
500
|
+
getOwnPropertyDescriptor: (~target as _, ~prop) =>
|
|
501
|
+
if (
|
|
502
|
+
typeof(prop) === #string &&
|
|
503
|
+
getKeys()->Array.includes(prop->(Utils.magic: unknown => string))
|
|
504
|
+
) {
|
|
505
|
+
Some({
|
|
506
|
+
value: get(~prop=prop->(Utils.magic: unknown => string)),
|
|
507
|
+
enumerable: true,
|
|
508
|
+
configurable: true,
|
|
509
|
+
})
|
|
510
|
+
} else {
|
|
511
|
+
None
|
|
512
|
+
},
|
|
513
|
+
}
|
|
172
514
|
|
|
173
|
-
|
|
515
|
+
Utils.Proxy.make(Utils.Object.createNullObject(), traps)->(Utils.magic: {..} => 'indexer)
|
|
174
516
|
}
|
|
175
517
|
|
|
176
518
|
let startServer = (~getState, ~ctx: Ctx.t, ~isDevelopmentMode: bool) => {
|
|
@@ -179,7 +521,7 @@ let startServer = (~getState, ~ctx: Ctx.t, ~isDevelopmentMode: bool) => {
|
|
|
179
521
|
let app = make()
|
|
180
522
|
|
|
181
523
|
let consoleCorsMiddleware = (req, res, next) => {
|
|
182
|
-
switch req.headers->
|
|
524
|
+
switch req.headers->Dict.get("origin") {
|
|
183
525
|
| Some(origin) if origin === Env.prodEnvioAppUrl || origin === Env.envioAppUrl =>
|
|
184
526
|
res->setHeader("Access-Control-Allow-Origin", origin)
|
|
185
527
|
| _ => ()
|
|
@@ -218,7 +560,7 @@ let startServer = (~getState, ~ctx: Ctx.t, ~isDevelopmentMode: bool) => {
|
|
|
218
560
|
if isDevelopmentMode {
|
|
219
561
|
(ctx.persistence->Persistence.getInitializedStorageOrThrow).dumpEffectCache()
|
|
220
562
|
->Promise.thenResolve(_ => res->json(Boolean(true)))
|
|
221
|
-
->Promise.
|
|
563
|
+
->Promise.ignore
|
|
222
564
|
} else {
|
|
223
565
|
res->json(Boolean(false))
|
|
224
566
|
}
|
|
@@ -245,7 +587,7 @@ let startServer = (~getState, ~ctx: Ctx.t, ~isDevelopmentMode: bool) => {
|
|
|
245
587
|
|
|
246
588
|
let server = app->listen(Env.serverPort)
|
|
247
589
|
server->Express.onError(err => {
|
|
248
|
-
let code = (err->(Utils.magic:
|
|
590
|
+
let code = (err->(Utils.magic: JsExn.t => {..}))["code"]
|
|
249
591
|
if code === "EADDRINUSE" {
|
|
250
592
|
Logging.error(
|
|
251
593
|
`Port ${Env.serverPort->Int.toString} is already in use. To fix this either:` ++
|
|
@@ -266,24 +608,63 @@ type process
|
|
|
266
608
|
|
|
267
609
|
type mainArgs = Yargs.parsedArgs<args>
|
|
268
610
|
|
|
611
|
+
type migrateOpts = {reset: bool, persistedState: JSON.t}
|
|
612
|
+
|
|
613
|
+
let migrate = async (~reset, ~persistedState) => {
|
|
614
|
+
let config = Config.loadWithoutRegistrations()
|
|
615
|
+
let persistence = PgStorage.makePersistenceFromConfig(~config)
|
|
616
|
+
await persistence->Persistence.init(~reset, ~chainConfigs=config.chainMap->ChainMap.values)
|
|
617
|
+
await Core.upsertPersistedState(persistedState->JSON.stringify)
|
|
618
|
+
await persistence.storage.close()
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
let dropSchema = async () => {
|
|
622
|
+
let config = Config.loadWithoutRegistrations()
|
|
623
|
+
let persistence = PgStorage.makePersistenceFromConfig(~config)
|
|
624
|
+
await persistence.storage.reset()
|
|
625
|
+
await persistence.storage.close()
|
|
626
|
+
}
|
|
627
|
+
|
|
269
628
|
let start = async (
|
|
270
|
-
~
|
|
271
|
-
~
|
|
629
|
+
~persistence: option<Persistence.t>=?,
|
|
630
|
+
~migrate: option<migrateOpts>=?,
|
|
272
631
|
~isTest=false,
|
|
273
632
|
~exitAfterFirstEventBlock=false,
|
|
274
633
|
~patchConfig: option<(Config.t, HandlerRegister.registrations) => Config.t>=?,
|
|
275
634
|
) => {
|
|
276
635
|
let mainArgs: mainArgs = process->argv->Yargs.hideBin->Yargs.yargs->Yargs.argv
|
|
277
636
|
let shouldUseTui = !isTest && !(mainArgs.tuiOff->Belt.Option.getWithDefault(Env.tuiOffEnvVar))
|
|
278
|
-
//
|
|
279
|
-
// and
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
637
|
+
// isDevelopmentMode controls whether the indexer stays alive after all
|
|
638
|
+
// chains finish (keepProcessAlive) and whether the console API is exposed.
|
|
639
|
+
// Set by `envio dev` via the ENVIO_DEV_MODE env var; `envio start` leaves
|
|
640
|
+
// it unset so the process exits cleanly when indexing completes.
|
|
641
|
+
let isDevelopmentMode = !isTest && Envio.isDevMode()
|
|
642
|
+
|
|
643
|
+
// Initialize persistence first so the exported indexer value contains state from the database
|
|
644
|
+
// when handler files are loaded (they may access the indexer at module top level).
|
|
645
|
+
// `migrate`, when provided, folds the DB setup into the same `init()` call.
|
|
646
|
+
let configWithoutRegistrations = Config.loadWithoutRegistrations()
|
|
647
|
+
let persistence = switch persistence {
|
|
648
|
+
| Some(p) => p
|
|
649
|
+
| None => PgStorage.makePersistenceFromConfig(~config=configWithoutRegistrations)
|
|
650
|
+
}
|
|
651
|
+
globalPersistenceRef := Some(persistence)
|
|
652
|
+
let reset = migrate->Option.map(m => m.reset)->Option.getOr(false)
|
|
653
|
+
await persistence->Persistence.init(
|
|
654
|
+
~reset,
|
|
655
|
+
~chainConfigs=configWithoutRegistrations.chainMap->ChainMap.values,
|
|
656
|
+
)
|
|
657
|
+
switch migrate {
|
|
658
|
+
| Some({persistedState}) => await Core.upsertPersistedState(persistedState->JSON.stringify)
|
|
659
|
+
| None => ()
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// `Config.loadWithoutRegistrations` never sees registration state; handler,
|
|
663
|
+
// contractRegister, and eventFilters are baked into each event config only
|
|
664
|
+
// by the returned value here.
|
|
665
|
+
let (config, registrations) = await HandlerLoader.registerAllHandlers(
|
|
666
|
+
~config=configWithoutRegistrations,
|
|
667
|
+
)
|
|
287
668
|
let config = if isTest {
|
|
288
669
|
{...config, shouldRollbackOnReorg: false}
|
|
289
670
|
} else {
|
|
@@ -322,11 +703,11 @@ let start = async (
|
|
|
322
703
|
)
|
|
323
704
|
let knownHeight =
|
|
324
705
|
cf->ChainFetcher.hasProcessedToEndblock
|
|
325
|
-
? cf.fetchState.endBlock->Option.
|
|
706
|
+
? cf.fetchState.endBlock->Option.getOr(cf.fetchState.knownHeight)
|
|
326
707
|
: cf.fetchState.knownHeight
|
|
327
708
|
|
|
328
709
|
{
|
|
329
|
-
chainId: cf.chainConfig.id->
|
|
710
|
+
chainId: cf.chainConfig.id->Int.toFloat,
|
|
330
711
|
poweredByHyperSync: (
|
|
331
712
|
cf.sourceManager->SourceManager.getActiveSource
|
|
332
713
|
).poweredByHyperSync,
|
|
@@ -360,9 +741,7 @@ let start = async (
|
|
|
360
741
|
)
|
|
361
742
|
}
|
|
362
743
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
let chainManager = await ChainManager.makeFromDbState(
|
|
744
|
+
let chainManager = ChainManager.makeFromDbState(
|
|
366
745
|
~initialState=ctx.persistence->Persistence.getInitializedState,
|
|
367
746
|
~config=ctx.config,
|
|
368
747
|
~registrations=ctx.registrations,
|