envio 3.0.1 → 3.0.2-svm-alpha.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.
Files changed (41) hide show
  1. package/evm.schema.json +8 -8
  2. package/fuel.schema.json +12 -12
  3. package/index.d.ts +155 -1
  4. package/package.json +6 -7
  5. package/src/ChainFetcher.res +25 -1
  6. package/src/ChainFetcher.res.mjs +19 -1
  7. package/src/Config.res +156 -94
  8. package/src/Config.res.mjs +60 -97
  9. package/src/Core.res +32 -0
  10. package/src/Env.res.mjs +1 -2
  11. package/src/Envio.res +94 -0
  12. package/src/EventConfigBuilder.res +50 -0
  13. package/src/EventConfigBuilder.res.mjs +31 -0
  14. package/src/HandlerLoader.res +12 -1
  15. package/src/HandlerLoader.res.mjs +6 -1
  16. package/src/Internal.res +38 -0
  17. package/src/Main.res +53 -3
  18. package/src/Main.res.mjs +34 -2
  19. package/src/Persistence.res +2 -17
  20. package/src/Persistence.res.mjs +2 -14
  21. package/src/SimulateItems.res +23 -10
  22. package/src/SimulateItems.res.mjs +21 -6
  23. package/src/SvmTypes.res +9 -0
  24. package/src/SvmTypes.res.mjs +14 -0
  25. package/src/sources/EventRouter.res +65 -0
  26. package/src/sources/EventRouter.res.mjs +43 -0
  27. package/src/sources/HyperSyncClient.res +30 -157
  28. package/src/sources/HyperSyncClient.res.mjs +20 -6
  29. package/src/sources/HyperSyncSolanaClient.res +227 -0
  30. package/src/sources/HyperSyncSolanaClient.res.mjs +25 -0
  31. package/src/sources/HyperSyncSolanaSource.res +515 -0
  32. package/src/sources/HyperSyncSolanaSource.res.mjs +441 -0
  33. package/src/sources/HyperSyncSource.res +5 -8
  34. package/src/sources/HyperSyncSource.res.mjs +1 -8
  35. package/src/sources/RpcSource.res.mjs +1 -1
  36. package/src/sources/Svm.res +2 -2
  37. package/src/sources/Svm.res.mjs +3 -2
  38. package/src/tui/Tui.res +9 -2
  39. package/src/tui/Tui.res.mjs +19 -4
  40. package/src/tui/components/TuiData.res +3 -0
  41. package/svm.schema.json +345 -4
package/src/Config.res CHANGED
@@ -30,7 +30,7 @@ type evmRpcConfig = {
30
30
  type sourceConfig =
31
31
  | EvmSourceConfig({hypersync: option<string>, rpcs: array<evmRpcConfig>})
32
32
  | FuelSourceConfig({hypersync: string})
33
- | SvmSourceConfig({rpc: string})
33
+ | SvmSourceConfig({hypersync: option<string>, rpc: string})
34
34
  // For tests: pass custom sources directly
35
35
  | CustomSources(array<Source.t>)
36
36
 
@@ -201,6 +201,38 @@ let publicConfigChainSchema = S.schema(s =>
201
201
  }
202
202
  )
203
203
 
204
+ let svmEventDescriptorSchema = S.schema(s =>
205
+ {
206
+ "discriminator": s.matches(S.option(S.string)),
207
+ "discriminatorByteLen": s.matches(S.int),
208
+ "includeTransaction": s.matches(S.bool),
209
+ "includeLogs": s.matches(S.bool),
210
+ "accountFilters": s.matches(
211
+ S.option(
212
+ S.array(
213
+ S.schema(
214
+ s => {
215
+ "position": s.matches(S.int),
216
+ "values": s.matches(S.array(S.string)),
217
+ },
218
+ ),
219
+ ),
220
+ ),
221
+ ),
222
+ "isInner": s.matches(S.option(S.bool)),
223
+ "accounts": s.matches(S.option(S.array(S.string))),
224
+ "args": s.matches(S.option(S.json(~validate=false))),
225
+ }
226
+ )
227
+
228
+ let svmAbiSchema = S.schema(s =>
229
+ {
230
+ "programId": s.matches(S.string),
231
+ "definedTypes": s.matches(S.json(~validate=false)),
232
+ "source": s.matches(S.string),
233
+ }
234
+ )
235
+
204
236
  let contractEventItemSchema = S.schema(s =>
205
237
  {
206
238
  "event": s.matches(S.string),
@@ -210,6 +242,7 @@ let contractEventItemSchema = S.schema(s =>
210
242
  "kind": s.matches(S.option(S.string)),
211
243
  "blockFields": s.matches(S.option(S.array(Internal.evmBlockFieldSchema))),
212
244
  "transactionFields": s.matches(S.option(S.array(Internal.evmTransactionFieldSchema))),
245
+ "svm": s.matches(S.option(svmEventDescriptorSchema)),
213
246
  }
214
247
  )
215
248
 
@@ -219,6 +252,8 @@ let contractConfigSchema = S.schema(s =>
219
252
  "handler": s.matches(S.option(S.string)),
220
253
  // EVM-specific: event signatures for HyperSync queries
221
254
  "events": s.matches(S.option(S.array(contractEventItemSchema))),
255
+ // SVM-only: program-level Borsh schema (defined-types registry, source).
256
+ "svmAbi": s.matches(S.option(svmAbiSchema)),
222
257
  }
223
258
  )
