envio 3.7.0 → 3.9.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 (79) hide show
  1. package/README.md +2 -2
  2. package/index.d.ts +399 -156
  3. package/package.json +6 -6
  4. package/src/AddressRows.res +121 -0
  5. package/src/AddressRows.res.mjs +143 -0
  6. package/src/Batch.res +2 -0
  7. package/src/Batch.res.mjs +2 -1
  8. package/src/ChainState.res +82 -68
  9. package/src/ChainState.res.mjs +81 -65
  10. package/src/ChainState.resi +14 -9
  11. package/src/Config.res +41 -91
  12. package/src/Config.res.mjs +34 -68
  13. package/src/ContractMapping.res +66 -0
  14. package/src/ContractMapping.res.mjs +73 -0
  15. package/src/Core.res +20 -0
  16. package/src/Core.res.mjs +4 -0
  17. package/src/Envio.res +54 -76
  18. package/src/EventConfigBuilder.res +157 -30
  19. package/src/EventConfigBuilder.res.mjs +118 -24
  20. package/src/FetchState.res +59 -67
  21. package/src/FetchState.res.mjs +34 -44
  22. package/src/HandlerRegister.res +20 -11
  23. package/src/HandlerRegister.res.mjs +22 -5
  24. package/src/Hasura.res +30 -22
  25. package/src/Hasura.res.mjs +11 -5
  26. package/src/InMemoryStore.res +18 -36
  27. package/src/InMemoryStore.res.mjs +12 -26
  28. package/src/IndexerState.res +21 -1
  29. package/src/IndexerState.res.mjs +9 -3
  30. package/src/IndexerState.resi +1 -0
  31. package/src/Internal.res +33 -13
  32. package/src/Internal.res.mjs +12 -16
  33. package/src/Main.res +27 -20
  34. package/src/Main.res.mjs +19 -14
  35. package/src/MemoryStorage.res +78 -73
  36. package/src/MemoryStorage.res.mjs +66 -81
  37. package/src/Metrics.res +15 -1
  38. package/src/Metrics.res.mjs +5 -1
  39. package/src/Persistence.res +34 -18
  40. package/src/Persistence.res.mjs +8 -5
  41. package/src/PgStorage.res +189 -66
  42. package/src/PgStorage.res.mjs +93 -59
  43. package/src/Rollback.res +14 -10
  44. package/src/Rollback.res.mjs +4 -2
  45. package/src/SimulateItems.res +254 -9
  46. package/src/SimulateItems.res.mjs +214 -47
  47. package/src/TestIndexer.res +119 -121
  48. package/src/TestIndexer.res.mjs +95 -100
  49. package/src/Utils.res +7 -0
  50. package/src/Utils.res.mjs +9 -2
  51. package/src/Writing.res +2 -0
  52. package/src/Writing.res.mjs +2 -1
  53. package/src/bindings/ClickHouse.res +39 -6
  54. package/src/bindings/ClickHouse.res.mjs +29 -4
  55. package/src/bindings/NodeJs.res +9 -0
  56. package/src/bindings/NodeJs.res.mjs +8 -1
  57. package/src/bindings/Postgres.res +9 -0
  58. package/src/bindings/Postgres.res.mjs +3 -0
  59. package/src/db/EntityHistory.res +12 -18
  60. package/src/db/EntityHistory.res.mjs +3 -14
  61. package/src/db/InternalTable.res +182 -65
  62. package/src/db/InternalTable.res.mjs +183 -73
  63. package/src/db/Table.res +24 -0
  64. package/src/db/Table.res.mjs +20 -1
  65. package/src/sources/AddressSet.res +0 -22
  66. package/src/sources/AddressStore.res +48 -88
  67. package/src/sources/AddressStore.res.mjs +11 -22
  68. package/src/sources/EvmHyperSyncSource.res +3 -2
  69. package/src/sources/SimulateSource.res +8 -4
  70. package/src/sources/SimulateSource.res.mjs +7 -3
  71. package/src/sources/Svm.res +34 -7
  72. package/src/sources/Svm.res.mjs +32 -4
  73. package/src/sources/SvmHyperSyncClient.res +24 -30
  74. package/src/sources/SvmHyperSyncClient.res.mjs +3 -2
  75. package/src/sources/SvmHyperSyncSource.res +88 -36
  76. package/src/sources/SvmHyperSyncSource.res.mjs +71 -39
  77. package/src/sources/TransactionStore.res +38 -0
  78. package/src/sources/TransactionStore.res.mjs +5 -0
  79. package/svm.schema.json +37 -82
