envio 3.10.0 → 3.11.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 (42) hide show
  1. package/evm.schema.json +21 -4
  2. package/fuel.schema.json +21 -4
  3. package/index.d.ts +7 -6
  4. package/package.json +6 -6
  5. package/src/ChainState.res +9 -73
  6. package/src/ChainState.res.mjs +6 -58
  7. package/src/Config.res +73 -18
  8. package/src/Config.res.mjs +69 -16
  9. package/src/EventConfigBuilder.res +14 -10
  10. package/src/EventConfigBuilder.res.mjs +6 -4
  11. package/src/HandlerRegister.res +12 -11
  12. package/src/HandlerRegister.res.mjs +6 -5
  13. package/src/Internal.res +20 -4
  14. package/src/Internal.res.mjs +9 -0
  15. package/src/Main.res +8 -1
  16. package/src/Main.res.mjs +3 -3
  17. package/src/Persistence.res +11 -1
  18. package/src/Persistence.res.mjs +6 -2
  19. package/src/PgStorage.res +1 -1
  20. package/src/PgStorage.res.mjs +1 -1
  21. package/src/SimulateItems.res +13 -6
  22. package/src/SimulateItems.res.mjs +7 -6
  23. package/src/TestIndexer.res +6 -5
  24. package/src/TestIndexer.res.mjs +5 -4
  25. package/src/db/InternalTable.res +1 -1
  26. package/src/db/InternalTable.res.mjs +2 -1
  27. package/src/sources/ChainSources.res +72 -0
  28. package/src/sources/ChainSources.res.mjs +55 -0
  29. package/src/sources/EvmHyperSyncSource.res +10 -22
  30. package/src/sources/EvmHyperSyncSource.res.mjs +9 -41
  31. package/src/sources/FuelHyperSyncSource.res +10 -18
  32. package/src/sources/FuelHyperSyncSource.res.mjs +8 -38
  33. package/src/sources/HyperSync.res +31 -0
  34. package/src/sources/HyperSync.res.mjs +31 -0
  35. package/src/sources/HyperSync.resi +16 -0
  36. package/src/sources/StartBlockResolver.res +173 -0
  37. package/src/sources/StartBlockResolver.res.mjs +134 -0
  38. package/src/sources/Svm.res +0 -51
  39. package/src/sources/Svm.res.mjs +0 -46
  40. package/src/sources/SvmHyperSyncSource.res +20 -9
  41. package/src/sources/SvmHyperSyncSource.res.mjs +34 -12
  42. package/svm.schema.json +80 -66
@@ -60,26 +60,72 @@ let chainContractSchema = S$RescriptSchema.schema(s => ({
60
60
  startBlock: s.m(S$RescriptSchema.option(S$RescriptSchema.int))
61
61
  }));
62
62
 
63
+ function startBlockOrThrow(chain) {
64
+ let startBlock = chain.startBlock;
65
+ if (startBlock === "latest") {
66
+ return Stdlib_JsError.throwWithMessage(`Chain ` + ChainId.toString(chain.id) + `: the "latest" start block was read before it was resolved. This is a bug in envio - please report it.`);
67
+ } else {
68
+ return startBlock;
69
+ }
70
+ }
71
+
72
+ function startBlockOrZero(chain) {
73
+ let startBlock = chain.startBlock;
74
+ if (startBlock === "latest") {
75
+ return 0;
76
+ } else {
77
+ return startBlock;
78
+ }
79
+ }
80
+
81
+ let startBlockSchema = S$RescriptSchema.union([
82
+ S$RescriptSchema.shape(S$RescriptSchema.int, n => (n)),
83
+ S$RescriptSchema.shape(S$RescriptSchema.literal("latest"), param => "latest")
84
+ ]);
85
+
63
86
  let publicConfigChainSchema = S$RescriptSchema.schema(s => ({
64
87
  id: s.m(ChainId.schema),
65
- startBlock: s.m(S$RescriptSchema.int),
88
+ startBlock: s.m(startBlockSchema),
66
89
  endBlock: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
67
90
  maxReorgDepth: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
68
91
  blockLag: s.m(S$RescriptSchema.option(S$RescriptSchema.int)),
69
92
  hypersync: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
70
93
  rpcs: s.m(S$RescriptSchema.option(S$RescriptSchema.array(rpcConfigSchema))),
71
- rpc: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
72
94
  contracts: s.m(S$RescriptSchema.option(S$RescriptSchema.dict(chainContractSchema)))
73
95
  }));
74
96
 
