envio 3.0.0-alpha.20 → 3.0.0-alpha.21

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.
Files changed (41) hide show
  1. package/index.d.ts +114 -33
  2. package/package.json +11 -6
  3. package/src/Config.res +142 -157
  4. package/src/Config.res.mjs +107 -118
  5. package/src/Env.res +4 -0
  6. package/src/Env.res.mjs +6 -0
  7. package/src/Envio.res +57 -0
  8. package/src/Envio.res.mjs +39 -1
  9. package/src/EventConfigBuilder.res +408 -0
  10. package/src/EventConfigBuilder.res.mjs +376 -0
  11. package/src/GlobalState.res +43 -9
  12. package/src/GlobalState.res.mjs +118 -10
  13. package/src/HandlerRegister.res +1 -1
  14. package/src/HandlerRegister.resi +1 -1
  15. package/src/Internal.gen.ts +2 -0
  16. package/src/Internal.res +38 -3
  17. package/src/Main.res +14 -1
  18. package/src/Main.res.mjs +10 -8
  19. package/src/SimulateItems.res +353 -0
  20. package/src/SimulateItems.res.mjs +335 -0
  21. package/src/TestIndexer.res +392 -171
  22. package/src/TestIndexer.res.mjs +285 -114
  23. package/src/bindings/EventSource.res +3 -1
  24. package/src/bindings/Vitest.res +3 -1
  25. package/src/sources/HyperFuelSource.res +2 -0
  26. package/src/sources/HyperFuelSource.res.mjs +2 -0
  27. package/src/sources/HyperSync.res +37 -66
  28. package/src/sources/HyperSync.res.mjs +51 -67
  29. package/src/sources/HyperSync.resi +2 -4
  30. package/src/sources/HyperSyncClient.res +28 -1
  31. package/src/sources/HyperSyncClient.res.mjs +9 -0
  32. package/src/sources/HyperSyncHeightStream.res +31 -4
  33. package/src/sources/HyperSyncHeightStream.res.mjs +22 -2
  34. package/src/sources/HyperSyncSource.res +4 -4
  35. package/src/sources/HyperSyncSource.res.mjs +4 -2
  36. package/src/sources/RpcSource.res +132 -22
  37. package/src/sources/RpcSource.res.mjs +2 -0
  38. package/src/sources/SimulateSource.res +59 -0
  39. package/src/sources/SimulateSource.res.mjs +50 -0
  40. package/src/sources/SourceManager.res +42 -5
  41. package/src/sources/SourceManager.res.mjs +44 -12