224
259
 
@@ -226,6 +261,10 @@ let publicConfigEcosystemSchema = S.schema(s =>
226
261
  {
227
262
  "chains": s.matches(S.dict(publicConfigChainSchema)),
228
263
  "contracts": s.matches(S.option(S.dict(contractConfigSchema))),
264
+ // SVM-only alias: programs are the SVM analog of EVM/Fuel contracts.
265
+ // Parsed via the same `contractConfigSchema` and read in `fromPublic`'s
266
+ // `publicContractsConfig` switch.
267
+ "programs": s.matches(S.option(S.dict(contractConfigSchema))),
229
268
  }
230
269
  )
231
270
 
@@ -523,10 +562,19 @@ let fromPublic = (publicConfigJson: JSON.t) => {
523
562
  | None => false
524
563
  }
525
564
 
526
- // Parse contract configs (ABIs, events, handlers)
527
- let publicContractsConfig = switch (ecosystemName, publicConfig["evm"], publicConfig["fuel"]) {
528
- | (Ecosystem.Evm, Some(evm), _) => evm["contracts"]
529
- | (Ecosystem.Fuel, _, Some(fuel)) => fuel["contracts"]
565
+ // Parse contract configs (ABIs, events, handlers).
566
+ // SVM stores them under `svm.programs` in the public JSON — the per-program
567
+ // events drive `indexer.onInstruction` registration the same way EVM/Fuel
568
+ // contracts drive `onEvent`.
569
+ let publicContractsConfig = switch (
570
+ ecosystemName,
571
+ publicConfig["evm"],
572
+ publicConfig["fuel"],
573
+ publicConfig["svm"],
574
+ ) {
575
+ | (Ecosystem.Evm, Some(evm), _, _) => evm["contracts"]
576
+ | (Ecosystem.Fuel, _, Some(fuel), _) => fuel["contracts"]
577
+ | (Ecosystem.Svm, _, _, Some(svm)) => svm["programs"]
530
578
  | _ => None
531
579
  }
532
580
 
@@ -549,6 +597,7 @@ let fromPublic = (publicConfigJson: JSON.t) => {
549
597
  "abi": EvmTypes.Abi.t,
550
598
  "eventSignatures": array<string>,
551
599
  "events": option<array<_>>,
600
+ "svmAbi": option<{"programId": string, "definedTypes": JSON.t, "source": string}>,
552
601
  }> = Dict.make()
553
602
  switch publicContractsConfig {
554
603
  | Some(contractsDict) =>
@@ -561,21 +610,40 @@ let fromPublic = (publicConfigJson: JSON.t) => {
561
610
  | Some(events) => events->Array.map(eventItem => eventItem["event"])
562
611
  | None => []
563
612
  }
613
+ let widened =
614
+ contractConfig->(
615
+ Utils.magic: _ => {
616
+ "svmAbi": option<{"programId": string, "definedTypes": JSON.t, "source": string}>,
617
+ }
618
+ )
564
619
  contractDataByName->Dict.set(
565
620
  capitalizedName,
566
- {"abi": abi, "eventSignatures": eventSignatures, "events": contractConfig["events"]},
621
+ {
622
+ "abi": abi,
623
+ "eventSignatures": eventSignatures,
624
+ "events": contractConfig["events"],
625
+ "svmAbi": widened["svmAbi"],
626
+ },
567
627
  )
568
628
  })
569
629
  | None => ()
570
630
  }
571
631
 