97
+ let svmAccountSlotSchema = S$RescriptSchema.schema(s => ({
98
+ name: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
99
+ optional: s.m(S$RescriptSchema.option(S$RescriptSchema.bool))
100
+ }));
101
+
102
+ function svmAccountSlotFromItem(slot) {
103
+ let match = slot.name;
104
+ let match$1 = slot.optional;
105
+ if (match !== undefined) {
106
+ if (match$1 !== undefined && match$1) {
107
+ return {
108
+ TAG: "Optional",
109
+ _0: match
110
+ };
111
+ } else {
112
+ return {
113
+ TAG: "Required",
114
+ _0: match
115
+ };
116
+ }
117
+ } else {
118
+ return "Unnamed";
119
+ }
120
+ }
121
+
75
122
  let svmEventDescriptorSchema = S$RescriptSchema.schema(s => ({
76
123
  discriminator: s.m(S$RescriptSchema.option(S$RescriptSchema.string)),
77
- accounts: s.m(S$RescriptSchema.option(S$RescriptSchema.array(S$RescriptSchema.string))),
124
+ accounts: s.m(S$RescriptSchema.option(S$RescriptSchema.array(svmAccountSlotSchema))),
78
125
  args: s.m(S$RescriptSchema.option(S$RescriptSchema.json(false)))
79
126
  }));
80
127
 
81
128
  let svmAbiSchema = S$RescriptSchema.schema(s => ({
82
- programId: s.m(S$RescriptSchema.string),
83
129
  definedTypes: s.m(S$RescriptSchema.json(false)),
84
130
  source: s.m(S$RescriptSchema.string)
85
131
  }));
@@ -538,7 +584,7 @@ function fromPublic(publicConfigJson) {
538
584
  ) : addresses[0];
539
585
  let s = eventItem.svm;
540
586
  let svm = s !== undefined ? Primitive_option.valFromOption(s) : Stdlib_JsError.throwWithMessage(`SVM instruction ` + contractName + `.` + eventName + ` is missing the "svm" descriptor in internal config`);
541
- return EventConfigBuilder.buildSvmInstructionEventConfig(contractName, eventName, programId, svm.discriminator, Stdlib_Option.getOr(svm.accounts, []), Stdlib_Option.getOr(svm.args, null), svmDefinedTypes);
587
+ return EventConfigBuilder.buildSvmInstructionEventConfig(contractName, eventName, programId, svm.discriminator, Stdlib_Option.getOr(svm.accounts, []).map(svmAccountSlotFromItem), Stdlib_Option.getOr(svm.args, null), svmDefinedTypes);
542
588
  }
543
589
  });