package/index.d.ts CHANGED
@@ -15,6 +15,18 @@ export type {
15
15
  import type { Address } from "./src/Types.ts";
16
16
  export type { EffectCaller, Address } from "./src/Types.ts";
17
17
 
18
+ export const TestHelpers: {
19
+ Addresses: {
20
+ readonly mockAddresses: readonly [
21
+ Address, Address, Address, Address, Address,
22
+ Address, Address, Address, Address, Address,
23
+ Address, Address, Address, Address, Address,
24
+ Address, Address, Address, Address, Address,
25
+ ];
26
+ readonly defaultAddress: Address;
27
+ };
28
+ };
29
+
18
30
  /** Utility type to expand/flatten complex types for better IDE display. */
19
31
  export type Prettify<T> = { [K in keyof T]: T[K] } & {};
20
32
 
@@ -378,11 +390,11 @@ type SvmChain<Id extends number = number> = {
378
390
  type IndexerConfigTypes = {
379
391
  evm?: {
380
392
  chains: Record<string, { id: number }>;
381
- contracts?: Record<string, {}>;
393
+ contracts?: Record<string, Record<string, { eventName: string }>>;
382
394
  };
383
395
  fuel?: {
384
396
  chains: Record<string, { id: number }>;
385
- contracts?: Record<string, {}>;
397
+ contracts?: Record<string, Record<string, { eventName: string }>>;
386
398
  };
387
399
  svm?: { chains: Record<string, { id: number }> };
388
400
  entities?: Record<string, object>;
@@ -526,12 +538,74 @@ export type IndexerFromConfig<Config extends IndexerConfigTypes> = Prettify<
526
538
 
527
539
  // ============== Test Indexer Types ==============
528
540
 
529
- /** Configuration for a single chain in the test indexer. */
530
- export type TestIndexerChainConfig = {
531
- /** The block number to start processing from. */
532
- startBlock: number;
533
- /** The block number to stop processing at. */
534
- endBlock: number;
541
+ /** Simulate item type for EVM ecosystem. */
542
+ type EvmSimulateItem<Config extends IndexerConfigTypes> =
543
+ Config["evm"] extends { contracts: infer Contracts extends Record<string, Record<string, any>> }
544
+ ? {
545
+ [C in keyof Contracts]: {
546
+ [E in keyof Contracts[C]]: {
547
+ /** The contract name as defined in config.yaml. */
548
+ contract: C;
549
+ /** The event name as defined in the contract ABI. */
550
+ event: E;
551
+ /** Override the source address. Defaults to the first contract address. */
552
+ srcAddress?: Address;
553
+ /** Override the log index. Auto-increments by default. */
554
+ logIndex?: number;
555
+ /** Override block fields. */
556
+ block?: Partial<Contracts[C][E]["block"]>;
557
+ /** Override transaction fields. */
558
+ transaction?: Partial<Contracts[C][E]["transaction"]>;
559
+ /** Event parameters. Keys match the event's parameter names. */
560
+ params?: Partial<Contracts[C][E]["params"]>;
561
+ };
562
+ }[keyof Contracts[C]];
563
+ }[keyof Contracts]
564
+ : never;
565
+
566
+ /** Simulate item type for Fuel ecosystem. */
567
+ type FuelSimulateItem<Config extends IndexerConfigTypes> =
568
+ Config["fuel"] extends { contracts: infer Contracts extends Record<string, Record<string, any>> }
569
+ ? {
570
+ [C in keyof Contracts]: {
571
+ [E in keyof Contracts[C]]: {
572
+ /** The contract name as defined in config.yaml. */
573
+ contract: C;
574
+ /** The event name as defined in the contract ABI. */
575
+ event: E;
576
+ /** Override the source address. Defaults to the first contract address. */
577
+ srcAddress?: Address;
578
+ /** Override the log index. Auto-increments by default. */
579
+ logIndex?: number;
580
+ /** Override block fields. */
581
+ block?: Partial<Contracts[C][E]["block"]>;
582
+ /** Override transaction fields. */
583
+ transaction?: Partial<Contracts[C][E]["transaction"]>;
584
+ /** Event parameters. Keys match the event's parameter names. */
585
+ params: Contracts[C][E]["params"];
586
+ };
587
+ }[keyof Contracts[C]];
588
+ }[keyof Contracts]
589
+ : never;
590
+
591
+ /** Configuration for a single EVM chain in the test indexer. */
592
+ type EvmTestIndexerChainConfig<Config extends IndexerConfigTypes> = {
593
+ /** The block number to start processing from. Defaults to config startBlock or progressBlock+1. */
594
+ startBlock?: number;
595
+ /** The block number to stop processing at. Defaults to max simulate block number when simulate is provided. */
596
+ endBlock?: number;
597
+ /** Simulate items to process instead of fetching from real sources. */
598
+ simulate?: EvmSimulateItem<Config>[];
599
+ };
600
+
601
+ /** Configuration for a single Fuel chain in the test indexer. */
602
+ type FuelTestIndexerChainConfig<Config extends IndexerConfigTypes> = {
603
+ /** The block number to start processing from. Defaults to config startBlock or progressBlock+1. */
604
+ startBlock?: number;
605
+ /** The block number to stop processing at. Defaults to max simulate block height when simulate is provided. */
606
+ endBlock?: number;
607
+ /** Simulate items to process instead of fetching from real sources. */
608
+ simulate?: FuelSimulateItem<Config>[];
535
609
  };
536
610
 
537
611
  /** Entity change value containing sets and/or deleted IDs. */
@@ -555,9 +629,13 @@ type ConfigEntities<Config extends IndexerConfigTypes> =
555
629
  Config["entities"] extends Record<string, object> ? Config["entities"] : {};
556
630
 
557
631
  /** Entity operations available on test indexer for direct entity manipulation. */
558
- type EntityOps<Entity> = {
632
+ type TestIndexerEntityOperations<Entity> = {
559
633
  /** Get an entity by ID. Returns undefined if not found. */
560
634
  readonly get: (id: string) => Promise<Entity | undefined>;
635
+ /** Get an entity by ID or throw if not found. */
636
+ readonly getOrThrow: (id: string, message?: string) => Promise<Entity>;
637
+ /** Get all entities. */
638
+ readonly getAll: () => Promise<Entity[]>;
561
639
  /** Set (create or update) an entity. */
562
640
  readonly set: (entity: Entity) => void;
563
641
  };
@@ -566,8 +644,6 @@ type EntityOps<Entity> = {
566
644
  type EntityChange<Config extends IndexerConfigTypes> = {
567
645
  /** The block where the changes occurred. */
568
646
  readonly block: number;
569
- /** The block hash (if available). */
570
- readonly blockHash?: string;
571
647
  /** The chain ID. */
572
648
  readonly chainId: number;
573
649
  /** Number of events processed in this block. */
@@ -583,34 +659,39 @@ type EntityChange<Config extends IndexerConfigTypes> = {
583
659
  };
584
660
 
585
661
 
586
- // Helper to extract chain IDs from config for test indexer
587
- type TestIndexerChainIds<Config extends IndexerConfigTypes> =
588
- HasEvm<Config> extends true
589
- ? Config["evm"] extends { chains: infer Chains }
590
- ? Chains extends Record<string, { id: number }>
591
- ? Chains[keyof Chains]["id"]
592
- : never
662
+ // Helper to extract chain IDs per ecosystem
663
+ type EvmChainIds<Config extends IndexerConfigTypes> =
664
+ Config["evm"] extends { chains: infer Chains }
665
+ ? Chains extends Record<string, { id: number }>
666
+ ? Chains[keyof Chains]["id"]
593
667
  : never
594
- : HasFuel<Config> extends true
595
- ? Config["fuel"] extends { chains: infer Chains }
596
- ? Chains extends Record<string, { id: number }>
597
- ? Chains[keyof Chains]["id"]
598
- : never
599
- : never
600
- : HasSvm<Config> extends true
601
- ? Config["svm"] extends { chains: infer Chains }
602
- ? Chains extends Record<string, { id: number }>
603
- ? Chains[keyof Chains]["id"]
604
- : never
668
+ : never;
669
+
670
+ type FuelChainIds<Config extends IndexerConfigTypes> =
671
+ Config["fuel"] extends { chains: infer Chains }
672
+ ? Chains extends Record<string, { id: number }>
673
+ ? Chains[keyof Chains]["id"]
605
674
  : never
606
675
  : never;
607
676
 
677
+ // Per-ecosystem chain config mappings
678
+ type EvmTestChains<Config extends IndexerConfigTypes> =
679
+ HasEvm<Config> extends true
680
+ ? { [K in EvmChainIds<Config>]?: EvmTestIndexerChainConfig<Config> }
681
+ : {};
682
+
683
+ type FuelTestChains<Config extends IndexerConfigTypes> =
684
+ HasFuel<Config> extends true
685
+ ? { [K in FuelChainIds<Config>]?: FuelTestIndexerChainConfig<Config> }
686
+ : {};
687
+
608
688
  /** Process configuration for the test indexer, with chains keyed by chain ID. */
609
689
  export type TestIndexerProcessConfig<Config extends IndexerConfigTypes> = {
610
690
  /** Chain configurations keyed by chain ID. Each chain specifies start and end blocks. */
611
- chains: {
612
- [K in TestIndexerChainIds<Config>]?: TestIndexerChainConfig;
613
- };
691
+ chains: Prettify<
692
+ EvmTestChains<Config> &
693
+ FuelTestChains<Config>
694
+ >;
614
695
  };
615
696
 
616
697
  /**
@@ -629,7 +710,7 @@ export type TestIndexerFromConfig<Config extends IndexerConfigTypes> = {
629
710
  ? SingleEcosystemChains<Config>
630
711
  : MultiEcosystemChains<Config>) & {
631
712
  /** Entity operations for direct manipulation outside of handlers. */
632
- readonly [K in keyof ConfigEntities<Config>]: EntityOps<
713
+ readonly [K in keyof ConfigEntities<Config>]: TestIndexerEntityOperations<
633
714
  ConfigEntities<Config>[K]
634
715
  >;
635
716
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envio",
3
- "version": "3.0.0-alpha.20",
3
+ "version": "3.0.0-alpha.21",
4
4
  "type": "module",
5
5
  "description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
6
6
  "bin": "./bin.mjs",
@@ -43,7 +43,12 @@
43
43
  "@clickhouse/client": "1.17.0",
44
44
  "@elastic/ecs-pino-format": "1.4.0",
45
45
  "@envio-dev/hyperfuel-client": "1.2.2",
46
- "@envio-dev/hypersync-client": "1.1.0",
46
+ "@envio-dev/hypersync-client": "1.3.0",
47
+ "@fuel-ts/crypto": "0.96.1",
48
+ "@fuel-ts/errors": "0.96.1",
49
+ "@fuel-ts/hasher": "0.96.1",
50
+ "@fuel-ts/math": "0.96.1",
51
+ "@fuel-ts/utils": "0.96.1",
47
52
  "bignumber.js": "9.3.1",
48
53
  "eventsource": "4.1.0",
49
54
  "express": "4.19.2",
@@ -65,9 +70,9 @@
65
70
  "tsx": "4.21.0"
66
71
  },
67
72
  "optionalDependencies": {
68
- "envio-linux-x64": "3.0.0-alpha.20",
69
- "envio-linux-arm64": "3.0.0-alpha.20",
70
- "envio-darwin-x64": "3.0.0-alpha.20",
71
- "envio-darwin-arm64": "3.0.0-alpha.20"
73
+ "envio-linux-x64": "3.0.0-alpha.21",
74
+ "envio-linux-arm64": "3.0.0-alpha.21",
75
+ "envio-darwin-x64": "3.0.0-alpha.21",
76
+ "envio-darwin-arm64": "3.0.0-alpha.21"
72
77
  }
73
78
  }
package/src/Config.res CHANGED
@@ -21,19 +21,6 @@ type contract = {
21
21
  eventSignatures: array<string>,
22
22
  }
23
23
 
24
- type codegenContract = {
25
- name: string,
26
- addresses: array<string>,
27
- events: array<Internal.eventConfig>,
28
- startBlock: option<int>,
29
- }
30
-
31
- // Source config is now parsed from internal.config.json and sources are created lazily
32
- type codegenChain = {
33
- id: int,
34
- contracts: array<codegenContract>,
35
- }
36
-
37
24
  // Source config parsed from internal.config.json - sources are created lazily in ChainFetcher
38
25
  type evmRpcConfig = {
39
26
  url: string,
@@ -71,7 +58,9 @@ type sourceSync = {
71
58
  pollingInterval: int,
72
59
  }
73
60
 
74
- type multichain = | @as("ordered") Ordered | @as("unordered") Unordered
61
+ type multichain = Internal.multichain =
62
+ | @as("ordered") Ordered
63
+ | @as("unordered") Unordered
75
64
 
76
65
  type contractHandler = {
77
66
  name: string,
@@ -185,6 +174,13 @@ let rpcConfigSchema = S.schema(s =>
185
174
  }
186
175
  )
187
176
 
177
+ let chainContractSchema = S.schema(s =>
178
+ {
179
+ "addresses": s.matches(S.option(S.array(S.string))),
180
+ "startBlock": s.matches(S.option(S.int)),
181
+ }
182
+ )
183
+
188
184
  let publicConfigChainSchema = S.schema(s =>
189
185
  {
190
186
  "id": s.matches(S.int),
@@ -197,6 +193,8 @@ let publicConfigChainSchema = S.schema(s =>
197
193
  "rpcs": s.matches(S.option(S.array(rpcConfigSchema))),
198
194
  // SVM source config
199
195
  "rpc": s.matches(S.option(S.string)),
196
+ // Per-chain contract data (addresses and optional start block)
197
+ "contracts": s.matches(S.option(S.dict(chainContractSchema))),
200
198
  }
201
199
  )
202
200
 
@@ -204,8 +202,11 @@ let contractEventItemSchema = S.schema(s =>
204
202
  {
205
203
  "event": s.matches(S.string),
206
204
  "name": s.matches(S.string),
207
- "blockFields": s.matches(S.option(S.array(S.string))),
208
- "transactionFields": s.matches(S.option(S.array(S.string))),
205
+ "sighash": s.matches(S.string),
206
+ "params": s.matches(S.option(S.array(EventConfigBuilder.eventParamSchema))),
207
+ "kind": s.matches(S.option(S.string)),
208
+ "blockFields": s.matches(S.option(S.array(Internal.evmBlockFieldSchema))),
209
+ "transactionFields": s.matches(S.option(S.array(Internal.evmTransactionFieldSchema))),
209
210
  }
210
211
  )
211
212
 
@@ -437,71 +438,7 @@ let publicConfigSchema = S.schema(s =>
437
438
  }
438
439
  )
439
440
 
440
- // Always-included block fields (number, timestamp, hash) are not in the JSON;
441
- // they're prepended at runtime so they're always present.
442
- let alwaysIncludedBlockFields: array<Internal.evmBlockField> = [Number, Timestamp, Hash]
443
-
444
- // Enrich EVM event configs with field selections from the JSON config.
445
- // Mutates the event configs in-place to set selectedBlockFields/selectedTransactionFields.
446
- let enrichEvmFieldSelections = (
447
- events: array<Internal.eventConfig>,
448
- ~jsonEvents: option<array<_>>,
449
- ~globalBlockFieldsSet: Utils.Set.t<Internal.evmBlockField>,
450
- ~globalTransactionFieldsSet: Utils.Set.t<Internal.evmTransactionField>,
451
- ) => {
452
- // Build a lookup by event name for events with per-event field overrides
453
- let fieldsByName: Js.Dict.t<_> = Js.Dict.empty()
454
- switch jsonEvents {
455
- | Some(jes) =>
456
- jes->Array.forEach(je => {
457
- let name = je["name"]
458
- if je["blockFields"] != None || je["transactionFields"] != None {
459
- fieldsByName->Js.Dict.set(name, je)
460
- }
461
- })
462
- | None => ()
463
- }
464
- events->Js.Array2.forEachi((event, i) => {
465
- let evmEvent = event->(Utils.magic: Internal.eventConfig => Internal.evmEventConfig)
466
- let (selectedBlockFields, selectedTransactionFields) = switch fieldsByName->Js.Dict.get(
467
- evmEvent.name,
468
- ) {
469
- | Some(je) => (
470
- switch je["blockFields"] {
471
- | Some(fields) =>
472
- // Prepend always-included block fields for per-event overrides too
473
- Utils.Set.fromArray(
474
- Array.concat(
475
- alwaysIncludedBlockFields,
476
- fields->(Utils.magic: array<string> => array<Internal.evmBlockField>),
477
- ),
478
- )
479
- | None => globalBlockFieldsSet
480
- },
481
- switch je["transactionFields"] {
482
- | Some(fields) =>
483
- Utils.Set.fromArray(
484
- fields->(Utils.magic: array<string> => array<Internal.evmTransactionField>),
485
- )
486
- | None => globalTransactionFieldsSet
487
- },
488
- )
489
- | None => (globalBlockFieldsSet, globalTransactionFieldsSet)
490
- }
491
- events->Js.Array2.unsafe_set(
492
- i,
493
- {...evmEvent, selectedBlockFields, selectedTransactionFields}->(
494
- Utils.magic: Internal.evmEventConfig => Internal.eventConfig
495
- ),
496
- )
497
- })
498
- }
499
-
500
- let fromPublic = (
501
- publicConfigJson: Js.Json.t,
502
- ~codegenChains: array<codegenChain>=[],
503
- ~maxAddrInPartition=5000,
504
- ) => {
441
+ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => {
505
442
  // Parse public config
506
443
  let publicConfig = try publicConfigJson->S.parseOrThrow(publicConfigSchema) catch {
507
444
  | S.Raised(exn) =>
@@ -533,7 +470,7 @@ let fromPublic = (
533
470
  | None => false
534
471
  }
535
472
 
536
- // Parse ABIs from public config
473
+ // Parse contract configs (ABIs, events, handlers)
537
474
  let publicContractsConfig = switch (ecosystemName, publicConfig["evm"], publicConfig["fuel"]) {
538
475
  | (Ecosystem.Evm, Some(evm), _) => evm["contracts"]
539
476
  | (Ecosystem.Fuel, _, Some(fuel)) => fuel["contracts"]
@@ -545,19 +482,21 @@ let fromPublic = (
545
482
  | Some(evm) => (
546
483
  Utils.Set.fromArray(
547
484
  Array.concat(
548
- alwaysIncludedBlockFields,
485
+ EventConfigBuilder.alwaysIncludedBlockFields,
549
486
  evm["globalBlockFields"]->Option.getWithDefault([]),
550
487
  ),
551
488
  ),
552
489
  Utils.Set.fromArray(evm["globalTransactionFields"]->Option.getWithDefault([])),
553
490
  )
554
- | None => (Utils.Set.fromArray(alwaysIncludedBlockFields), Utils.Set.make())
491
+ | None => (Utils.Set.fromArray(EventConfigBuilder.alwaysIncludedBlockFields), Utils.Set.make())
555
492
  }
556
493
 
557
- // Store ABI and event signatures for each contract
558
- let contractsWithAbis: Js.Dict.t<(EvmTypes.Abi.t, array<string>)> = Js.Dict.empty()
559
- // Per-event field selection overrides, keyed by capitalized contract name
560
- let jsonEventsByContract: Js.Dict.t<array<_>> = Js.Dict.empty()
494
+ // Build contract data lookup: ABI, event signatures, event configs (keyed by capitalized name)
495
+ let contractDataByName: Js.Dict.t<{
496
+ "abi": EvmTypes.Abi.t,
497
+ "eventSignatures": array<string>,
498
+ "events": option<array<_>>,
499
+ }> = Js.Dict.empty()
561
500
  switch publicContractsConfig {
562
501
  | Some(contractsDict) =>
563
502
  contractsDict
@@ -569,68 +508,80 @@ let fromPublic = (
569
508
  | Some(events) => events->Array.map(eventItem => eventItem["event"])
570
509
  | None => []
571
510
  }
572
- contractsWithAbis->Js.Dict.set(capitalizedName, (abi, eventSignatures))
573
- switch contractConfig["events"] {
574
- | Some(events) => jsonEventsByContract->Js.Dict.set(capitalizedName, events)
575
- | None => ()
576
- }
511
+ contractDataByName->Js.Dict.set(
512
+ capitalizedName,
513
+ {"abi": abi, "eventSignatures": eventSignatures, "events": contractConfig["events"]},
514
+ )
577
515
  })
578
516
  | None => ()
579
517
  }
580
518
 
581
- // Index codegenChains by id for efficient lookup
582
- let codegenChainById = Js.Dict.empty()
583
- codegenChains->Array.forEach(codegenChain => {
584
- codegenChainById->Js.Dict.set(codegenChain.id->Int.toString, codegenChain)
585
- })
586
-
587
- // Create a dictionary to store merged contracts with ABIs by chain id
588
- let contractsByChainId: Js.Dict.t<array<contract>> = Js.Dict.empty()
589
- codegenChains->Array.forEach(codegenChain => {
590
- let mergedContracts = codegenChain.contracts->Array.map(codegenContract => {
591
- switch contractsWithAbis->Js.Dict.get(codegenContract.name) {
592
- | Some((abi, eventSignatures)) =>
593
- // Parse addresses based on ecosystem and address format
594
- let parsedAddresses = codegenContract.addresses->Array.map(
595
- addressString => {
596
- switch ecosystemName {
597
- | Ecosystem.Evm =>
598
- if lowercaseAddresses {
599
- addressString->Address.Evm.fromStringLowercaseOrThrow
600
- } else {
601
- addressString->Address.Evm.fromStringOrThrow
602
- }
603
- | Ecosystem.Fuel | Ecosystem.Svm => addressString->Address.unsafeFromString
604
- }
605
- },
606
- )
607
-
608
- // Enrich EVM event configs with field selections from JSON config
609
- if ecosystemName == Ecosystem.Evm {
610
- enrichEvmFieldSelections(
611
- codegenContract.events,
612
- ~jsonEvents=jsonEventsByContract->Js.Dict.get(codegenContract.name),
519
+ // Build event configs for a contract from JSON event items
520
+ let buildContractEvents = (~contractName, ~events: option<array<_>>, ~abi) => {
521
+ switch events {
522
+ | None => []
523
+ | Some(eventItems) =>
524
+ eventItems->Array.map(eventItem => {
525
+ let eventName = eventItem["name"]
526
+ let sighash = eventItem["sighash"]
527
+ let params = eventItem["params"]->Option.getWithDefault([])
528
+ let kind = eventItem["kind"]
529
+ // Get handler registration data
530
+ let isWildcard = HandlerRegister.isWildcard(~contractName, ~eventName)
531
+ let handler = HandlerRegister.getHandler(~contractName, ~eventName)
532
+ let contractRegister = HandlerRegister.getContractRegister(~contractName, ~eventName)
533
+
534
+ switch ecosystemName {
535
+ | Ecosystem.Fuel =>
536
+ switch kind {
537
+ | Some(fuelKind) =>
538
+ (EventConfigBuilder.buildFuelEventConfig(
539
+ ~contractName,
540
+ ~eventName,
541
+ ~kind=fuelKind,
542
+ ~sighash,
543
+ ~rawAbi=abi->(Utils.magic: EvmTypes.Abi.t => Js.Json.t),
544
+ ~isWildcard,
545
+ ~handler,
546
+ ~contractRegister,
547
+ ) :> Internal.eventConfig)
548
+ | None =>
549
+ Js.Exn.raiseError(
550
+ `Fuel event ${contractName}.${eventName} is missing "kind" in internal config`,
551
+ )
552
+ }
553
+ | _ =>
554
+ (EventConfigBuilder.buildEvmEventConfig(
555
+ ~contractName,
556
+ ~eventName,
557
+ ~sighash,
558
+ ~params,
559
+ ~isWildcard,
560
+ ~handler,
561
+ ~contractRegister,
562
+ ~eventFilters=HandlerRegister.getEventFilters(~contractName, ~eventName),
563
+ ~blockFields=?eventItem["blockFields"],
564
+ ~transactionFields=?eventItem["transactionFields"],
613
565
  ~globalBlockFieldsSet,
614
566
  ~globalTransactionFieldsSet,
615
- )
567
+ ) :> Internal.eventConfig)
616
568
  }
617
- // Convert codegenContract to contract by adding abi and eventSignatures
618
- {
619
- name: codegenContract.name,
620
- abi,
621
- addresses: parsedAddresses,
622
- events: codegenContract.events,
623
- startBlock: codegenContract.startBlock,
624
- eventSignatures,
625
- }
626
- | None =>
627
- Js.Exn.raiseError(
628
- `Contract "${codegenContract.name}" is missing ABI in public config (internal.config.ts)`,
629
- )
569
+ })
570
+ }
571
+ }
572
+
573
+ // Parse address based on ecosystem and address format
574
+ let parseAddress = addressString => {
575
+ switch ecosystemName {
576
+ | Ecosystem.Evm =>
577
+ if lowercaseAddresses {
578
+ addressString->Address.Evm.fromStringLowercaseOrThrow
579
+ } else {
580
+ addressString->Address.Evm.fromStringOrThrow
630
581
  }
631
- })
632
- contractsByChainId->Js.Dict.set(codegenChain.id->Int.toString, mergedContracts)
633
- })
582
+ | Ecosystem.Fuel | Ecosystem.Svm => addressString->Address.unsafeFromString
583
+ }
584
+ }
634
585
 
635
586
  // Helper to convert parsed RPC config to evmRpcConfig
636
587
  let parseRpcSourceFor = (sourceFor: rpcSourceFor): Source.sourceFor => {
@@ -641,25 +592,45 @@ let fromPublic = (
641
592
  }
642
593
  }
643
594
 
644
- // Merge codegenChains with names from publicConfig
595
+ // Build chains from JSON config (no more codegenChains)
645
596
  let chains =
646
597
  publicChainsConfig
647
598
  ->Js.Dict.keys
648
599
  ->Js.Array2.map(chainName => {
649
600
  let publicChainConfig = publicChainsConfig->Js.Dict.unsafeGet(chainName)
650
601
  let chainId = publicChainConfig["id"]
651
- let codegenChain = switch codegenChainById->Js.Dict.get(chainId->Int.toString) {
652
- | Some(c) => c
653
- | None =>
654
- Js.Exn.raiseError(`Chain with id ${chainId->Int.toString} not found in codegen chains`)
655
- }
656
- let mergedContracts = switch contractsByChainId->Js.Dict.get(chainId->Int.toString) {
657
- | Some(contracts) => contracts
658
- | None =>
659
- Js.Exn.raiseError(
660
- `Contracts for chain with id ${chainId->Int.toString} not found in merged contracts`,
661
- )
662
- }
602
+
603
+ // Build contracts for this chain from per-chain contract data + contract configs
604
+ let chainContracts = publicChainConfig["contracts"]->Option.getWithDefault(Js.Dict.empty())
605
+ let contracts =
606
+ contractDataByName
607
+ ->Js.Dict.entries
608
+ ->Array.map(((capitalizedName, contractData)) => {
609
+ // Get per-chain contract data (addresses, startBlock)
610
+ let chainContract = chainContracts->Js.Dict.get(capitalizedName)
611
+ let addresses =
612
+ chainContract
613
+ ->Option.flatMap(cc => cc["addresses"])
614
+ ->Option.getWithDefault([])
615
+ ->Array.map(parseAddress)
616
+ let startBlock = chainContract->Option.flatMap(cc => cc["startBlock"])
617
+
618
+ // Build event configs from JSON (field selections resolved inline)
619
+ let events = buildContractEvents(
620
+ ~contractName=capitalizedName,
621
+ ~events=contractData["events"],
622
+ ~abi=contractData["abi"],
623
+ )
624
+
625
+ {
626
+ name: capitalizedName,
627
+ abi: contractData["abi"],
628
+ addresses,
629
+ events,
630
+ startBlock,
631
+ eventSignatures: contractData["eventSignatures"],
632
+ }
633
+ })
663
634
 
664
635
  // Build sourceConfig from the parsed chain config
665
636
  let sourceConfig = switch ecosystemName {
@@ -722,7 +693,7 @@ let fromPublic = (
722
693
 
723
694
  {
724
695
  name: chainName,
725
- id: codegenChain.id,
696
+ id: chainId,
726
697
  startBlock: publicChainConfig["startBlock"],
727
698
  endBlock: ?publicChainConfig["endBlock"],
728
699
  maxReorgDepth: switch ecosystemName {
@@ -731,7 +702,7 @@ let fromPublic = (
731
702
  | Ecosystem.Fuel | Ecosystem.Svm => 0
732
703
  },
733
704
  blockLag: publicChainConfig["blockLag"]->Option.getWithDefault(0),
734
- contracts: mergedContracts,
705
+ contracts,
735
706
  sourceConfig,
736
707
  }
737
708
  })
@@ -820,6 +791,20 @@ let fromPublic = (
820
791
  }
821
792
  }
822
793
 
794
+ let getEventConfig = (config: t, ~contractName, ~eventName) => {
795
+ config.chainMap
796
+ ->ChainMap.values
797
+ ->Js.Array2.reduce((acc, chain) => {
798
+ switch acc {
799
+ | Some(_) => acc
800
+ | None =>
801
+ chain.contracts
802
+ ->Js.Array2.find(c => c.name == contractName)
803
+ ->Belt.Option.flatMap(contract => contract.events->Js.Array2.find(e => e.name == eventName))
804
+ }
805
+ }, None)
806
+ }
807
+
823
808
  let shouldSaveHistory = (config, ~isInReorgThreshold) =>
824
809
  config.shouldSaveFullHistory || (config.shouldRollbackOnReorg && isInReorgThreshold)
825
810