@@ -10,6 +10,12 @@ type fuelChainConfig = {
10
10
  simulate?: array<Envio.fuelSimulateItem>,
11
11
  }
12
12
 
13
+ type svmChainConfig = {
14
+ startBlock?: int,
15
+ endBlock?: int,
16
+ simulate?: array<Envio.svmSimulateItem>,
17
+ }
18
+
13
19
  // Internal type used for block range validation and state management
14
20
  type chainConfig = {
15
21
  startBlock: int,
@@ -31,44 +37,19 @@ type testIndexerState = {
31
37
  // Store decoded entities (not JSON) for proper comparison operations
32
38
  entities: dict<dict<Internal.entity>>,
33
39
  entityConfigs: dict<Internal.entityConfig>,
40
+ addresses: AddressRows.Table.t,
41
+ contractMapping: ContractMapping.t,
34
42
  mutable processChanges: array<unknown>,
35
43
  }
36
44
 
37
- // Cast Internal.entity back to EnvioAddresses.t
38
- external castToEnvioAddresses: Internal.entity => InternalTable.EnvioAddresses.t = "%identity"
45
+ let addressRowsByChain = (state: testIndexerState) =>
46
+ state.addresses->AddressRows.Table.groupByChain
39
47
 
40
- let toIndexingAddress = (dc: InternalTable.EnvioAddresses.t): Internal.indexingAddress => {
41
- address: dc->Config.EnvioAddresses.getAddress,
42
- contractName: dc.contractName,
43
- registrationBlock: dc.registrationBlock,
44
- }
45
-
46
- // All indexing addresses (config-seeded + dynamically registered) grouped by
47
- // chain id string, derived from the envio_addresses entities.
48
- let getIndexingAddressesByChain = (state: testIndexerState): dict<
49
- array<Internal.indexingAddress>,
50
- > => {
51
- let byChain = Dict.make()
52
- switch state.entities->Dict.get(InternalTable.EnvioAddresses.name) {
53
- | Some(dcDict) =>
54
- dcDict
55
- ->Dict.valuesToArray
56
- ->Array.forEach(entity => {
57
- let dc = entity->castToEnvioAddresses
58
- let chainIdStr = dc.chainId->ChainId.toString
59
- let contracts = switch byChain->Dict.get(chainIdStr) {
60
- | Some(arr) => arr
61
- | None =>
62
- let arr = []
63
- byChain->Dict.set(chainIdStr, arr)
64
- arr
65
- }
66
- contracts->Array.push(dc->toIndexingAddress)->ignore
67
- })
68
- | None => ()
69
- }
70
- byChain
71
- }
48
+ let renderRows = (rows: array<AddressRows.row>, ~config: Config.t) =>
49
+ rows->AddressRows.render(
50
+ ~ecosystem=(config.ecosystem.name :> string),
51
+ ~shouldChecksum=!config.lowercaseAddresses,
52
+ )
72
53
 
73
54
  // Rows of a per-chain entity are keyed per (chain, id): the same id exists
74
55
  // independently on every chain.
@@ -119,7 +100,9 @@ let handleLoad = (state: testIndexerState, ~tableName: string, ~filter: EntityFi
119
100
 
120
101
  let handleWriteBatch = (
121
102
  state: testIndexerState,
103
+ ~config: Config.t,
122
104
  ~updatedEntities: array<Persistence.updatedEntity>,
105
+ ~registeredAddresses: array<AddressRows.staged>,
123
106
  ~checkpointIds: array<bigint>,
124
107
  ~checkpointChainIds: array<ChainId.t>,
125
108
  ~checkpointBlockNumbers: array<int>,
@@ -129,6 +112,22 @@ let handleWriteBatch = (
129
112
  // checkpointId -> entityName -> entityChange
130
113
  let changesByCheckpoint: dict<dict<entityChange>> = Dict.make()
131
114
 
115
+ // checkpointId -> the addresses that checkpoint registered, rendered for the
116
+ // change log. Rendering is the Rust codec's job, so the rows keep their keys
117
+ // right up to here.
118
+ let addressesByCheckpoint: dict<array<{"address": Address.t, "contract": string}>> = Dict.make()
119
+ let rendered = registeredAddresses->Array.map(({row}) => row)->renderRows(~config)
120
+ registeredAddresses->Array.forEachWithIndex(({row, checkpointId}, idx) => {
121
+ state.addresses->AddressRows.Table.insert(row)
122
+ addressesByCheckpoint->Utils.Dict.push(
123
+ checkpointId->BigInt.toString,
124
+ {
125
+ "address": rendered->Array.getUnsafe(idx),
126
+ "contract": state.contractMapping->ContractMapping.nameOfOrThrow(row.contractId),
127
+ },
128
+ )
129
+ })
130
+
132
131
  updatedEntities->Array.forEach(({entityConfig, scope, changes}: Persistence.updatedEntity) => {
133
132
  let entityName = entityConfig.name
134
133
  // The scope is what makes a per-chain row identifiable, so it's stamped
@@ -204,43 +203,33 @@ let handleWriteBatch = (
204
203
 
205
204
  // Add entity changes for this checkpoint
206
205
  let checkpointKey = checkpointId->BigInt.toString
206
+ switch addressesByCheckpoint->Utils.Dict.dangerouslyGetNonOption(checkpointKey) {
207
+ | Some(rendered) =>
208
+ let addressesObj: dict<unknown> = Dict.make()
209
+ addressesObj->Dict.set(
210
+ "sets",
211
+ rendered->(Utils.magic: array<{"address": Address.t, "contract": string}> => unknown),
212
+ )
213
+ change->Dict.set("addresses", addressesObj->(Utils.magic: dict<unknown> => unknown))
214
+ | None => ()
215
+ }
207
216
  switch changesByCheckpoint->Dict.get(checkpointKey) {
208
217
  | Some(entityChanges) =>
209
218
  entityChanges
210
219
  ->Dict.toArray
211
220
  ->Array.forEach(((entityName, {sets, deleted})) => {
212
- // Transform envio_addresses to addresses with simplified structure
213
- if entityName === InternalTable.EnvioAddresses.name {
214
- let entityObj: dict<unknown> = Dict.make()
215
- if sets->Array.length > 0 {
216
- // Transform sets to simplified {address, contract} objects
217
- let simplifiedSets = sets->Array.map(entity => {
218
- let dc = entity->Utils.magic->castToEnvioAddresses
219
- {"address": dc->Config.EnvioAddresses.getAddress, "contract": dc.contractName}
220
- })
221
- entityObj->Dict.set(
222
- "sets",
223
- simplifiedSets->(
224
- Utils.magic: array<{"address": Address.t, "contract": string}> => unknown
225
- ),
226
- )
227
- }
228
- // Note: deleted is not relevant for addresses since we use address string directly
229
- change->Dict.set("addresses", entityObj->(Utils.magic: dict<unknown> => unknown))
230
- } else {
231
- let entityObj: dict<unknown> = Dict.make()
232
- if sets->Array.length > 0 {
233
- entityObj->Dict.set("sets", sets->(Utils.magic: array<unknown> => unknown))
234
- }
235
- if deleted->Array.length > 0 {
236
- entityObj->Dict.set("deleted", deleted->(Utils.magic: array<EntityId.t> => unknown))
237
- }
238
- // Match the capitalized entity accessor the generated change types expose.
239
- change->Dict.set(
240
- entityName->Utils.String.capitalize,
241
- entityObj->(Utils.magic: dict<unknown> => unknown),
242
- )
221
+ let entityObj: dict<unknown> = Dict.make()
222
+ if sets->Array.length > 0 {
223
+ entityObj->Dict.set("sets", sets->(Utils.magic: array<unknown> => unknown))
243
224
  }
225
+ if deleted->Array.length > 0 {
226
+ entityObj->Dict.set("deleted", deleted->(Utils.magic: array<EntityId.t> => unknown))
227
+ }
228
+ // Match the capitalized entity accessor the generated change types expose.
229
+ change->Dict.set(
230
+ entityName->Utils.String.capitalize,
231
+ entityObj->(Utils.magic: dict<unknown> => unknown),
232
+ )
244
233
  })
245
234
  | None => ()
246
235
  }
@@ -254,7 +243,8 @@ let handleWriteBatch = (
254
243
  let makeInitialState = (
255
244
  ~config: Config.t,
256
245
  ~processConfigChains: dict<chainConfig>,
257
- ~indexingAddressesByChain: dict<array<Internal.indexingAddress>>,
246
+ ~addressRowsByChain: dict<AddressRows.seedRows>,
247
+ ~contractMapping: ContractMapping.t,
258
248
  ): Persistence.initialState => {
259
249
  let chainKeys = processConfigChains->Dict.keysToArray
260
250
  let chains = chainKeys->Array.map(chainIdStr => {
@@ -265,7 +255,10 @@ let makeInitialState = (
265
255
  }
266
256
 
267
257
  let processChainConfig = processConfigChains->Dict.getUnsafe(chainIdStr)
268
- let indexingAddresses = indexingAddressesByChain->Dict.get(chainIdStr)->Option.getOr([])
258
+ let addressRows =
259
+ addressRowsByChain
260
+ ->Utils.Dict.dangerouslyGetNonOption(chainIdStr)
261
+ ->Option.getOr(AddressRows.emptySeedRows())
269
262
  {
270
263
  Persistence.id: chain,
271
264
  startBlock: processChainConfig.startBlock,
@@ -276,17 +269,18 @@ let makeInitialState = (
276
269
  numEventsProcessed: 0.,
277
270
  firstEventBlockNumber: None,
278
271
  timestampCaughtUpToHeadOrEndblock: None,
279
- indexingAddresses,
272
+ addressRows,
280
273
  }
281
274
  })
282
275
 
283
276
  {
284
277
  cleanRun: true,
278
+ contractMapping,
279
+ envioInfo: Some(JSON.Encode.object(Dict.make())),
285
280
  cache: Dict.make(),
286
281
  chains,
287
282
  checkpointId: InternalTable.Checkpoints.initialCheckpointId,
288
283
  reorgCheckpoints: [],
289
- envioInfo: Some(Config.getPublicConfigJson()->Config.stripSensitiveData),
290
284
  }
291
285
  }
292
286
 
@@ -314,22 +308,32 @@ let getSimulateEndBlock = (
314
308
  ~startBlock: int,
315
309
  ): int => {
316
310
  let maxBlock = ref(startBlock)
311
+ let blockNumberKey = switch config.ecosystem.name {
312
+ | Svm => "slot"
313
+ | _ => config.ecosystem.blockNumberName
314
+ }
315
+ let bump = (n: option<int>) =>
316
+ switch n {
317
+ | Some(v) if v > maxBlock.contents => maxBlock := v
318
+ | _ => ()
319
+ }
320
+ let getInt = (d: dict<JSON.t>, key) =>
321
+ d
322
+ ->Dict.get(key)
323
+ ->Option.flatMap(v => v->(Utils.magic: JSON.t => Nullable.t<int>)->Nullable.toOption)
317
324
  simulateItems->Array.forEach(rawJson => {
325
+ let itemDict = rawJson->(Utils.magic: JSON.t => dict<JSON.t>)
326
+ // SVM items carry the slot at the top level (`block.slot` is the override).
327
+ switch config.ecosystem.name {
328
+ | Svm => itemDict->getInt("slot")->bump
329
+ | _ => ()
330
+ }
318
331
  let blockJson: option<JSON.t> =
319
332
  (rawJson->(Utils.magic: JSON.t => {..}))["block"]
320
333
  ->(Utils.magic: 'a => Nullable.t<JSON.t>)
321
334
  ->Nullable.toOption
322
335
  switch blockJson {
323
- | Some(bj) =>
324
- let blockDict = bj->(Utils.magic: JSON.t => dict<JSON.t>)
325
- let n: option<int> =
326
- blockDict
327
- ->Dict.get(config.ecosystem.blockNumberName)
328
- ->Option.flatMap(v => v->(Utils.magic: JSON.t => Nullable.t<int>)->Nullable.toOption)
329
- switch n {
330
- | Some(v) if v > maxBlock.contents => maxBlock := v
331
- | _ => ()
332
- }
336
+ | Some(bj) => bj->(Utils.magic: JSON.t => dict<JSON.t>)->getInt(blockNumberKey)->bump
333
337
  | None => ()
334
338
  }
335
339
  })
@@ -575,7 +579,13 @@ let makeInMemoryStorage = (~state: testIndexerState): Persistence.storage => {
575
579
  // The runner injects the config-derived initial state by setting
576
580
  // `persistence.storageStatus = Ready(...)` directly, bypassing `Persistence.init`,
577
581
  // so neither of these is reached.
578
- initialize: async (~chainConfigs as _=?, ~entities as _=?, ~enums as _=?, ~envioInfo as _) =>
582
+ initialize: async (
583
+ ~chainConfigs as _=?,
584
+ ~entities as _=?,
585
+ ~enums as _=?,
586
+ ~contractMapping as _,
587
+ ~envioInfo as _,
588
+ ) =>
579
589
  JsError.throwWithMessage(
580
590
  "TestIndexer: initialize should not be called; the initial state is derived from config.",
581
591
  ),
@@ -595,15 +605,18 @@ let makeInMemoryStorage = (~state: testIndexerState): Persistence.storage => {
595
605
  ~batch,
596
606
  ~rollback as _,
597
607
  ~isInReorgThreshold as _,
598
- ~config as _,
608
+ ~config,
599
609
  ~allEntities as _,
600
610
  ~updatedEffectsCache as _,
601
611
  ~updatedEntities,
612
+ ~registeredAddresses,
602
613
  ~chainMetaData as _,
603
614
  ~onWrite as _,
604
615
  ) =>
605
616
  state->handleWriteBatch(
617
+ ~config,
606
618
  ~updatedEntities,
619
+ ~registeredAddresses,
607
620
  ~checkpointIds=batch.checkpointIds,
608
621
  ~checkpointChainIds=batch.checkpointChainIds,
609
622
  ~checkpointBlockNumbers=batch.checkpointBlockNumbers,
@@ -672,7 +685,7 @@ let getRegistrations = (~config) =>
672
685
 
673
686
  let createTestIndexer = (): t<'processConfig> => {
674
687
  let config = Config.load()
675
- let allEntities = config.allEntities
688
+ let allEntities = config.userEntities
676
689
  let entities = Dict.make()
677
690
  let entityConfigs = Dict.make()
678
691
  allEntities->Array.forEach(entityConfig => {
@@ -680,32 +693,19 @@ let createTestIndexer = (): t<'processConfig> => {
680
693
  entityConfigs->Dict.set(entityConfig.name, entityConfig)
681
694
  })
682
695
 
683
- // Populate config addresses into the entity dict, mirroring PgStorage.initialize
684
- let envioAddressesDict = entities->Dict.getUnsafe(InternalTable.EnvioAddresses.name)
685
- config.chainMap
686
- ->ChainMap.values
687
- ->Array.forEach(chainConfig => {
688
- chainConfig.contracts->Array.forEach(contract => {
689
- contract.addresses->Array.forEach(
690
- address => {
691
- let entity: InternalTable.EnvioAddresses.t = {
692
- id: Config.EnvioAddresses.makeId(~chainId=chainConfig.id, ~address),
693
- chainId: chainConfig.id,
694
- contractName: contract.name,
695
- registrationBlock: -1,
696
- registrationLogIndex: -1,
697
- }
698
- envioAddressesDict->Dict.set(entity.id, entity->Config.EnvioAddresses.castToInternal)
699
- },
700
- )
701
- })
702
- })
696
+ let chainConfigs = config.chainMap->ChainMap.values
697
+ let contractMapping = config.contractMapping
698
+ let ecosystem = config.ecosystem.name
699
+ let addresses = AddressRows.Table.make()
700
+ addresses->ChainState.seedConfigAddresses(~chainConfigs, ~ecosystem, ~contractMapping)
703
701
 
704
702
  let state = {
705
703
  processInProgress: false,
706
704
  progressBlockByChain: Dict.make(),
707
705
  entities,
708
706
  entityConfigs,
707
+ addresses,
708
+ contractMapping,
709
709
  processChanges: [],
710
710
  }
711
711
 
@@ -728,19 +728,16 @@ let createTestIndexer = (): t<'processConfig> => {
728
728
  // Build entity operations for each user entity
729
729
  let entityOpsDict: dict<entityOperations> = Dict.make()
730
730
  allEntities->Array.forEach(entityConfig => {
731
- // Only create ops for user entities (not internal tables like envio_addresses)
732
- if entityConfig.name !== InternalTable.EnvioAddresses.name {
733
- entityOpsDict->Dict.set(
734
- entityConfig.name,
735
- {
736
- get: makeEntityGet(~state, ~entityConfig),
737
- getAll: makeEntityGetAll(~state, ~entityConfig),
738
- getWhere: makeEntityGetWhere(~state, ~entityConfig),
739
- getOrThrow: makeEntityGetOrThrow(~state, ~entityConfig),
740
- set: makeEntitySet(~state, ~entityConfig),
741
- },
742
- )
743
- }
731
+ entityOpsDict->Dict.set(
732
+ entityConfig.name,
733
+ {
734
+ get: makeEntityGet(~state, ~entityConfig),
735
+ getAll: makeEntityGetAll(~state, ~entityConfig),
736
+ getWhere: makeEntityGetWhere(~state, ~entityConfig),
737
+ getOrThrow: makeEntityGetOrThrow(~state, ~entityConfig),
738
+ set: makeEntitySet(~state, ~entityConfig),
739
+ },
740
+ )
744
741
  })
745
742
 
746
743
  // Build chain info from config (similar to Main.getGlobalIndexer but static)
@@ -783,10 +780,11 @@ let createTestIndexer = (): t<'processConfig> => {
783
780
  `Cannot access ${contract.name}.addresses while indexer.process() is running. ` ++ "Wait for process() to complete before reading contract addresses.",
784
781
  )
785
782
  }
786
- getIndexingAddressesByChain(state)
787
- ->Dict.get(chainConfig.id->ChainId.toString)
788
- ->Option.getOr([])
789
- ->Array.filterMap(ia => ia.contractName === contract.name ? Some(ia.address) : None)
783
+ let contractId = state.contractMapping->ContractMapping.idOfOrThrow(contract.name)
784
+ state.addresses
785
+ ->AddressRows.Table.rows
786
+ ->Array.filter(row => row.chainId === chainConfig.id && row.contractId === contractId)
787
+ ->renderRows(~config)
790
788
  },
791
789
  },
792
790
  )
@@ -883,11 +881,11 @@ let createTestIndexer = (): t<'processConfig> => {
883
881
  // by earlier ones.
884
882
  let chains: dict<chainConfig> = Dict.make()
885
883
  chains->Dict.set(chainIdStr, processChainConfig)
886
- let indexingAddressesByChain = getIndexingAddressesByChain(state)
887
884
  let initialState = makeInitialState(
888
885
  ~config,
889
886
  ~processConfigChains=chains,
890
- ~indexingAddressesByChain,
887
+ ~addressRowsByChain=addressRowsByChain(state),
888
+ ~contractMapping=state.contractMapping,
891
889
  )
892
890
 
893
891
  // No endBlock means auto-exit mode: process one block checkpoint at a