544
590
  } else {
@@ -572,7 +618,15 @@ function fromPublic(publicConfigJson) {
572
618
  let publicChainConfig = publicChainsConfig[chainName];
573
619
  let chainId = publicChainConfig.id;
574
620
  let chainContracts = Stdlib_Option.getOr(publicChainConfig.contracts, {});
575
- let contracts = Object.entries(contractDataByName).map(param => {
621
+ let contracts = Object.entries(contractDataByName).filter(param => {
622
+ switch (ecosystemName) {
623
+ case "evm" :
624
+ case "fuel" :
625
+ return true;
626
+ case "svm" :
627
+ return Stdlib_Option.isSome(chainContracts[param[0]]);
628
+ }
629
+ }).map(param => {
576
630
  let contractData = param[1];
577
631
  let capitalizedName = param[0];
578
632
  let chainContract = chainContracts[capitalizedName];
@@ -644,15 +698,10 @@ function fromPublic(publicConfigJson) {
644
698
  break;
645
699
  case "svm" :
646
700
  let hypersync$1 = publicChainConfig.hypersync;
647
- let rpc = publicChainConfig.rpc;
648
- if (Stdlib_Option.isNone(hypersync$1) && Stdlib_Option.isNone(rpc)) {
649
- Stdlib_JsError.throwWithMessage(`Chain ` + chainName + ` is missing a data source: provide either an rpc endpoint or an experimental hypersync config`);
650
- }
651
- sourceConfig = {
652
- TAG: "SvmSourceConfig",
653
- hypersync: hypersync$1,
654
- rpc: rpc
655
- };
701
+ sourceConfig = hypersync$1 !== undefined ? ({
702
+ TAG: "SvmSourceConfig",
703
+ hypersync: hypersync$1
704
+ }) : Stdlib_JsError.throwWithMessage(`Chain ` + chainName + ` is missing hypersync endpoint in config`);
656
705
  break;
657
706
  }
658
707
  let tmp;
@@ -877,7 +926,6 @@ function stripSensitiveData(json) {
877
926
  return;
878
927
  }
879
928
  Utils.Dict.deleteInPlace(chainJson, "rpcs");
880
- Utils.Dict.deleteInPlace(chainJson, "rpc");
881
929
  Utils.Dict.deleteInPlace(chainJson, "hypersync");
882
930
  });
883
931
  };
@@ -1083,7 +1131,12 @@ export {
1083
1131
  rpcSourceForSchema,
1084
1132
  rpcConfigSchema,
1085
1133
  chainContractSchema,
1134
+ startBlockOrThrow,
1135
+ startBlockOrZero,
1136
+ startBlockSchema,
1086
1137
  publicConfigChainSchema,
1138
+ svmAccountSlotSchema,
1139
+ svmAccountSlotFromItem,
1087
1140
  svmEventDescriptorSchema,
1088
1141
  svmAbiSchema,
1089
1142
  contractEventItemSchema,
@@ -635,7 +635,7 @@ let buildSvmInstructionEventConfig = (
635
635
  ~instructionName: string,
636
636
  ~programId: SvmTypes.Pubkey.t,
637
637
  ~discriminator: option<string>,
638
- ~accounts: array<string>=[],
638
+ ~accounts: array<Internal.svmAccountSlot>=[],
639
639
  ~args: JSON.t=JSON.Null,
640
640
  ~definedTypes: JSON.t=JSON.Null,
641
641
  ): Internal.svmInstructionEventConfig => {
@@ -699,7 +699,7 @@ let resolveSvmWhereOrThrow = (
699
699
  where: JSON.t,
700
700
  ~contractName: string,
701
701
  ~eventName: string,
702
- ~accountNames: array<string>,
702
+ ~accounts: array<Internal.svmAccountSlot>,
703
703
  ): parsedSvmWhere => {
704
704
  let invalid = message =>
705
705
  JsError.throwWithMessage(
@@ -722,26 +722,30 @@ let resolveSvmWhereOrThrow = (
722
722
  | Some(_) => invalid(`The "isInner" filter must be a boolean.`)
723
723
  }
724
724
 
725
+ let namedAccounts = accounts->Array.filterMap(Internal.svmAccountSlotName)
726
+
725
727
  let parseGroup = (group: dict<JSON.t>): Internal.svmAccountFilterGroup =>
726
728
  group
727
729
  ->Dict.toArray
728
730
  ->Array.map(((name, value)) => {
729
- let position = switch accountNames->Array.indexOf(name) {
730
- | -1 if accountNames->Utils.Array.isEmpty =>
731
+ let position = switch accounts->Array.findIndexOpt(slot =>
732
+ slot->Internal.svmAccountSlotName == Some(name)
733
+ ) {
734
+ | None if namedAccounts->Utils.Array.isEmpty =>
731
735
  invalid(
732
- "The instruction has no named accounts to filter on. Add `accounts` and `args` to it in config.yaml, or attach an IDL.",
736
+ "The instruction has no named accounts to filter on. Add `accounts` to it in config.yaml, or attach an IDL.",
733
737
  )
734
- | -1 =>
738
+ | None =>
735
739
  invalid(
736
740
  `The instruction has no account named "${name}" to filter on. Named accounts: ${Utils.Array.quotedJoin(
737
- accountNames,
741
+ namedAccounts,
738
742
  )}.`,
739
743
  )
740
- | position if position >= filterableAccountCount =>
744
+ | Some(position) if position >= filterableAccountCount =>
741
745
  invalid(
742
746
  `Account "${name}" is at position ${position->Int.toString}, and only the first ${filterableAccountCount->Int.toString} accounts of an instruction can be filtered.`,
743
747
  )
744
- | position => position
748
+ | Some(position) => position
745
749
  }
746
750
  let values = value->normalizeOrThrow
747
751
  if values->Utils.Array.isEmpty {
@@ -817,7 +821,7 @@ let buildSvmOnEventRegistration = (
817
821
  where->resolveSvmWhereOrThrow(
818
822
  ~contractName=eventConfig.contractName,
819
823
  ~eventName=eventConfig.name,
820
- ~accountNames=eventConfig.accounts,
824
+ ~accounts=eventConfig.accounts,
821
825
  )
822
826
  }
823
827
 
@@ -12,6 +12,7 @@ import * as LogSelection from "./LogSelection.res.mjs";
12
12
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
13
13
  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
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
15
16
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
16
17
  import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
17
18
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
@@ -464,7 +465,7 @@ let svmBlockFilterSchema = S$RescriptSchema.strict(S$RescriptSchema.object(s =>
464
465
  slot: s.f("slot", S$RescriptSchema.option(LogSelection.eventBlockRangeSchema))
465
466
  })));
466
467
 
467
- function resolveSvmWhereOrThrow(where, contractName, eventName, accountNames) {
468
+ function resolveSvmWhereOrThrow(where, contractName, eventName, accounts) {
468
469
  let invalid = message => Stdlib_JsError.throwWithMessage(`Invalid where configuration for the "` + eventName + `" instruction on program "` + contractName + `". ` + message);
469
470
  let obj;
470
471
  obj = typeof where === "object" && where !== null && !Array.isArray(where) ? where : invalid("Expected an object.");
@@ -477,13 +478,14 @@ function resolveSvmWhereOrThrow(where, contractName, eventName, accountNames) {
477
478
  let isInner = match !== undefined ? (
478
479
  typeof match === "boolean" ? match : invalid(`The "isInner" filter must be a boolean.`)
479
480
  ) : undefined;
481
+ let namedAccounts = Stdlib_Array.filterMap(accounts, Internal.svmAccountSlotName);
480
482
  let parseGroup = group => Object.entries(group).map(param => {
481
483
  let name = param[0];
482
- let position = accountNames.indexOf(name);
483
- let position$1 = position !== -1 ? (
484
+ let position = Stdlib_Array.findIndexOpt(accounts, slot => Primitive_object.equal(Internal.svmAccountSlotName(slot), name));
485
+ let position$1 = position !== undefined ? (
484
486
  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
487
  ) : (
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) + `.`)
488
+ Utils.$$Array.isEmpty(namedAccounts) ? invalid("The instruction has no named accounts to filter on. Add `accounts` 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(namedAccounts) + `.`)
487
489
  );
488
490
  let values = normalizeOrThrow(param[1]);
489
491
  if (Utils.$$Array.isEmpty(values)) {
@@ -312,13 +312,7 @@ let addOnEventRegistration = (
312
312
  ),
313
313
  )
314
314
  | Svm =>
315
- Some(
316
- EventConfigBuilder.resolveSvmInlineFieldSelection(
317
- fields,
318
- ~contractName,
319
- ~eventName,
320
- ),
321
- )
315
+ Some(EventConfigBuilder.resolveSvmInlineFieldSelection(fields, ~contractName, ~eventName))
322
316
  | Fuel =>
323
317
  JsError.throwWithMessage(
324
318
  `The fields option of the "${eventName}" event registration on contract "${contractName}" is not supported on Fuel. Select the fields in your config instead.`,
@@ -340,13 +334,16 @@ let addOnEventRegistration = (
340
334
  | (Svm, Some(fieldSelection)) if fieldSelection.instructionFields->Utils.Set.has("args") =>
341
335
  let svmEventConfig =
342
336
  eventConfig->(Utils.magic: Internal.eventConfig => Internal.svmInstructionEventConfig)
337
+ // An empty layout is still a layout: it decodes to `{}` and filters
338
+ // out the calls that carry a payload. Only an absent one has nothing
339
+ // to decode.
343
340
  let declaresArgs = switch svmEventConfig.args {
344
- | JSON.Array(args) => args->Array.length > 0
341
+ | JSON.Array(_) => true
345
342
  | _ => false
346
343
  }
347
344
  if !declaresArgs {
348
345
  JsError.throwWithMessage(
349
- `Invalid "args" field in the fields.instruction option of the "${eventName}" instruction on program "${contractName}". The instruction declares no args in config.yaml, so there is nothing to decode. Remove "args" from the selection, or declare the instruction's args.`,
346
+ `Invalid "args" field in the fields.instruction option of the "${eventName}" instruction on program "${contractName}". The instruction attaches no args layout in config.yaml, so there is nothing to decode. Remove "args" from the selection, or give the instruction an \`args\` layout — \`args: []\` if it takes none.`,
350
347
  )
351
348
  }
352
349
  | _ => ()
@@ -800,9 +797,13 @@ let registerOnBlock = (
800
797
 
801
798
  if shouldRegister {
802
799
  matchedAny := true
803
- if range._gte->Option.getOr(chainConfig.startBlock) < chainConfig.startBlock {
800
+ // Off the same object the predicate above was handed, not off
801
+ // `chainConfig`: that one still says whatever config.yaml said, and for
802
+ // `start_block: latest` the resolved head only lives in persisted state.
803
+ let chainStartBlock = (chainObj->(Utils.magic: unknown => {"startBlock": int}))["startBlock"]
804
+ if range._gte->Option.getOr(chainStartBlock) < chainStartBlock {
804
805
  JsError.throwWithMessage(
805
- `The start block for onBlock handler "${name}" is less than the chain start block (${chainConfig.startBlock->Int.toString}). This is not supported yet.`,
806
+ `The start block for onBlock handler "${name}" is less than the chain start block (${chainStartBlock->Int.toString}). This is not supported yet.`,
806
807
  )
807
808
  }
808
809
  switch chainConfig.endBlock {
@@ -238,11 +238,11 @@ function addOnEventRegistration(registration, contractName, eventName, handler,
238
238
  break;
239
239
  case "svm" :
240
240
  if (fieldSelection !== undefined && fieldSelection.instructionFields.has("args")) {
241
- let args = eventConfig.args;
241
+ let match$1 = eventConfig.args;
242
242
  let declaresArgs;
243
- declaresArgs = Array.isArray(args) ? args.length !== 0 : false;
243
+ declaresArgs = Array.isArray(match$1);
244
244
  if (!declaresArgs) {
245
- Stdlib_JsError.throwWithMessage(`Invalid "args" field in the fields.instruction option of the "` + eventName + `" instruction on program "` + contractName + `". The instruction declares no args in config.yaml, so there is nothing to decode. Remove "args" from the selection, or declare the instruction's args.`);
245
+ Stdlib_JsError.throwWithMessage(`Invalid "args" field in the fields.instruction option of the "` + eventName + `" instruction on program "` + contractName + `". The instruction attaches no args layout in config.yaml, so there is nothing to decode. Remove "args" from the selection, or give the instruction an \`args\` layout — \`args: []\` if it takes none.`);
246
246
  }
247
247
  }
248
248
  break;
@@ -492,8 +492,9 @@ function registerOnBlock(name, where, handler, getChainsObject) {
492
492
  }
493
493
  let range = match[1];
494
494
  matchedAny.contents = true;
495
- if (Stdlib_Option.getOr(range._gte, chainConfig.startBlock) < chainConfig.startBlock) {
496
- Stdlib_JsError.throwWithMessage(`The start block for onBlock handler "` + name + `" is less than the chain start block (` + chainConfig.startBlock.toString() + `). This is not supported yet.`);
495
+ let chainStartBlock = chainObj.startBlock;
496
+ if (Stdlib_Option.getOr(range._gte, chainStartBlock) < chainStartBlock) {
497
+ Stdlib_JsError.throwWithMessage(`The start block for onBlock handler "` + name + `" is less than the chain start block (` + chainStartBlock.toString() + `). This is not supported yet.`);
497
498
  }
498
499
  let chainEndBlock = chainConfig.endBlock;
499
500
  if (chainEndBlock !== undefined && Stdlib_Option.getOr(range._lte, chainEndBlock) > chainEndBlock) {
package/src/Internal.res CHANGED
@@ -567,17 +567,33 @@ type svmAccountFilter = {
567
567
  /** AND-group: every entry must match the same instruction. */
568
568
  type svmAccountFilterGroup = array<svmAccountFilter>
569
569
 
570
+ /** One positional account slot of an instruction. An `Optional` slot is absent
571
+ from the payload when the call carries no such slot, or fills it with the id of
572
+ the program being invoked. `Unnamed` holds a position and surfaces nothing. */
573
+ type svmAccountSlot =
574
+ | Unnamed
575
+ | Required(string)
576
+ | Optional(string)
577
+
578
+ let svmAccountSlotName = slot =>
579
+ switch slot {
580
+ | Unnamed => None
581
+ | Required(name) | Optional(name) => Some(name)
582
+ }
583
+
570
584
  type svmInstructionEventConfig = {
571
585
  ...eventConfig,
572
586
  /** Base58 Solana program id this instruction belongs to. */
573
587
  programId: SvmTypes.Pubkey.t,
574
588
  /** Hex-encoded discriminator. `None` matches every instruction in the program. */
575
589
  discriminator: option<string>,
576
- /** Positional account names from the Borsh schema, in declared order.
577
- `[]` means no schema is attached for this instruction. */
578
- accounts: array<string>,
590
+ /** Positional account slots in declared order. `[]` means no schema is
591
+ attached for this instruction. */
592
+ accounts: array<svmAccountSlot>,
579
593
  /** Borsh args layout as `Vec<ArgDef>` JSON (see `human_config::svm::ArgDef`
580
- on the Rust side). `JSON.Null` means no schema is attached. */
594
+ on the Rust side). `JSON.Null` attaches no decoder, so every matched call is
595
+ delivered with its payload raw. An array attaches one, `[]` included: a call
596
+ whose data the layout rejects is skipped. */
581
597
  args: JSON.t,
582
598
  /** Program-level nominal-type registry (`BTreeMap<String, ArgType>` JSON).
583
599
  Duplicated on every event of the same program — the runtime dedups by
@@ -174,6 +174,14 @@ function dependsOnAddresses(isWildcard, filterByAddresses) {
174
174
  }
175
175
  }
176
176
 
177
+ function svmAccountSlotName(slot) {
178
+ if (typeof slot !== "object") {
179
+ return;
180
+ } else {
181
+ return slot._0;
182
+ }
183
+ }
184
+
177
185
  function getItemChainId(item) {
178
186
  if (item.kind === 0) {
179
187
  return item.chainId;
@@ -334,6 +342,7 @@ export {
334
342
  unionFields,
335
343
  unionFieldSelection,
336
344
  dependsOnAddresses,
345
+ svmAccountSlotName,
337
346
  getItemChainId,
338
347
  fuelSupplyParamsSchema,
339
348
  fuelTransferParamsSchema,
package/src/Main.res CHANGED
@@ -126,7 +126,9 @@ let buildChainsObject = (~config: Config.t) => {
126
126
  get: () => {
127
127
  switch getInitialChainState(~chainId=chainConfig.id) {
128
128
  | Some(chainState) => chainState.startBlock
129
- | None => chainConfig.startBlock
129
+ // Only before persistence is ready, which in a real run is before any
130
+ // handler module has loaded. The test indexer sits here for good.
131
+ | None => chainConfig->Config.startBlockOrZero
130
132
  }
131
133
  },
132
134
  },
@@ -589,6 +591,10 @@ let migrate = async (~reset) => {
589
591
  ~envioInfo=getEnvioInfo(),
590
592
  ~resetCommand="envio local db-migrate setup",
591
593
  ~runCommand=None,
594
+ ~lowercaseAddresses=config.lowercaseAddresses,
595
+ // A migration command runs once and exits, with nobody watching it recover:
596
+ // an unreachable chain should say so now rather than hold the command open.
597
+ ~startBlockRetry=StartBlockResolver.Once,
592
598
  )
593
599
  await persistence.storage.close()
594
600
  }
@@ -641,6 +647,7 @@ let start = async (
641
647
  ~envioInfo=getEnvioInfo(),
642
648
  ~resetCommand=isDevelopmentMode ? "envio dev -r" : "envio start -r",
643
649
  ~runCommand=Some(isDevelopmentMode ? "envio dev" : "envio start"),
650
+ ~lowercaseAddresses=config.lowercaseAddresses,
644
651
  )
645
652
 
646
653
  // Loads user handler files, which register handler/contractRegister/where
package/src/Main.res.mjs CHANGED
@@ -130,7 +130,7 @@ function buildChainsObject(config) {
130
130
  if (chainState !== undefined) {
131
131
  return chainState.startBlock;
132
132
  } else {
133
- return chainConfig.startBlock;
133
+ return Config.startBlockOrZero(chainConfig);
134
134
  }
135
135
  }
136
136
  }), "endBlock", {
@@ -467,7 +467,7 @@ function getEnvioInfo() {
467
467
  async function migrate(reset) {
468
468
  let config = Config.load();
469
469
  let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
470
- await Persistence.init(persistence, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset);
470
+ await Persistence.init(persistence, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), "envio local db-migrate setup", undefined, reset, config.lowercaseAddresses, "Once");
471
471
  return await persistence.storage.close();
472
472
  }
473
473
 
@@ -494,7 +494,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
494
494
  let isDevelopmentMode = !isTest && config.isDev;
495
495
  let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(config, undefined);
496
496
  EnvioGlobal.value.persistence = Primitive_option.some(persistence$1);
497
- await Persistence.init(persistence$1, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset);
497
+ await Persistence.init(persistence$1, ChainMap.values(config.chainMap), config.contractMapping, Config.stripSensitiveData(Config.getPublicConfigJson()), isDevelopmentMode ? "envio dev -r" : "envio start -r", isDevelopmentMode ? "envio dev" : "envio start", reset, config.lowercaseAddresses, undefined);
498
498
  let registrationsByChainId = await HandlerLoader.registerAllHandlers(config);
499
499
  let config$1;
500
500
  if (isTest) {
@@ -235,12 +235,14 @@ let make = (
235
235
  let init = {
236
236
  async (
237
237
  persistence,
238
- ~chainConfigs,
238
+ ~chainConfigs: array<Config.chain>,
239
239
  ~contractMapping,
240
240
  ~envioInfo,
241
241
  ~resetCommand,
242
242
  ~runCommand,
243
243
  ~reset=false,
244
+ ~lowercaseAddresses=false,
245
+ ~startBlockRetry=StartBlockResolver.UntilItAnswers,
244
246
  ) => {
245
247
  try {
246
248
  let shouldRun = switch persistence.storageStatus {
@@ -259,6 +261,14 @@ let init = {
259
261
  persistence.storageStatus = Initializing(promise)
260
262
  if reset || !(await persistence.storage.isInitialized()) {
261
263
  Logging.info(`Initializing the indexer storage...`)
264
+ // Only runs once per schema (this branch is the "first deploy or
265
+ // reset" gate), which is exactly when a `latest` start block must be
266
+ // resolved: every later resume reads `envio_chains.start_block` back
267
+ // verbatim instead of re-running this.
268
+ let chainConfigs = await chainConfigs->StartBlockResolver.resolveAllOrThrow(
269
+ ~lowercaseAddresses,
270
+ ~retry=startBlockRetry,
271
+ )
262
272
  let initialState = await persistence.storage.initialize(
263
273
  ~entities=persistence.allEntities,
264
274
  ~enums=persistence.allEnums,
@@ -5,6 +5,7 @@ import * as Logging from "./Logging.res.mjs";
5
5
  import * as EntityHistory from "./db/EntityHistory.res.mjs";
6
6
  import * as ErrorHandling from "./ErrorHandling.res.mjs";
7
7
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
+ import * as StartBlockResolver from "./sources/StartBlockResolver.res.mjs";
8
9
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
9
10
 
10
11
  let StorageError = /* @__PURE__ */Primitive_exceptions.create("Persistence.StorageError");
@@ -20,8 +21,10 @@ function make(userEntities, allEnums, storage) {
20
21
  };
21
22
  }
22
23
 
23
- async function init(persistence, chainConfigs, contractMapping, envioInfo, resetCommand, runCommand, resetOpt) {
24
+ async function init(persistence, chainConfigs, contractMapping, envioInfo, resetCommand, runCommand, resetOpt, lowercaseAddressesOpt, startBlockRetryOpt) {
24
25
  let reset = resetOpt !== undefined ? resetOpt : false;
26
+ let lowercaseAddresses = lowercaseAddressesOpt !== undefined ? lowercaseAddressesOpt : false;
27
+ let startBlockRetry = startBlockRetryOpt !== undefined ? startBlockRetryOpt : "UntilItAnswers";
25
28
  try {
26
29
  let promise = persistence.storageStatus;
27
30
  let shouldRun;
@@ -48,7 +51,8 @@ async function init(persistence, chainConfigs, contractMapping, envioInfo, reset
48
51
  };
49
52
  if (reset || !await persistence.storage.isInitialized()) {
50
53
  Logging.info(`Initializing the indexer storage...`);
51
- let initialState = await persistence.storage.initialize(chainConfigs, persistence.allEntities, persistence.allEnums, contractMapping, envioInfo);
54
+ let chainConfigs$1 = await StartBlockResolver.resolveAllOrThrow(chainConfigs, lowercaseAddresses, startBlockRetry, undefined, undefined);
55
+ let initialState = await persistence.storage.initialize(chainConfigs$1, persistence.allEntities, persistence.allEnums, contractMapping, envioInfo);
52
56
  Logging.info(`The indexer storage is ready. Starting indexing!`);
53
57
  persistence.storageStatus = {
54
58
  TAG: "Ready",
package/src/PgStorage.res CHANGED
@@ -1917,7 +1917,7 @@ let make = (
1917
1917
  idx,
1918
1918
  ): Persistence.initialChainState => {
1919
1919
  id: chainConfig.id,
1920
- startBlock: chainConfig.startBlock,
1920
+ startBlock: chainConfig->Config.startBlockOrThrow,
1921
1921
  endBlock: chainConfig.endBlock,
1922
1922
  maxReorgDepth: chainConfig.maxReorgDepth,
1923
1923
  progressBlockNumber: -1,
@@ -1161,7 +1161,7 @@ function make(sql, pgHost, pgSchema, pgPort, pgUser, pgDatabase, pgPassword, isH
1161
1161
  cache: cache,
1162
1162
  chains: chainConfigs.map((chainConfig, idx) => ({
1163
1163
  id: chainConfig.id,
1164
- startBlock: chainConfig.startBlock,
1164
+ startBlock: Config.startBlockOrThrow(chainConfig),
1165
1165
  endBlock: chainConfig.endBlock,
1166
1166
  maxReorgDepth: chainConfig.maxReorgDepth,
1167
1167
  progressBlockNumber: -1,
@@ -299,7 +299,7 @@ let parse = (
299
299
  ~onEventRegistrations: array<Internal.onEventRegistration>,
300
300
  ): parseResult => {
301
301
  let chainId = chainConfig.id
302
- let startBlock = chainConfig.startBlock
302
+ let startBlock = chainConfig->Config.startBlockOrZero
303
303
  let currentBlock = ref(startBlock)
304
304
  let currentLogIndex = ref(0)
305
305
 
@@ -358,15 +358,22 @@ let parse = (
358
358
  let path = item.path->Option.getOr([0])
359
359
  let programId =
360
360
  item.programId->Option.getOr(svmEventConfig.programId->SvmTypes.Pubkey.toString)
361
+ // `accountArguments` is the list the call carries, so it stands as
362
+ // written: a short one leaves the later slots absent, the way a
363
+ // truncated instruction reads from chain. Named accounts speak about
364
+ // some slots instead, so they go back onto their declared positions and
365
+ // a slot the item leaves out carries the program id — chain's own
366
+ // stand-in for an absent account, which reads back as absent for an
367
+ // optional slot and as the program for a required one.
361
368
  let accountArguments = switch item.accountArguments {
362
369
  | Some(args) => args
363
370
  | None =>
364
371
  switch item.accounts {
365
372
  | Some(named) =>
366
- svmEventConfig.accounts->Array.map(name =>
367
- switch named->Dict.get(name) {
368
- | Some({address}) => address
369
- | None => ""
373
+ svmEventConfig.accounts->Array.map(slot =>
374
+ switch slot->Internal.svmAccountSlotName {
375
+ | None => programId
376
+ | Some(name) => named->Dict.get(name)->Option.mapOr(programId, ({address}) => address)
370
377
  }
371
378
  )
372
379
  | None => []
@@ -695,7 +702,7 @@ let patchConfig = (
695
702
  let endBlock: int = raw["endBlock"]->(Utils.magic: 'a => int)
696
703
  // Parse with the process's startBlock so items default into the range
697
704
  // the source will be queried over; the source now filters by range.
698
- let chainConfig = {...chainConfig, startBlock, endBlock}
705
+ let chainConfig = {...chainConfig, startBlock: Config.Block(startBlock), endBlock}
699
706
  let {items, transactionStore, blockStore} = parse(
700
707
  ~simulateItems,
701
708
  ~config,
@@ -5,6 +5,7 @@ import * as Config from "./Config.res.mjs";
5
5
  import * as Address from "./Address.res.mjs";
6
6
  import * as ChainId from "./ChainId.res.mjs";
7
7
  import * as ChainMap from "./ChainMap.res.mjs";
8
+ import * as Internal from "./Internal.res.mjs";
8
9
  import * as BlockStore from "./sources/BlockStore.res.mjs";
9
10
  import * as Stdlib_Null from "@rescript/runtime/lib/es6/Stdlib_Null.js";
10
11
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
@@ -184,7 +185,7 @@ function liveRegistrationsFor(config, chainId, eventConfig) {
184
185
 
185
186
  function parse(simulateItems, config, chainConfig, onEventRegistrations) {
186
187
  let chainId = chainConfig.id;
187
- let startBlock = chainConfig.startBlock;
188
+ let startBlock = Config.startBlockOrZero(chainConfig);
188
189
  let currentBlock = {
189
190
  contents: startBlock
190
191
  };
@@ -236,12 +237,12 @@ function parse(simulateItems, config, chainConfig, onEventRegistrations) {
236
237
  accountArguments = args;
237
238
  } else {
238
239
  let named = rawJson.accounts;
239
- accountArguments = named !== undefined ? eventConfig.accounts.map(name => {
240
- let match = named[name];
241
- if (match !== undefined) {
242
- return match.address;
240
+ accountArguments = named !== undefined ? eventConfig.accounts.map(slot => {
241
+ let name = Internal.svmAccountSlotName(slot);
242
+ if (name !== undefined) {
243
+ return Stdlib_Option.mapOr(named[name], programId, param => param.address);
243
244
  } else {
244
- return "";
245
+ return programId;
245
246
  }
246
247
  }) : [];
247
248
  }
@@ -355,13 +355,14 @@ let parseBlockRange = (
355
355
  JsError.throwWithMessage(`Chain ${chainIdStr} is not configured in config.yaml`)
356
356
  }
357
357
  let configChain = config.chainMap->ChainMap.get(chain)
358
+ let configStartBlock = configChain->Config.startBlockOrZero
358
359
 
359
360
  let startBlock = switch rawChainConfig.startBlock {
360
361
  | Some(sb) => sb
361
362
  | None =>
362
363
  switch progressBlock {
363
364
  | Some(prevEndBlock) => prevEndBlock + 1
364
- | None => configChain.startBlock
365
+ | None => configStartBlock
365
366
  }
366
367
  }
367
368
 
@@ -378,10 +379,10 @@ let parseBlockRange = (
378
379
  | None => None // auto-exit mode: will fetch first block with events and exit
379
380
  }
380
381
 
381
- if startBlock < configChain.startBlock {
382
+ if startBlock < configStartBlock {
382
383
  JsError.throwWithMessage(
383
- `Invalid block range for chain ${chainIdStr}: startBlock (${startBlock->Int.toString}) is less than config.startBlock (${configChain.startBlock->Int.toString}). ` ++
384
- `Either use startBlock >= ${configChain.startBlock->Int.toString} or create a new test indexer with createTestIndexer().`,
384
+ `Invalid block range for chain ${chainIdStr}: startBlock (${startBlock->Int.toString}) is less than config.startBlock (${configStartBlock->Int.toString}). ` ++
385
+ `Either use startBlock >= ${configStartBlock->Int.toString} or create a new test indexer with createTestIndexer().`,
385
386
  )
386
387
  }
387
388
 
@@ -754,7 +755,7 @@ let createTestIndexer = (): t<'processConfig> => {
754
755
  ->Utils.Object.definePropertyWithValue("id", {enumerable: true, value: chainConfig.id})
755
756
  ->Utils.Object.definePropertyWithValue(
756
757
  "startBlock",
757
- {enumerable: true, value: chainConfig.startBlock},
758
+ {enumerable: true, value: chainConfig->Config.startBlockOrZero},
758
759
  )
759
760
  ->Utils.Object.definePropertyWithValue(
760
761
  "endBlock",