envio 3.9.0 → 3.10.0
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 +26 -0
- package/fuel.schema.json +26 -0
- package/index.d.ts +35 -3
- package/package.json +6 -7
- package/src/Batch.res +9 -0
- package/src/ChainState.res +35 -39
- package/src/ChainState.res.mjs +14 -11
- package/src/ChainState.resi +15 -7
- package/src/Config.res +52 -44
- package/src/Config.res.mjs +51 -17
- package/src/Core.res +4 -0
- package/src/CrossChainState.res +15 -23
- package/src/CrossChainState.res.mjs +10 -18
- package/src/CrossChainState.resi +4 -1
- package/src/Ecosystem.res +7 -2
- package/src/Envio.res +6 -4
- package/src/EventConfigBuilder.res +169 -26
- package/src/EventConfigBuilder.res.mjs +124 -15
- package/src/HandlerRegister.res +21 -4
- package/src/HandlerRegister.res.mjs +24 -5
- package/src/InMemoryStore.res +9 -7
- package/src/InMemoryStore.res.mjs +3 -3
- package/src/IndexerState.res +43 -6
- package/src/IndexerState.res.mjs +36 -7
- package/src/IndexerState.resi +4 -4
- package/src/Internal.res +16 -17
- package/src/Metrics.res +72 -6
- package/src/Metrics.res.mjs +30 -2
- package/src/Persistence.res +36 -30
- package/src/Persistence.res.mjs +1 -20
- package/src/PgStorage.res +104 -56
- package/src/PgStorage.res.mjs +57 -35
- package/src/PruneStaleHistory.res +85 -52
- package/src/PruneStaleHistory.res.mjs +63 -37
- package/src/Rollback.res +68 -45
- package/src/Rollback.res.mjs +38 -19
- package/src/RollbackCommit.res +7 -9
- package/src/RollbackCommit.res.mjs +5 -6
- package/src/SafeCheckpointTracking.res +11 -11
- package/src/SafeCheckpointTracking.res.mjs +7 -6
- package/src/SimulateItems.res +33 -18
- package/src/SimulateItems.res.mjs +26 -22
- package/src/Sink.res +38 -26
- package/src/Sink.res.mjs +21 -16
- package/src/TestIndexer.res +5 -5
- package/src/TestIndexer.res.mjs +1 -1
- package/src/Utils.res +170 -0
- package/src/Utils.res.mjs +153 -0
- package/src/Writing.res +2 -2
- package/src/Writing.res.mjs +1 -1
- package/src/bindings/ClickHouse.res +366 -698
- package/src/bindings/ClickHouse.res.mjs +371 -421
- package/src/bindings/ClickHouseSink.res +337 -0
- package/src/bindings/ClickHouseSink.res.mjs +246 -0
- package/src/bindings/EventSource.res +8 -2
- package/src/bindings/NodeJs.res +6 -0
- package/src/bindings/Vitest.res +23 -0
- package/src/bindings/Vitest.res.mjs +3 -0
- package/src/bindings/WebSocket.res +9 -10
- package/src/db/CheckpointBounds.res +53 -0
- package/src/db/CheckpointBounds.res.mjs +44 -0
- package/src/db/EntityFilter.res +36 -17
- package/src/db/EntityFilter.res.mjs +25 -14
- package/src/db/EntityHistory.res +47 -22
- package/src/db/EntityHistory.res.mjs +19 -12
- package/src/db/InternalTable.res +84 -35
- package/src/db/InternalTable.res.mjs +56 -23
- package/src/db/RollbackFloors.res +55 -0
- package/src/db/RollbackFloors.res.mjs +92 -0
- package/src/db/Table.res +22 -4
- package/src/db/Table.res.mjs +30 -9
- package/src/sources/Evm.res +2 -0
- package/src/sources/Evm.res.mjs +2 -0
- package/src/sources/EvmHyperSyncSource.res +3 -3
- package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
- package/src/sources/EvmRpcWs.res +124 -0
- package/src/sources/EvmRpcWs.res.mjs +117 -0
- package/src/sources/Fuel.res +2 -0
- package/src/sources/Fuel.res.mjs +2 -0
- package/src/sources/FuelHyperSyncSource.res +1 -1
- package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
- package/src/sources/HeightFeed.res +568 -0
- package/src/sources/HeightFeed.res.mjs +452 -0
- package/src/sources/HeightStream.res +257 -0
- package/src/sources/HeightStream.res.mjs +194 -0
- package/src/sources/HyperSync.res +5 -0
- package/src/sources/HyperSync.res.mjs +3 -0
- package/src/sources/HyperSync.resi +4 -0
- package/src/sources/HyperSyncSSE.res +49 -0
- package/src/sources/HyperSyncSSE.res.mjs +79 -0
- package/src/sources/RpcSource.res +1 -1
- package/src/sources/RpcSource.res.mjs +2 -2
- package/src/sources/Source.res +54 -1
- package/src/sources/Source.res.mjs +25 -0
- package/src/sources/SourceManager.res +220 -227
- package/src/sources/SourceManager.res.mjs +139 -164
- package/src/sources/SourceManager.resi +8 -0
- package/src/sources/Svm.res +2 -0
- package/src/sources/Svm.res.mjs +2 -0
- package/src/sources/SvmHyperSyncClient.res +70 -127
- package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
- package/src/sources/SvmHyperSyncSource.res +18 -15
- package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
- package/svm.schema.json +1 -75
- package/src/MemoryStorage.res +0 -729
- package/src/MemoryStorage.res.mjs +0 -586
- package/src/sources/HyperSyncHeightStream.res +0 -129
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
- package/src/sources/RpcWebSocketHeightStream.res +0 -175
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
package/src/Core.res
CHANGED
|
@@ -10,6 +10,7 @@ type svmHyperSyncClientCtor
|
|
|
10
10
|
type fuelHyperSyncClientCtor
|
|
11
11
|
type transactionStoreCtor
|
|
12
12
|
type blockStoreCtor
|
|
13
|
+
type clickHouseSinkCtor
|
|
13
14
|
type addressStoreCtor
|
|
14
15
|
// Test-only: a local HyperSync server, bound by MockHyperSyncServer in envio-tests.
|
|
15
16
|
type mockHyperSyncServerCtor
|
|
@@ -29,6 +30,7 @@ type fromUserApiResult = {
|
|
|
29
30
|
type addon = {
|
|
30
31
|
getConfigJson: (~configPath: Null.t<string>, ~directory: Null.t<string>) => string,
|
|
31
32
|
encodeIndexedTopic: (~abiType: string, ~value: unknown) => EvmTypes.Hex.t,
|
|
33
|
+
isSvmPubkey: (~value: string) => bool,
|
|
32
34
|
fromUserApi: (string, fromUserApiOptions) => fromUserApiResult,
|
|
33
35
|
runCli: (~args: array<string>, ~envioPackageDir: Null.t<string>) => promise<Null.t<string>>,
|
|
34
36
|
@as("EvmHyperSyncClient")
|
|
@@ -45,6 +47,8 @@ type addon = {
|
|
|
45
47
|
blockStore: blockStoreCtor,
|
|
46
48
|
@as("AddressStore")
|
|
47
49
|
addressStore: addressStoreCtor,
|
|
50
|
+
@as("ClickHouseSink")
|
|
51
|
+
clickHouseSink: clickHouseSinkCtor,
|
|
48
52
|
@as("MockHyperSyncServer")
|
|
49
53
|
mockHyperSyncServer: mockHyperSyncServerCtor,
|
|
50
54
|
encodeAddresses: (~ecosystem: string, ~addresses: array<Address.t>) => array<NodeJs.Buffer.t>,
|
package/src/CrossChainState.res
CHANGED
|
@@ -72,31 +72,23 @@ let totalReadyCount = (crossChainState: t) => {
|
|
|
72
72
|
let nextItemIsNone = (crossChainState: t): bool =>
|
|
73
73
|
!(crossChainState.chainStates->Dict.valuesToArray->Array.some(ChainState.hasReadyItem))
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
75
|
+
// Each chain's safe checkpoint: the last one a reorg on that chain can no longer
|
|
76
|
+
// reach, or None while it has nothing safe yet. A chain that can't be rolled
|
|
77
|
+
// back (maxReorgDepth = 0) tracks none, and everything it has committed is safe.
|
|
78
|
+
let getSafeCheckpointIdByChain = (crossChainState: t, ~committedCheckpointId) =>
|
|
79
|
+
crossChainState.chainIds->Array.map(chainId => {
|
|
80
|
+
let cs = crossChainState->getChainState(chainId)
|
|
81
|
+
(
|
|
82
|
+
chainId,
|
|
83
|
+
switch cs->ChainState.safeCheckpointTracking {
|
|
84
|
+
| None => Some(committedCheckpointId)
|
|
85
|
+
| Some(tracking) =>
|
|
86
|
+
tracking->SafeCheckpointTracking.getSafeCheckpointId(
|
|
85
87
|
~sourceBlockNumber=cs->ChainState.knownHeight,
|
|
86
88
|
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
| _ => ()
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
switch result.contents {
|
|
96
|
-
| Some(id) if id > 0n => Some(id)
|
|
97
|
-
| _ => None // No safe checkpoint found
|
|
98
|
-
}
|
|
99
|
-
}
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
})
|
|
100
92
|
|
|
101
93
|
// --- Cross-chain transitions. ---
|
|
102
94
|
|
|
@@ -65,23 +65,15 @@ function nextItemIsNone(crossChainState) {
|
|
|
65
65
|
return !Object.values(crossChainState.chainStates).some(ChainState.hasReadyItem);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
let
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
result = safeCheckpointId;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
let id = result;
|
|
82
|
-
if (id !== undefined && id > 0n) {
|
|
83
|
-
return id;
|
|
84
|
-
}
|
|
68
|
+
function getSafeCheckpointIdByChain(crossChainState, committedCheckpointId) {
|
|
69
|
+
return crossChainState.chainIds.map(chainId => {
|
|
70
|
+
let cs = crossChainState.chainStates[chainId];
|
|
71
|
+
let tracking = ChainState.safeCheckpointTracking(cs);
|
|
72
|
+
return [
|
|
73
|
+
chainId,
|
|
74
|
+
tracking !== undefined ? SafeCheckpointTracking.getSafeCheckpointId(tracking, ChainState.knownHeight(cs)) : committedCheckpointId
|
|
75
|
+
];
|
|
76
|
+
});
|
|
85
77
|
}
|
|
86
78
|
|
|
87
79
|
function createBatch(crossChainState, processedCheckpointId, batchSizeTarget, isRollback) {
|
|
@@ -261,7 +253,7 @@ export {
|
|
|
261
253
|
isInReorgThreshold,
|
|
262
254
|
targetBufferSize,
|
|
263
255
|
nextItemIsNone,
|
|
264
|
-
|
|
256
|
+
getSafeCheckpointIdByChain,
|
|
265
257
|
createBatch,
|
|
266
258
|
enterReorgThreshold,
|
|
267
259
|
applyBatchProgress,
|
package/src/CrossChainState.resi
CHANGED
|
@@ -21,7 +21,10 @@ let targetBufferSize: t => int
|
|
|
21
21
|
|
|
22
22
|
// Derived (pure).
|
|
23
23
|
let nextItemIsNone: t => bool
|
|
24
|
-
let
|
|
24
|
+
let getSafeCheckpointIdByChain: (
|
|
25
|
+
t,
|
|
26
|
+
~committedCheckpointId: Internal.checkpointId,
|
|
27
|
+
) => array<(ChainId.t, option<Internal.checkpointId>)>
|
|
25
28
|
|
|
26
29
|
// Cross-chain transitions.
|
|
27
30
|
let createBatch: (
|
package/src/Ecosystem.res
CHANGED
|
@@ -10,6 +10,11 @@ type t = {
|
|
|
10
10
|
for SVM. Centralised here so adding a new ecosystem only requires a
|
|
11
11
|
new ecosystem record, not another switch in `Main.res`. */
|
|
12
12
|
onBlockMethodName: string,
|
|
13
|
+
/** What the ecosystem calls a contract and one of its events, for
|
|
14
|
+
registration errors that are shared across ecosystems — "contract" /
|
|
15
|
+
"event" on EVM and Fuel, "program" / "instruction" on SVM. */
|
|
16
|
+
contractNoun: string,
|
|
17
|
+
eventNoun: string,
|
|
13
18
|
/** Schema that unwraps the ecosystem-specific outer wrapper around the
|
|
14
19
|
user's `where`-returned filter (`block.number` on EVM, `block.height`
|
|
15
20
|
on Fuel, `slot` on SVM) and surfaces the raw inner `{_gte?, _lte?,
|
|
@@ -23,8 +28,8 @@ type t = {
|
|
|
23
28
|
`option<unknown>`. Separate from `onBlockFilterSchema` because event
|
|
24
29
|
block filters support only `_gte` (→ per-event `startBlock`) — `_lte`
|
|
25
30
|
and `_every` are rejected by the inner `eventBlockRangeSchema` in
|
|
26
|
-
`LogSelection.res`. SVM
|
|
27
|
-
schema always surfaces `None`. */
|
|
31
|
+
`LogSelection.res`. SVM parses its own `where` (including the
|
|
32
|
+
`block.slot` chunk), so its schema always surfaces `None`. */
|
|
28
33
|
onEventBlockFilterSchema: S.t<option<unknown>>,
|
|
29
34
|
/** Base logger injected at construction. Used to build per-item child
|
|
30
35
|
loggers (see `getItemLogger`). */
|
package/src/Envio.res
CHANGED
|
@@ -83,10 +83,12 @@ type svmInstruction = {
|
|
|
83
83
|
instructionName: string,
|
|
84
84
|
discriminator: string,
|
|
85
85
|
programId?: SvmTypes.Pubkey.t,
|
|
86
|
-
data?:
|
|
86
|
+
data?: Uint8Array.t,
|
|
87
87
|
path?: array<int>,
|
|
88
88
|
isInner?: bool,
|
|
89
|
-
args
|
|
89
|
+
// Decoded Borsh args; wide integers (u64/u128/i64/i128) are bigint, so the
|
|
90
|
+
// tree is not valid JSON.
|
|
91
|
+
args?: unknown,
|
|
90
92
|
accounts?: dict<svmInstructionAccount>,
|
|
91
93
|
accountArguments?: array<SvmTypes.Pubkey.t>,
|
|
92
94
|
transaction?: svmTransaction,
|
|
@@ -283,9 +285,9 @@ type svmSimulateItem = {
|
|
|
283
285
|
slot?: int,
|
|
284
286
|
path?: array<int>,
|
|
285
287
|
programId?: string,
|
|
286
|
-
data?:
|
|
288
|
+
data?: Uint8Array.t,
|
|
287
289
|
isInner?: bool,
|
|
288
|
-
args?:
|
|
290
|
+
args?: unknown,
|
|
289
291
|
accounts?: dict<{address: string}>,
|
|
290
292
|
accountArguments?: array<string>,
|
|
291
293
|
logs?: array<{kind?: string, message?: string}>,
|
|
@@ -317,9 +317,6 @@ let svmSelectionKinds = ["instruction", "transaction", "accountActivity", "block
|
|
|
317
317
|
// typo, and an unrecognised key would read as an empty selection — silently
|
|
318
318
|
// dropping every field `config.yaml` selected. Rejected here so the option is
|
|
319
319
|
// held to the same shape whether or not the project type-checks it.
|
|
320
|
-
let quotedJoin = (names: array<string>) =>
|
|
321
|
-
names->Array.map(name => `"${name}"`)->Array.joinUnsafe(", ")
|
|
322
|
-
|
|
323
320
|
let validateFieldsShapeOrThrow = (
|
|
324
321
|
fields: unknown,
|
|
325
322
|
~registration: string,
|
|
@@ -337,7 +334,7 @@ let validateFieldsShapeOrThrow = (
|
|
|
337
334
|
->Array.forEach(key =>
|
|
338
335
|
if !(validKeys->Array.includes(key)) {
|
|
339
336
|
JsError.throwWithMessage(
|
|
340
|
-
`Invalid "${key}" key in the fields option of ${registration}. Valid keys: ${quotedJoin(
|
|
337
|
+
`Invalid "${key}" key in the fields option of ${registration}. Valid keys: ${Utils.Array.quotedJoin(
|
|
341
338
|
validKeys,
|
|
342
339
|
)}.`,
|
|
343
340
|
)
|
|
@@ -373,7 +370,7 @@ let parseFieldsOrThrow = (
|
|
|
373
370
|
fields->Array.forEach(name => {
|
|
374
371
|
if !(valid->Utils.Set.has(name)) {
|
|
375
372
|
JsError.throwWithMessage(
|
|
376
|
-
`Invalid "${name}" field in the fields.${kind} option of ${registration}. Valid ${kind} fields: ${quotedJoin(
|
|
373
|
+
`Invalid "${name}" field in the fields.${kind} option of ${registration}. Valid ${kind} fields: ${Utils.Array.quotedJoin(
|
|
377
374
|
valid->Utils.Set.toArray,
|
|
378
375
|
)}.`,
|
|
379
376
|
)
|
|
@@ -577,7 +574,7 @@ let resolveSvmInlineFieldSelection = (
|
|
|
577
574
|
~contractName: string,
|
|
578
575
|
~eventName: string,
|
|
579
576
|
): Internal.fieldSelection => {
|
|
580
|
-
let registration = `the "${eventName}"
|
|
577
|
+
let registration = `the "${eventName}" instruction on program "${contractName}"`
|
|
581
578
|
validateFieldsShapeOrThrow(
|
|
582
579
|
fields,
|
|
583
580
|
~registration,
|
|
@@ -638,9 +635,6 @@ let buildSvmInstructionEventConfig = (
|
|
|
638
635
|
~instructionName: string,
|
|
639
636
|
~programId: SvmTypes.Pubkey.t,
|
|
640
637
|
~discriminator: option<string>,
|
|
641
|
-
~discriminatorByteLen: int,
|
|
642
|
-
~accountFilters: array<Internal.svmAccountFilterGroup>,
|
|
643
|
-
~isInner: option<bool>,
|
|
644
638
|
~accounts: array<string>=[],
|
|
645
639
|
~args: JSON.t=JSON.Null,
|
|
646
640
|
~definedTypes: JSON.t=JSON.Null,
|
|
@@ -667,38 +661,187 @@ let buildSvmInstructionEventConfig = (
|
|
|
667
661
|
simulateParamsSchema: paramsSchema,
|
|
668
662
|
programId,
|
|
669
663
|
discriminator,
|
|
670
|
-
discriminatorByteLen,
|
|
671
664
|
fieldSelection,
|
|
672
|
-
accountFilters,
|
|
673
|
-
isInner,
|
|
674
665
|
accounts,
|
|
675
666
|
args,
|
|
676
667
|
definedTypes,
|
|
677
668
|
}
|
|
678
669
|
}
|
|
679
670
|
|
|
680
|
-
//
|
|
681
|
-
|
|
671
|
+
// ============== SVM `where` ==============
|
|
672
|
+
|
|
673
|
+
type parsedSvmWhere = {
|
|
674
|
+
// Disjunctive normal form: outer array is OR of AND-groups. Empty means the
|
|
675
|
+
// registration accepts any accounts.
|
|
676
|
+
accountFilters: array<Internal.svmAccountFilterGroup>,
|
|
677
|
+
isInner: option<bool>,
|
|
678
|
+
startBlock: option<int>,
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
let validSvmWhereKeys = ["accounts", "isInner", "block"]
|
|
682
|
+
|
|
683
|
+
// `{slot: {_gte?}}`. Both levels are strict: the inner `eventBlockRangeSchema`
|
|
684
|
+
// rejects the `_lte` / `_every` that only `onSlot` supports, the outer rejects
|
|
685
|
+
// a wrapper key other than `slot`.
|
|
686
|
+
type svmBlockFilter = {slot?: LogSelection.eventBlockRange}
|
|
687
|
+
let svmBlockFilterSchema: S.t<svmBlockFilter> = S.object(s => {
|
|
688
|
+
slot: ?s.field("slot", S.option(LogSelection.eventBlockRangeSchema)),
|
|
689
|
+
})->S.strict
|
|
690
|
+
|
|
691
|
+
// The source query narrows accounts through `a0..a9`, so only an instruction's
|
|
692
|
+
// leading account slots can be filtered.
|
|
693
|
+
let filterableAccountCount = 10
|
|
694
|
+
|
|
695
|
+
// Resolve the `where` option of an `onInstruction` registration. Account names
|
|
696
|
+
// are resolved against the instruction's declared accounts here, so the
|
|
697
|
+
// registration carries positions and nothing downstream needs the names.
|
|
698
|
+
let resolveSvmWhereOrThrow = (
|
|
699
|
+
where: JSON.t,
|
|
700
|
+
~contractName: string,
|
|
701
|
+
~eventName: string,
|
|
702
|
+
~accountNames: array<string>,
|
|
703
|
+
): parsedSvmWhere => {
|
|
704
|
+
let invalid = message =>
|
|
705
|
+
JsError.throwWithMessage(
|
|
706
|
+
`Invalid where configuration for the "${eventName}" instruction on program "${contractName}". ${message}`,
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
let obj = switch where {
|
|
710
|
+
| Object(obj) => obj
|
|
711
|
+
| _ => invalid("Expected an object.")
|
|
712
|
+
}
|
|
713
|
+
obj->Utils.Dict.forEachWithKey((_, key) =>
|
|
714
|
+
if !(validSvmWhereKeys->Array.includes(key)) {
|
|
715
|
+
invalid(`Unknown field "${key}". Valid fields: ${Utils.Array.quotedJoin(validSvmWhereKeys)}.`)
|
|
716
|
+
}
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
let isInner = switch obj->Dict.get("isInner") {
|
|
720
|
+
| None => None
|
|
721
|
+
| Some(Boolean(isInner)) => Some(isInner)
|
|
722
|
+
| Some(_) => invalid(`The "isInner" filter must be a boolean.`)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
let parseGroup = (group: dict<JSON.t>): Internal.svmAccountFilterGroup =>
|
|
726
|
+
group
|
|
727
|
+
->Dict.toArray
|
|
728
|
+
->Array.map(((name, value)) => {
|
|
729
|
+
let position = switch accountNames->Array.indexOf(name) {
|
|
730
|
+
| -1 if accountNames->Utils.Array.isEmpty =>
|
|
731
|
+
invalid(
|
|
732
|
+
"The instruction has no named accounts to filter on. Add `accounts` and `args` to it in config.yaml, or attach an IDL.",
|
|
733
|
+
)
|
|
734
|
+
| -1 =>
|
|
735
|
+
invalid(
|
|
736
|
+
`The instruction has no account named "${name}" to filter on. Named accounts: ${Utils.Array.quotedJoin(
|
|
737
|
+
accountNames,
|
|
738
|
+
)}.`,
|
|
739
|
+
)
|
|
740
|
+
| position if position >= filterableAccountCount =>
|
|
741
|
+
invalid(
|
|
742
|
+
`Account "${name}" is at position ${position->Int.toString}, and only the first ${filterableAccountCount->Int.toString} accounts of an instruction can be filtered.`,
|
|
743
|
+
)
|
|
744
|
+
| position => position
|
|
745
|
+
}
|
|
746
|
+
let values = value->normalizeOrThrow
|
|
747
|
+
if values->Utils.Array.isEmpty {
|
|
748
|
+
invalid(`The "${name}" filter must list at least one pubkey.`)
|
|
749
|
+
}
|
|
750
|
+
{
|
|
751
|
+
Internal.position,
|
|
752
|
+
values: values->Array.map(value =>
|
|
753
|
+
switch value {
|
|
754
|
+
| JSON.String(pubkey) if Core.getAddon().isSvmPubkey(~value=pubkey) =>
|
|
755
|
+
pubkey->SvmTypes.Pubkey.fromStringUnsafe
|
|
756
|
+
| JSON.String(pubkey) =>
|
|
757
|
+
invalid(`The "${name}" filter value "${pubkey}" is not a base58 SVM pubkey.`)
|
|
758
|
+
| _ => invalid(`The "${name}" filter must list base58 pubkeys as strings.`)
|
|
759
|
+
}
|
|
760
|
+
),
|
|
761
|
+
}
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
// An empty AND-group matches every instruction, so it makes the whole OR
|
|
765
|
+
// vacuous. Normalized away here so the source builds one unfiltered
|
|
766
|
+
// selection instead of a match-all selection beside the narrower ones.
|
|
767
|
+
let groups = switch obj->Dict.get("accounts") {
|
|
768
|
+
| None => []
|
|
769
|
+
| Some(Object(group)) => [parseGroup(group)]
|
|
770
|
+
| Some(Array(entries)) =>
|
|
771
|
+
entries->Array.map(entry =>
|
|
772
|
+
switch entry {
|
|
773
|
+
| Object(group) => parseGroup(group)
|
|
774
|
+
| _ => invalid(`Each entry in "accounts" must be an object.`)
|
|
775
|
+
}
|
|
776
|
+
)
|
|
777
|
+
| Some(_) => invalid(`Expected "accounts" to be an object or an array of objects.`)
|
|
778
|
+
}
|
|
779
|
+
let accountFilters = groups->Array.some(Utils.Array.isEmpty) ? [] : groups
|
|
780
|
+
|
|
781
|
+
let startBlock = switch obj->Dict.get("block") {
|
|
782
|
+
| None => None
|
|
783
|
+
| Some(block) =>
|
|
784
|
+
let filter = try block->S.parseOrThrow(svmBlockFilterSchema) catch {
|
|
785
|
+
| S.Raised(exn) =>
|
|
786
|
+
invalid(
|
|
787
|
+
`The "block" filter is invalid: ${exn
|
|
788
|
+
->Utils.prettifyExn
|
|
789
|
+
->(
|
|
790
|
+
Utils.magic: exn => string
|
|
791
|
+
)}. Only \`_gte\` is supported on instruction filters — use \`indexer.onSlot\` for \`_lte\` or \`_every\`.`,
|
|
792
|
+
)
|
|
793
|
+
}
|
|
794
|
+
switch filter.slot {
|
|
795
|
+
| Some({_gte}) => _gte
|
|
796
|
+
| None => None
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
{accountFilters, isInner, startBlock}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Enrich an SVM definition into a registration: the handler binding plus the
|
|
804
|
+
// `where`-derived account/isInner filters and startBlock override.
|
|
682
805
|
let buildSvmOnEventRegistration = (
|
|
683
806
|
~eventConfig: Internal.svmInstructionEventConfig,
|
|
684
807
|
~isWildcard: bool,
|
|
685
808
|
~handler: option<Internal.handler>,
|
|
686
809
|
~contractRegister: option<Internal.contractRegister>,
|
|
810
|
+
~where: option<JSON.t>,
|
|
687
811
|
~fieldSelection: option<Internal.fieldSelection>=?,
|
|
688
812
|
~startBlock: option<int>=?,
|
|
689
813
|
): Internal.svmOnEventRegistration => {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
814
|
+
let resolvedWhere = switch where {
|
|
815
|
+
| None => {accountFilters: [], isInner: None, startBlock: None}
|
|
816
|
+
| Some(where) =>
|
|
817
|
+
where->resolveSvmWhereOrThrow(
|
|
818
|
+
~contractName=eventConfig.contractName,
|
|
819
|
+
~eventName=eventConfig.name,
|
|
820
|
+
~accountNames=eventConfig.accounts,
|
|
821
|
+
)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
{
|
|
825
|
+
index: -1,
|
|
826
|
+
eventConfig: (eventConfig :> Internal.eventConfig),
|
|
827
|
+
handler,
|
|
828
|
+
contractRegister,
|
|
829
|
+
isWildcard,
|
|
830
|
+
filterByAddresses: false,
|
|
831
|
+
dependsOnAddresses: Internal.dependsOnAddresses(~isWildcard, ~filterByAddresses=false),
|
|
832
|
+
// `where.block.slot._gte` overrides the program-level startBlock when
|
|
833
|
+
// present, mirroring EVM's `where.block.number._gte`.
|
|
834
|
+
startBlock: switch resolvedWhere.startBlock {
|
|
835
|
+
| Some(_) as sb => sb
|
|
836
|
+
| None => startBlock
|
|
837
|
+
},
|
|
838
|
+
fieldSelection: switch fieldSelection {
|
|
839
|
+
| Some(fieldSelection) => fieldSelection
|
|
840
|
+
| None => eventConfig.fieldSelection
|
|
841
|
+
},
|
|
842
|
+
accountFilters: resolvedWhere.accountFilters,
|
|
843
|
+
isInner: resolvedWhere.isInner,
|
|
844
|
+
}
|
|
702
845
|
}
|
|
703
846
|
|
|
704
847
|
// ============== Build Fuel event config ==============
|
|
@@ -14,6 +14,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
14
14
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
15
15
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
16
16
|
import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
|
|
17
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
17
18
|
|
|
18
19
|
let paramMetaSchema = S$RescriptSchema.recursive(self => S$RescriptSchema.object(s => ({
|
|
19
20
|
name: s.f("name", S$RescriptSchema.string),
|
|
@@ -261,17 +262,13 @@ let svmSelectionKinds = [
|
|
|
261
262
|
"log"
|
|
262
263
|
];
|
|
263
264
|
|
|
264
|
-
function quotedJoin(names) {
|
|
265
|
-
return names.map(name => `"` + name + `"`).join(", ");
|
|
266
|
-
}
|
|
267
|
-
|
|
268
265
|
function validateFieldsShapeOrThrow(fields, registration, validKeys, shapeNoun) {
|
|
269
266
|
if (typeof fields !== "object" || fields === null || Array.isArray(fields)) {
|
|
270
267
|
Stdlib_JsError.throwWithMessage(`The fields option of ` + registration + ` must be an object of ` + shapeNoun + ` field names.`);
|
|
271
268
|
}
|
|
272
269
|
Object.keys(fields).forEach(key => {
|
|
273
270
|
if (!validKeys.includes(key)) {
|
|
274
|
-
return Stdlib_JsError.throwWithMessage(`Invalid "` + key + `" key in the fields option of ` + registration + `. Valid keys: ` + quotedJoin(validKeys) + `.`);
|
|
271
|
+
return Stdlib_JsError.throwWithMessage(`Invalid "` + key + `" key in the fields option of ` + registration + `. Valid keys: ` + Utils.$$Array.quotedJoin(validKeys) + `.`);
|
|
275
272
|
}
|
|
276
273
|
});
|
|
277
274
|
}
|
|
@@ -295,7 +292,7 @@ function parseFieldsOrThrow(fields, valid, kind, registration, rejectEmptyOpt) {
|
|
|
295
292
|
) : [];
|
|
296
293
|
fields$1.forEach(name => {
|
|
297
294
|
if (!valid.has(name)) {
|
|
298
|
-
Stdlib_JsError.throwWithMessage(`Invalid "` + name + `" field in the fields.` + kind + ` option of ` + registration + `. Valid ` + kind + ` fields: ` + quotedJoin(Array.from(valid)) + `.`);
|
|
295
|
+
Stdlib_JsError.throwWithMessage(`Invalid "` + name + `" field in the fields.` + kind + ` option of ` + registration + `. Valid ` + kind + ` fields: ` + Utils.$$Array.quotedJoin(Array.from(valid)) + `.`);
|
|
299
296
|
}
|
|
300
297
|
if (seen.has(name)) {
|
|
301
298
|
Stdlib_JsError.throwWithMessage(`Duplicate "` + name + `" field in the fields.` + kind + ` option of ` + registration + `.`);
|
|
@@ -420,7 +417,7 @@ let validSvmLogFields = new Set([
|
|
|
420
417
|
]);
|
|
421
418
|
|
|
422
419
|
function resolveSvmInlineFieldSelection(fields, contractName, eventName) {
|
|
423
|
-
let registration = `the "` + eventName + `"
|
|
420
|
+
let registration = `the "` + eventName + `" instruction on program "` + contractName + `"`;
|
|
424
421
|
validateFieldsShapeOrThrow(fields, registration, svmSelectionKinds, "instruction, transaction, accountActivity, block and log");
|
|
425
422
|
let accountActivity = selectionList(fields, "accountActivity");
|
|
426
423
|
let log = selectionList(fields, "log");
|
|
@@ -436,7 +433,7 @@ function resolveSvmInlineFieldSelection(fields, contractName, eventName) {
|
|
|
436
433
|
return Internal.makeFieldSelection(blockFields, transactionFields, Primitive_option.some(instructionFields), Primitive_option.some(accountActivityFields), Primitive_option.some(logFields), Svm.eventBlockFieldMask, Svm.eventTransactionFieldMask);
|
|
437
434
|
}
|
|
438
435
|
|
|
439
|
-
function buildSvmInstructionEventConfig(contractName, instructionName, programId, discriminator,
|
|
436
|
+
function buildSvmInstructionEventConfig(contractName, instructionName, programId, discriminator, accountsOpt, argsOpt, definedTypesOpt) {
|
|
440
437
|
let accounts = accountsOpt !== undefined ? accountsOpt : [];
|
|
441
438
|
let args = argsOpt !== undefined ? argsOpt : null;
|
|
442
439
|
let definedTypes = definedTypesOpt !== undefined ? definedTypesOpt : null;
|
|
@@ -451,16 +448,121 @@ function buildSvmInstructionEventConfig(contractName, instructionName, programId
|
|
|
451
448
|
fieldSelection: fieldSelection,
|
|
452
449
|
programId: programId,
|
|
453
450
|
discriminator: discriminator,
|
|
454
|
-
discriminatorByteLen: discriminatorByteLen,
|
|
455
|
-
accountFilters: accountFilters,
|
|
456
|
-
isInner: isInner,
|
|
457
451
|
accounts: accounts,
|
|
458
452
|
args: args,
|
|
459
453
|
definedTypes: definedTypes
|
|
460
454
|
};
|
|
461
455
|
}
|
|
462
456
|
|
|
463
|
-
|
|
457
|
+
let validSvmWhereKeys = [
|
|
458
|
+
"accounts",
|
|
459
|
+
"isInner",
|
|
460
|
+
"block"
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
let svmBlockFilterSchema = S$RescriptSchema.strict(S$RescriptSchema.object(s => ({
|
|
464
|
+
slot: s.f("slot", S$RescriptSchema.option(LogSelection.eventBlockRangeSchema))
|
|
465
|
+
})));
|
|
466
|
+
|
|
467
|
+
function resolveSvmWhereOrThrow(where, contractName, eventName, accountNames) {
|
|
468
|
+
let invalid = message => Stdlib_JsError.throwWithMessage(`Invalid where configuration for the "` + eventName + `" instruction on program "` + contractName + `". ` + message);
|
|
469
|
+
let obj;
|
|
470
|
+
obj = typeof where === "object" && where !== null && !Array.isArray(where) ? where : invalid("Expected an object.");
|
|
471
|
+
Utils.Dict.forEachWithKey(obj, (param, key) => {
|
|
472
|
+
if (!validSvmWhereKeys.includes(key)) {
|
|
473
|
+
return invalid(`Unknown field "` + key + `". Valid fields: ` + Utils.$$Array.quotedJoin(validSvmWhereKeys) + `.`);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
let match = obj["isInner"];
|
|
477
|
+
let isInner = match !== undefined ? (
|
|
478
|
+
typeof match === "boolean" ? match : invalid(`The "isInner" filter must be a boolean.`)
|
|
479
|
+
) : undefined;
|
|
480
|
+
let parseGroup = group => Object.entries(group).map(param => {
|
|
481
|
+
let name = param[0];
|
|
482
|
+
let position = accountNames.indexOf(name);
|
|
483
|
+
let position$1 = position !== -1 ? (
|
|
484
|
+
position >= 10 ? invalid(`Account "` + name + `" is at position ` + position.toString() + `, and only the first ` + (10).toString() + ` accounts of an instruction can be filtered.`) : position
|
|
485
|
+
) : (
|
|
486
|
+
Utils.$$Array.isEmpty(accountNames) ? invalid("The instruction has no named accounts to filter on. Add `accounts` and `args` to it in config.yaml, or attach an IDL.") : invalid(`The instruction has no account named "` + name + `" to filter on. Named accounts: ` + Utils.$$Array.quotedJoin(accountNames) + `.`)
|
|
487
|
+
);
|
|
488
|
+
let values = normalizeOrThrow(param[1]);
|
|
489
|
+
if (Utils.$$Array.isEmpty(values)) {
|
|
490
|
+
invalid(`The "` + name + `" filter must list at least one pubkey.`);
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
position: position$1,
|
|
494
|
+
values: values.map(value => {
|
|
495
|
+
if (typeof value === "string") {
|
|
496
|
+
if (Core.getAddon().isSvmPubkey(value)) {
|
|
497
|
+
return value;
|
|
498
|
+
} else {
|
|
499
|
+
return invalid(`The "` + name + `" filter value "` + value + `" is not a base58 SVM pubkey.`);
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
return invalid(`The "` + name + `" filter must list base58 pubkeys as strings.`);
|
|
503
|
+
}
|
|
504
|
+
})
|
|
505
|
+
};
|
|
506
|
+
});
|
|
507
|
+
let match$1 = obj["accounts"];
|
|
508
|
+
let groups;
|
|
509
|
+
if (match$1 !== undefined) {
|
|
510
|
+
if (Array.isArray(match$1)) {
|
|
511
|
+
groups = match$1.map(entry => {
|
|
512
|
+
if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) {
|
|
513
|
+
return parseGroup(entry);
|
|
514
|
+
} else {
|
|
515
|
+
return invalid(`Each entry in "accounts" must be an object.`);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
} else if (match$1 === null) {
|
|
519
|
+
groups = invalid(`Expected "accounts" to be an object or an array of objects.`);
|
|
520
|
+
} else {
|
|
521
|
+
switch (typeof match$1) {
|
|
522
|
+
case "object" :
|
|
523
|
+
groups = [parseGroup(match$1)];
|
|
524
|
+
break;
|
|
525
|
+
default:
|
|
526
|
+
groups = invalid(`Expected "accounts" to be an object or an array of objects.`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
groups = [];
|
|
531
|
+
}
|
|
532
|
+
let accountFilters = groups.some(Utils.$$Array.isEmpty) ? [] : groups;
|
|
533
|
+
let block = obj["block"];
|
|
534
|
+
let startBlock;
|
|
535
|
+
if (block !== undefined) {
|
|
536
|
+
let filter;
|
|
537
|
+
try {
|
|
538
|
+
filter = S$RescriptSchema.parseOrThrow(block, svmBlockFilterSchema);
|
|
539
|
+
} catch (raw_exn) {
|
|
540
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
541
|
+
if (exn.RE_EXN_ID === S$RescriptSchema.Raised) {
|
|
542
|
+
filter = invalid(`The "block" filter is invalid: ` + Utils.prettifyExn(exn._1) + `. Only \`_gte\` is supported on instruction filters — use \`indexer.onSlot\` for \`_lte\` or \`_every\`.`);
|
|
543
|
+
} else {
|
|
544
|
+
throw exn;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
let match$2 = filter.slot;
|
|
548
|
+
startBlock = match$2 !== undefined ? match$2._gte : undefined;
|
|
549
|
+
} else {
|
|
550
|
+
startBlock = undefined;
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
accountFilters: accountFilters,
|
|
554
|
+
isInner: isInner,
|
|
555
|
+
startBlock: startBlock
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function buildSvmOnEventRegistration(eventConfig, isWildcard, handler, contractRegister, where, fieldSelection, startBlock) {
|
|
560
|
+
let resolvedWhere = where !== undefined ? resolveSvmWhereOrThrow(where, eventConfig.contractName, eventConfig.name, eventConfig.accounts) : ({
|
|
561
|
+
accountFilters: [],
|
|
562
|
+
isInner: undefined,
|
|
563
|
+
startBlock: undefined
|
|
564
|
+
});
|
|
565
|
+
let sb = resolvedWhere.startBlock;
|
|
464
566
|
return {
|
|
465
567
|
index: -1,
|
|
466
568
|
eventConfig: eventConfig,
|
|
@@ -469,8 +571,10 @@ function buildSvmOnEventRegistration(eventConfig, isWildcard, handler, contractR
|
|
|
469
571
|
isWildcard: isWildcard,
|
|
470
572
|
filterByAddresses: false,
|
|
471
573
|
dependsOnAddresses: Internal.dependsOnAddresses(isWildcard, false),
|
|
472
|
-
startBlock: startBlock,
|
|
473
|
-
fieldSelection: fieldSelection !== undefined ? fieldSelection : eventConfig.fieldSelection
|
|
574
|
+
startBlock: sb !== undefined ? sb : startBlock,
|
|
575
|
+
fieldSelection: fieldSelection !== undefined ? fieldSelection : eventConfig.fieldSelection,
|
|
576
|
+
accountFilters: resolvedWhere.accountFilters,
|
|
577
|
+
isInner: resolvedWhere.isInner
|
|
474
578
|
};
|
|
475
579
|
}
|
|
476
580
|
|
|
@@ -542,6 +646,8 @@ function buildFuelOnEventRegistration(eventConfig, isWildcard, handler, contract
|
|
|
542
646
|
};
|
|
543
647
|
}
|
|
544
648
|
|
|
649
|
+
let filterableAccountCount = 10;
|
|
650
|
+
|
|
545
651
|
export {
|
|
546
652
|
paramMetaSchema,
|
|
547
653
|
normalizeOrThrow,
|
|
@@ -561,7 +667,6 @@ export {
|
|
|
561
667
|
rawEventBlockFields,
|
|
562
668
|
evmSelectionKinds,
|
|
563
669
|
svmSelectionKinds,
|
|
564
|
-
quotedJoin,
|
|
565
670
|
validateFieldsShapeOrThrow,
|
|
566
671
|
selectionList,
|
|
567
672
|
validBlockFields,
|
|
@@ -578,6 +683,10 @@ export {
|
|
|
578
683
|
validSvmLogFields,
|
|
579
684
|
resolveSvmInlineFieldSelection,
|
|
580
685
|
buildSvmInstructionEventConfig,
|
|
686
|
+
validSvmWhereKeys,
|
|
687
|
+
svmBlockFilterSchema,
|
|
688
|
+
filterableAccountCount,
|
|
689
|
+
resolveSvmWhereOrThrow,
|
|
581
690
|
buildSvmOnEventRegistration,
|
|
582
691
|
buildFuelEventConfig,
|
|
583
692
|
buildFuelOnEventRegistration,
|