572
- // Build event configs for a contract from JSON event items
632
+ // Build event configs for a contract from JSON event items.
633
+ //
634
+ // `~addresses` is the chain-side address list. For SVM programs it's the
635
+ // single base58 program_id — wired onto each instruction's event config so
636
+ // the source can build `(programId, discriminator)`-keyed InstructionSelections.
637
+ // EVM and Fuel ignore it (the address lives in `ChainContract.addresses` and
638
+ // is looked up at dispatch time, not stamped on the event).
573
639
  let buildContractEvents = (
574
640
  ~contractName,
575
641
  ~events: option<array<_>>,
576
642
  ~abi,
577
643
  ~chainId: int,
578
644
  ~startBlock: option<int>,
645
+ ~addresses: array<string>,
646
+ ~svmDefinedTypes: JSON.t=JSON.Null,
579
647
  ) => {
580
648
  switch events {
581
649
  | None => []
@@ -606,6 +674,65 @@ let fromPublic = (publicConfigJson: JSON.t) => {
606
674
  `Fuel event ${contractName}.${eventName} is missing "kind" in internal config`,
607
675
  )
608
676
  }
677
+ | Ecosystem.Svm =>
678
+ let programId = switch addresses {
679
+ | [pid] => pid->SvmTypes.Pubkey.fromStringUnsafe
680
+ | [] =>
681
+ JsError.throwWithMessage(
682
+ `SVM program ${contractName} on chain ${chainId->Int.toString} is missing a program_id`,
683
+ )
684
+ | _ =>
685
+ JsError.throwWithMessage(
686
+ `SVM program ${contractName} on chain ${chainId->Int.toString} has multiple addresses; a program is uniquely identified by a single program_id`,
687
+ )
688
+ }
689
+ let widenedEventItem =
690
+ eventItem->(
691
+ Utils.magic: _ => {
692
+ "svm": option<{
693
+ "discriminator": option<string>,
694
+ "discriminatorByteLen": int,
695
+ "includeTransaction": bool,
696
+ "includeLogs": bool,
697
+ "accountFilters": option<
698
+ array<{"position": int, "values": array<string>}>,
699
+ >,
700
+ "isInner": option<bool>,
701
+ "accounts": option<array<string>>,
702
+ "args": option<JSON.t>,
703
+ }>,
704
+ }
705
+ )
706
+ let svm = switch widenedEventItem["svm"] {
707
+ | Some(s) => s
708
+ | None =>
709
+ JsError.throwWithMessage(
710
+ `SVM instruction ${contractName}.${eventName} is missing the "svm" descriptor in internal config`,
711
+ )
712
+ }
713
+ let accountFilters =
714
+ (svm["accountFilters"]->Option.getOr([]))->Array.map(af => {
715
+ Internal.position: af["position"],
716
+ values: af["values"]->SvmTypes.Pubkey.fromStringsUnsafe,
717
+ })
718
+ (EventConfigBuilder.buildSvmInstructionEventConfig(
719
+ ~contractName,
720
+ ~instructionName=eventName,
721
+ ~programId,
722
+ ~discriminator=svm["discriminator"],
723
+ ~discriminatorByteLen=svm["discriminatorByteLen"],
724
+ ~includeTransaction=svm["includeTransaction"],
725
+ ~includeLogs=svm["includeLogs"],
726
+ ~accountFilters,
727
+ ~isInner=svm["isInner"],
728
+ ~isWildcard=false,
729
+ ~handler=None,
730
+ ~contractRegister=None,
731
+ ~accounts=svm["accounts"]->Option.getOr([]),
732
+ ~args=svm["args"]->Option.getOr(JSON.Null),
733
+ ~definedTypes=svmDefinedTypes,
734
+ ~startBlock?,
735
+ ) :> Internal.eventConfig)
609
736
  | _ =>
610
737
  (EventConfigBuilder.buildEvmEventConfig(
611
738
  ~contractName,
@@ -665,11 +792,11 @@ let fromPublic = (publicConfigJson: JSON.t) => {
665
792
  ->Dict.toArray
666
793
  ->Array.map(((capitalizedName, contractData)) => {
667
794
  let chainContract = chainContracts->Dict.get(capitalizedName)
668
- let addresses =
795
+ let rawAddresses =
669
796
  chainContract
670
797
  ->Option.flatMap(cc => cc["addresses"])
671
798
  ->Option.getOr([])
672
- ->Array.map(parseAddress)
799
+ let addresses = rawAddresses->Array.map(parseAddress)
673
800
  let startBlock = chainContract->Option.flatMap(cc => cc["startBlock"])
674
801
 
675
802
  // Build event configs from JSON (field selections resolved inline)
@@ -683,6 +810,10 @@ let fromPublic = (publicConfigJson: JSON.t) => {
683
810
  ~abi=contractData["abi"],
684
811
  ~chainId,
685
812
  ~startBlock,
813
+ ~addresses=rawAddresses,
814
+ ~svmDefinedTypes=contractData["svmAbi"]
815
+ ->Option.map(a => a["definedTypes"])
816
+ ->Option.getOr(JSON.Null),
686
817
  )
687
818
 
688
819
  {
@@ -748,7 +879,11 @@ let fromPublic = (publicConfigJson: JSON.t) => {
748
879
  }
749
880
  | Ecosystem.Svm =>
750
881
  switch publicChainConfig["rpc"] {
751
- | Some(rpc) => SvmSourceConfig({rpc: rpc})
882
+ | Some(rpc) =>
883
+ SvmSourceConfig({
884
+ hypersync: publicChainConfig["hypersync"],
885
+ rpc,
886
+ })
752
887
  | None => JsError.throwWithMessage(`Chain ${chainName} is missing rpc endpoint in config`)
753
888
  }
754
889
  }
@@ -957,18 +1092,11 @@ let rec canonicalJson = (json: JSON.t): JSON.t =>
957
1092
  }
958
1093
 
959
1094
  // Returns dotted leaf paths (`a.b[i].c`) where `stored` differs from
960
- // `current`, restricted to the highest-priority top-level tier with any
961
- // diff. Tiers in order: version → name → storage → ecosystem
962
- // (evm/fuel/svm) → entities → other top-level keys. The first tier
963
- // containing a diff is the only one rendered; lower tiers are silenced
964
- // so a single noisy section doesn't bury the actionable change.
1095
+ // `current`. Skips subtrees whose canonical JSON matches.
965
1096
  let diffPaths = (~stored: JSON.t, ~current: JSON.t): array<string> => {
966
- let canonEq = (a: JSON.t, b: JSON.t) =>
967
- JSON.stringify(canonicalJson(a)) === JSON.stringify(canonicalJson(b))
968
-
969
1097
  let acc = []
970
1098
  let rec go = (s: JSON.t, c: JSON.t, prefix: string) => {
971
- if canonEq(s, c) {
1099
+ if JSON.stringify(canonicalJson(s)) === JSON.stringify(canonicalJson(c)) {
972
1100
  ()
973
1101
  } else {
974
1102
  switch (s, c) {
@@ -994,91 +1122,25 @@ let diffPaths = (~stored: JSON.t, ~current: JSON.t): array<string> => {
994
1122
  | (Some(sv), Some(cv)) => go(sv, cv, p)
995
1123
  }
996
1124
  }
997
- | _ => acc->Array.push(prefix === "" ? "<root>" : prefix)->ignore
1125
+ | _ =>
1126
+ // Type mismatch or scalar diff
1127
+ acc->Array.push(prefix === "" ? "<root>" : prefix)->ignore
998
1128
  }
999
1129
  }
1000
1130
  }
1001
-
1002
- let getTopKey = (j: JSON.t, k: string) =>
1003
- switch j {
1004
- | Object(d) => d->Dict.get(k)
1005
- | _ => None
1006
- }
1007
- let topKeyDiffers = (k: string) =>
1008
- switch (getTopKey(stored, k), getTopKey(current, k)) {
1009
- | (None, None) => false
1010
- | (None, _) | (_, None) => true
1011
- | (Some(s), Some(c)) => !canonEq(s, c)
1012
- }
1013
- let runTier = (keys: array<string>) =>
1014
- keys->Array.forEach(k =>
1015
- switch (getTopKey(stored, k), getTopKey(current, k)) {
1016
- | (None, None) => ()
1017
- | (None, _) | (_, None) => acc->Array.push(k)->ignore
1018
- | (Some(s), Some(c)) => go(s, c, k)
1019
- }
1020
- )
1021
-
1022
- switch (stored, current) {
1023
- | (Object(sObj), Object(cObj)) =>
1024
- let tiers = [["version"], ["name"], ["storage"], ["evm", "fuel", "svm"], ["entities"]]
1025
- let firstHit = tiers->Array.reduce(None, (acc, tier) =>
1026
- switch acc {
1027
- | Some(_) => acc
1028
- | None =>
1029
- switch tier->Array.filter(topKeyDiffers) {
1030
- | [] => None
1031
- | hits => Some(hits)
1032
- }
1033
- }
1034
- )
1035
- switch firstHit {
1036
- | Some(hits) => runTier(hits)
1037
- | None =>
1038
- let knownSet = Utils.Set.fromArray(tiers->Belt.Array.concatMany)
1039
- let extras =
1040
- Utils.Set.fromArray(Array.concat(sObj->Dict.keysToArray, cObj->Dict.keysToArray))
1041
- ->Utils.Set.toArray
1042
- ->Array.filter(k => !(knownSet->Utils.Set.has(k)))
1043
- ->Array.toSorted(String.compare)
1044
- ->Array.filter(topKeyDiffers)
1045
- runTier(extras)
1046
- }
1047
- | _ => go(stored, current, "")
1048
- }
1131
+ go(stored, current, "")
1049
1132
  acc
1050
1133
  }
1051
1134
 
1052
1135
  // Throws an `incompatible config` error listing each path in `changedPaths`,
1053
- // plus the remediation options. `~resetCommand` is rendered as-is for
1054
- // option 2 (the wipe-and-redo). `~runCommand` controls option 3 (parallel
1055
- // indexer recipe): when `None`, option 3 is omitted — the migrate flow
1056
- // uses this because running a second indexer doesn't apply.
1057
- // `~hasClickhouse` adds the extra env line so users running both
1058
- // Postgres and Clickhouse get a complete override.
1059
- let throwIfIncompatible = (
1060
- changedPaths: array<string>,
1061
- ~resetCommand: string,
1062
- ~runCommand: option<string>,
1063
- ~hasClickhouse: bool,
1064
- ) => {
1136
+ // plus the revert/reset remediation. `~resetCommand` lets each call site
1137
+ // (`envio dev -r` / `envio start -r` / `envio local db-migrate setup`)
1138
+ // surface the right wipe-and-restart command.
1139
+ let throwIfIncompatible = (changedPaths: array<string>, ~resetCommand: string) => {
1065
1140
  if changedPaths->Array.length > 0 {
1066
1141
  let bullets = changedPaths->Array.map(p => ` - ${p}`)->Array.joinUnsafe("\n")
1067
- let option1 = "Revert the changes above"
1068
- let padTo = (s, col) => s ++ " "->String.repeat(Math.Int.max(col - String.length(s), 1))
1069
- let col = Math.Int.max(String.length(option1), String.length(resetCommand)) + 2
1070
- let option3 = switch runCommand {
1071
- | None => ""
1072
- | Some(cmd) =>
1073
- let clickhouseLine = hasClickhouse ? " ENVIO_CLICKHOUSE_DATABASE=<new_db> \\\n" : ""
1074
- `\n 3. Run a second indexer alongside this one — keep both datasets:\n ENVIO_PG_SCHEMA=<new_schema> \\\n${clickhouseLine} ENVIO_INDEXER_PORT=<new_port> \\\n ${cmd}`
1075
- }
1076
1142
  JsError.throwWithMessage(
1077
- `The following config changes are incompatible with the existing indexer data:\n\n${bullets}\n\nPick one:\n 1. ${option1->padTo(
1078
- col,
1079
- )}# resume indexing where it left off\n 2. ${resetCommand->padTo(
1080
- col,
1081
- )}# delete all indexed data and start over${option3}`,
1143
+ `The following config changes are incompatible with the existing indexer data:\n\n${bullets}\n\nPick one:\n\n 1. Revert the changes above # resume indexing where it left off\n 2. ${resetCommand} # wipe the database and re-index from scratch`,
1082
1144
  )
1083
1145
  }
1084
1146
  }
@@ -113,6 +113,26 @@ let publicConfigChainSchema = S$RescriptSchema.schema(s => ({
113
113
  contracts: s.m(S$RescriptSchema.option(S$RescriptSchema.dict(chainContractSchema)))
114
114
  }));
115
115
 
116
+ let svmEventDescriptorSchema = S$RescriptSchema.schema(s => ({
117
+ discriminator: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
118
+ discriminatorByteLen: s.m(S$RescriptSchema.int),
119
+ includeTransaction: s.m(S$RescriptSchema.bool),
120
+ includeLogs: s.m(S$RescriptSchema.bool),
121
+ accountFilters: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.schema(s => ({
122
+ position: s.m(S$RescriptSchema.int),
123
+ values: s.m(S$RescriptSchema.array(S$RescriptSchema.string))
124
+ }))))),
125
+ isInner: s.m(S$RescriptSchema.option(S$RescriptSchema.bool)),
126
+ accounts: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.string))),
127
+ args: s.m(S$RescriptSchema.option(S$RescriptSchema.json(false)))
128
+ }));
129
+
130
+ let svmAbiSchema = S$RescriptSchema.schema(s => ({
131
+ programId: s.m(S$RescriptSchema.string),
132
+ definedTypes: s.m(S$RescriptSchema.json(false)),
133
+ source: s.m(S$RescriptSchema.string)
134
+ }));
135
+
116
136
  let contractEventItemSchema = S$RescriptSchema.schema(s => ({
117
137
  event: s.m(S$RescriptSchema.string),
118
138
  name: s.m(S$RescriptSchema.string),
@@ -120,18 +140,21 @@ let contractEventItemSchema = S$RescriptSchema.schema(s => ({
120
140
  params: s.m(S$RescriptSchema.option(S$RescriptSchema.array(EventConfigBuilder.eventParamSchema))),
121
141
  kind: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
122
142
  blockFields: s.m(S$RescriptSchema.option(S$RescriptSchema.array(Internal.evmBlockFieldSchema))),
123
- transactionFields: s.m(S$RescriptSchema.option(S$RescriptSchema.array(Internal.evmTransactionFieldSchema)))
143
+ transactionFields: s.m(S$RescriptSchema.option(S$RescriptSchema.array(Internal.evmTransactionFieldSchema))),
144
+ svm: s.m(S$RescriptSchema.option(svmEventDescriptorSchema))
124
145
  }));
125
146
 
126
147
  let contractConfigSchema = S$RescriptSchema.schema(s => ({
127
148
  abi: s.m(S$RescriptSchema.json(false)),
128
149
  handler: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
129
- events: s.m(S$RescriptSchema.option(S$RescriptSchema.array(contractEventItemSchema)))
150
+ events: s.m(S$RescriptSchema.option(S$RescriptSchema.array(contractEventItemSchema))),
151
+ svmAbi: s.m(S$RescriptSchema.option(svmAbiSchema))
130
152
  }));
131
153
 
132
154
  let publicConfigEcosystemSchema = S$RescriptSchema.schema(s => ({
133
155
  chains: s.m(S$RescriptSchema.dict(publicConfigChainSchema)),
134
- contracts: s.m(S$RescriptSchema.option(S$RescriptSchema.dict(contractConfigSchema)))
156
+ contracts: s.m(S$RescriptSchema.option(S$RescriptSchema.dict(contractConfigSchema))),
157
+ programs: s.m(S$RescriptSchema.option(S$RescriptSchema.dict(contractConfigSchema)))
135
158
  }));
136
159
 
137
160
  let publicConfigEvmSchema = S$RescriptSchema.schema(s => ({
@@ -419,6 +442,7 @@ function fromPublic(publicConfigJson) {
419
442
  let lowercaseAddresses = evm !== undefined ? Stdlib_Option.getOr(Primitive_option.valFromOption(evm).addressFormat, "checksum") === "lowercase" : false;
420
443
  let match$4 = publicConfig.evm;
421
444
  let match$5 = publicConfig.fuel;
445
+ let match$6 = publicConfig.svm;
422
446
  let publicContractsConfig;
423
447
  switch (ecosystemName) {
424
448
  case "evm" :
@@ -428,25 +452,25 @@ function fromPublic(publicConfigJson) {
428
452
  publicContractsConfig = match$5 !== undefined ? Primitive_option.valFromOption(match$5).contracts : undefined;
429
453
  break;
430
454
  case "svm" :
431
- publicContractsConfig = undefined;
455
+ publicContractsConfig = match$6 !== undefined ? Primitive_option.valFromOption(match$6).programs : undefined;
432
456
  break;
433
457
  }
434
458
  let evm$1 = publicConfig.evm;
435
- let match$6;
459
+ let match$7;
436
460
  if (evm$1 !== undefined) {
437
461
  let evm$2 = Primitive_option.valFromOption(evm$1);
438
- match$6 = [
462
+ match$7 = [
439
463
  new Set(EventConfigBuilder.alwaysIncludedBlockFields.concat(Stdlib_Option.getOr(evm$2.globalBlockFields, []))),
440
464
  new Set(Stdlib_Option.getOr(evm$2.globalTransactionFields, []))
441
465
  ];
442
466
  } else {
443
- match$6 = [
467
+ match$7 = [
444
468
  new Set(EventConfigBuilder.alwaysIncludedBlockFields),
445
469
  new Set()
446
470
  ];
447
471
  }
448
- let globalTransactionFieldsSet = match$6[1];
449
- let globalBlockFieldsSet = match$6[0];
472
+ let globalTransactionFieldsSet = match$7[1];
473
+ let globalBlockFieldsSet = match$7[0];
450
474
  let contractDataByName = {};
451
475
  if (publicContractsConfig !== undefined) {
452
476
  Object.entries(publicContractsConfig).forEach(param => {
@@ -458,11 +482,13 @@ function fromPublic(publicConfigJson) {
458
482
  contractDataByName[capitalizedName] = {
459
483
  abi: abi,
460
484
  eventSignatures: eventSignatures,
461
- events: contractConfig.events
485
+ events: contractConfig.events,
486
+ svmAbi: contractConfig.svmAbi
462
487
  };
463
488
  });
464
489
  }
465
- let buildContractEvents = (contractName, events, abi, chainId, startBlock) => {
490
+ let buildContractEvents = (contractName, events, abi, chainId, startBlock, addresses, svmDefinedTypesOpt) => {
491
+ let svmDefinedTypes = svmDefinedTypesOpt !== undefined ? svmDefinedTypesOpt : null;
466
492
  if (events !== undefined) {
467
493
  return events.map(eventItem => {
468
494
  let eventName = eventItem.name;
@@ -470,17 +496,27 @@ function fromPublic(publicConfigJson) {
470
496
  let params = Stdlib_Option.getOr(eventItem.params, []);
471
497
  let kind = eventItem.kind;
472
498
  switch (ecosystemName) {
499
+ case "evm" :
500
+ return EventConfigBuilder.buildEvmEventConfig(contractName, eventName, sighash, params, false, undefined, undefined, undefined, chainId, ecosystem.onEventBlockFilterSchema, eventItem.blockFields, eventItem.transactionFields, startBlock, Primitive_option.some(globalBlockFieldsSet), Primitive_option.some(globalTransactionFieldsSet));
473
501
  case "fuel" :
474
502
  if (kind !== undefined) {
475
503
  return EventConfigBuilder.buildFuelEventConfig(contractName, eventName, kind, sighash, abi, false, undefined, undefined, startBlock);
476
504
  } else {
477
505
  return Stdlib_JsError.throwWithMessage(`Fuel event ` + contractName + `.` + eventName + ` is missing "kind" in internal config`);
478
506
  }
479
- case "evm" :
480
507
  case "svm" :
481
- break;
508
+ let len = addresses.length;
509
+ let programId = len !== 1 ? (
510
+ len !== 0 ? Stdlib_JsError.throwWithMessage(`SVM program ` + contractName + ` on chain ` + chainId.toString() + ` has multiple addresses; a program is uniquely identified by a single program_id`) : Stdlib_JsError.throwWithMessage(`SVM program ` + contractName + ` on chain ` + chainId.toString() + ` is missing a program_id`)
511
+ ) : addresses[0];
512
+ let s = eventItem.svm;
513
+ let svm = s !== undefined ? Primitive_option.valFromOption(s) : Stdlib_JsError.throwWithMessage(`SVM instruction ` + contractName + `.` + eventName + ` is missing the "svm" descriptor in internal config`);
514
+ let accountFilters = Stdlib_Option.getOr(svm.accountFilters, []).map(af => ({
515
+ position: af.position,
516
+ values: af.values
517
+ }));
518
+ return EventConfigBuilder.buildSvmInstructionEventConfig(contractName, eventName, programId, svm.discriminator, svm.discriminatorByteLen, svm.includeTransaction, svm.includeLogs, accountFilters, svm.isInner, false, undefined, undefined, Stdlib_Option.getOr(svm.accounts, []), Stdlib_Option.getOr(svm.args, null), svmDefinedTypes, startBlock);
482
519
  }
483
- return EventConfigBuilder.buildEvmEventConfig(contractName, eventName, sighash, params, false, undefined, undefined, undefined, chainId, ecosystem.onEventBlockFilterSchema, eventItem.blockFields, eventItem.transactionFields, startBlock, Primitive_option.some(globalBlockFieldsSet), Primitive_option.some(globalTransactionFieldsSet));
484
520
  });
485
521
  } else {
486
522
  return [];
@@ -517,9 +553,10 @@ function fromPublic(publicConfigJson) {
517
553
  let contractData = param[1];
518
554
  let capitalizedName = param[0];
519
555
  let chainContract = chainContracts[capitalizedName];
520
- let addresses = Stdlib_Option.getOr(Stdlib_Option.flatMap(chainContract, cc => cc.addresses), []).map(parseAddress);
556
+ let rawAddresses = Stdlib_Option.getOr(Stdlib_Option.flatMap(chainContract, cc => cc.addresses), []);
557
+ let addresses = rawAddresses.map(parseAddress);
521
558
  let startBlock = Stdlib_Option.flatMap(chainContract, cc => cc.startBlock);
522
- let events = buildContractEvents(capitalizedName, contractData.events, contractData.abi, chainId, startBlock);
559
+ let events = buildContractEvents(capitalizedName, contractData.events, contractData.abi, chainId, startBlock, rawAddresses, Stdlib_Option.getOr(Stdlib_Option.map(contractData.svmAbi, a => a.definedTypes), null));
523
560
  return {
524
561
  name: capitalizedName,
525
562
  abi: contractData.abi,
@@ -576,6 +613,7 @@ function fromPublic(publicConfigJson) {
576
613
  let rpc = publicChainConfig.rpc;
577
614
  sourceConfig = rpc !== undefined ? ({
578
615
  TAG: "SvmSourceConfig",
616
+ hypersync: publicChainConfig.hypersync,
579
617
  rpc: rpc
580
618
  }) : Stdlib_JsError.throwWithMessage(`Chain ` + chainName + ` is missing rpc endpoint in config`);
581
619
  break;
@@ -780,10 +818,9 @@ function canonicalJson(json) {
780
818
  }
781
819
 
782
820
  function diffPaths(stored, current) {
783
- let canonEq = (a, b) => JSON.stringify(canonicalJson(a)) === JSON.stringify(canonicalJson(b));
784
821
  let acc = [];
785
822
  let go = (s, c, prefix) => {
786
- if (canonEq(s, c)) {
823
+ if (JSON.stringify(canonicalJson(s)) === JSON.stringify(canonicalJson(c))) {
787
824
  return;
788
825
  }
789
826
  if (Array.isArray(s)) {
@@ -831,92 +868,16 @@ function diffPaths(stored, current) {
831
868
  }
832
869
  acc.push(prefix === "" ? "<root>" : prefix);
833
870
  };
834
- let getTopKey = (j, k) => {
835
- if (typeof j === "object" && j !== null && !Array.isArray(j)) {
836
- return j[k];
837
- }
838
- };
839
- let topKeyDiffers = k => {
840
- let match = getTopKey(stored, k);
841
- let match$1 = getTopKey(current, k);
842
- if (match !== undefined) {
843
- if (match$1 !== undefined) {
844
- return !canonEq(match, match$1);
845
- } else {
846
- return true;
847
- }
848
- } else {
849
- return match$1 !== undefined;
850
- }
851
- };
852
- let runTier = keys => {
853
- keys.forEach(k => {
854
- let match = getTopKey(stored, k);
855
- let match$1 = getTopKey(current, k);
856
- if (match !== undefined) {
857
- if (match$1 !== undefined) {
858
- return go(match, match$1, k);
859
- } else {
860
- acc.push(k);
861
- return;
862
- }
863
- } else if (match$1 !== undefined) {
864
- acc.push(k);
865
- return;
866
- } else {
867
- return;
868
- }
869
- });
870
- };
871
- if (typeof stored === "object" && stored !== null && !Array.isArray(stored) && typeof current === "object" && current !== null && !Array.isArray(current)) {
872
- let tiers = [
873
- ["version"],
874
- ["name"],
875
- ["storage"],
876
- [
877
- "evm",
878
- "fuel",
879
- "svm"
880
- ],
881
- ["entities"]
882
- ];
883
- let firstHit = Stdlib_Array.reduce(tiers, undefined, (acc, tier) => {
884
- if (acc !== undefined) {
885
- return acc;
886
- }
887
- let hits = tier.filter(topKeyDiffers);
888
- if (hits.length !== 0) {
889
- return hits;
890
- }
891
- });
892
- if (firstHit !== undefined) {
893
- runTier(firstHit);
894
- } else {
895
- let knownSet = new Set(Belt_Array.concatMany(tiers));
896
- runTier(Array.from(new Set(Object.keys(stored).concat(Object.keys(current)))).filter(k => !knownSet.has(k)).toSorted(Primitive_string.compare).filter(topKeyDiffers));
897
- }
898
- } else {
899
- go(stored, current, "");
900
- }
871
+ go(stored, current, "");
901
872
  return acc;
902
873
  }
903
874
 
904
- function throwIfIncompatible(changedPaths, resetCommand, runCommand, hasClickhouse) {
875
+ function throwIfIncompatible(changedPaths, resetCommand) {
905
876
  if (changedPaths.length === 0) {
906
877
  return;
907
878
  }
908
879
  let bullets = changedPaths.map(p => ` - ` + p).join("\n");
909
- let option1 = "Revert the changes above";
910
- let padTo = (s, col) => s + " ".repeat(Math.max(col - s.length | 0, 1));
911
- let col = Math.max(option1.length, resetCommand.length) + 2 | 0;
912
- let option3;
913
- if (runCommand !== undefined) {
914
- let clickhouseLine = hasClickhouse ? " ENVIO_CLICKHOUSE_DATABASE=<new_db> \\\n" : "";
915
- option3 = `\n 3. Run a second indexer alongside this one — keep both datasets:\n ENVIO_PG_SCHEMA=<new_schema> \\\n` + clickhouseLine + ` ENVIO_INDEXER_PORT=<new_port> \\\n ` + runCommand;
916
- } else {
917
- option3 = "";
918
- }
919
- Stdlib_JsError.throwWithMessage(`The following config changes are incompatible with the existing indexer data:\n\n` + bullets + `\n\nPick one:\n 1. ` + padTo(option1, col) + `# resume indexing where it left off\n 2. ` + padTo(resetCommand, col) + `# delete all indexed data and start over` + option3);
880
+ Stdlib_JsError.throwWithMessage(`The following config changes are incompatible with the existing indexer data:\n\n` + bullets + `\n\nPick one:\n\n 1. Revert the changes above # resume indexing where it left off\n 2. ` + resetCommand + ` # wipe the database and re-index from scratch`);
920
881
  }
921
882
 
922
883
  function loadWithoutRegistrations() {
@@ -939,6 +900,8 @@ export {
939
900
  rpcConfigSchema,
940
901
  chainContractSchema,
941
902
  publicConfigChainSchema,
903
+ svmEventDescriptorSchema,
904
+ svmAbiSchema,
942
905
  contractEventItemSchema,
943
906
  contractConfigSchema,
944
907
  publicConfigEcosystemSchema,