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
package/src/PgStorage.res CHANGED
@@ -86,6 +86,7 @@ let makeCreateTableQuery = (
86
86
  ~pgSchema,
87
87
  ~isNumericArrayAsText,
88
88
  ~chainIdMode: ChainId.mode=Int32,
89
+ ~partitionByColumn: option<string>=?,
89
90
  ) => {
90
91
  let fieldsMapped =
91
92
  table
@@ -115,7 +116,23 @@ let makeCreateTableQuery = (
115
116
 
116
117
  `CREATE TABLE IF NOT EXISTS "${pgSchema}"."${table.tableName}"(${fieldsMapped}${primaryKeyFieldNames->Array.length > 0
117
118
  ? `, PRIMARY KEY(${primaryKey})`
118
- : ""});`
119
+ : ""})${switch partitionByColumn {
120
+ | Some(column) => ` PARTITION BY LIST ("${column}")`
121
+ | None => ""
122
+ }};`
123
+ }
124
+
125
+ // A per-chain entity's rows are partitioned by the chain that owns them, so a
126
+ // chain-filtered read scans one chain's partition rather than the whole table.
127
+ // `$` can't occur in a GraphQL entity name, so a partition name can never
128
+ // collide with the table another entity claims; past the identifier limit the
129
+ // entity index keeps what survives truncation unique.
130
+ let partitionTableName = (~entityConfig: Internal.entityConfig, ~chainId: ChainId.t) => {
131
+ let chainIdStr = chainId->ChainId.toString
132
+ Table.fitPgTableName(
133
+ `${entityConfig.table.tableName}$${chainIdStr}`,
134
+ ~uniqueSuffix=`$${entityConfig.index->Int.toString}$${chainIdStr}`,
135
+ )
119
136
  }
120
137
 
121
138
  // The entity as it's stored: the handler-visible schema plus the chain-id
@@ -222,6 +239,40 @@ let getEntityHistory = (~entityConfig: Internal.entityConfig): EntityHistory.pgE
222
239
  }
223
240
  }
224
241
 
242
+ // Every table an entity needs: its own, one partition per chain when it's
243
+ // per-chain, and its history table. The chain set is fixed for the life of a
244
+ // schema — changing it fails the resume compat check against `envio_info` and
245
+ // forces a resync — so every partition the entity will ever need is created
246
+ // here, at init.
247
+ //
248
+ // History stays unpartitioned: it is only ever read by checkpoint, never by
249
+ // chain, so partitioning it would route every write and prune nothing.
250
+ let makeCreateEntityTableQueries = (
251
+ entityConfig: Internal.entityConfig,
252
+ ~pgSchema,
253
+ ~isNumericArrayAsText,
254
+ ~chainIdMode: ChainId.mode=Int32,
255
+ ~chainIds: array<ChainId.t>,
256
+ ) => {
257
+ let createTable = (table, ~partitionByColumn=?) =>
258
+ makeCreateTableQuery(table, ~pgSchema, ~isNumericArrayAsText, ~chainIdMode, ~partitionByColumn?)
259
+
260
+ switch entityConfig.table->Table.getChainIdField {
261
+ | None => [entityConfig.table->createTable]
262
+ | Some(chainIdField) =>
263
+ [
264
+ entityConfig.table->createTable(~partitionByColumn=chainIdField->Table.getPgDbFieldName),
265
+ ]->Array.concat(
266
+ chainIds->Array.map(chainId =>
267
+ `CREATE TABLE IF NOT EXISTS "${pgSchema}"."${partitionTableName(
268
+ ~entityConfig,
269
+ ~chainId,
270
+ )}" PARTITION OF "${pgSchema}"."${entityConfig.table.tableName}" FOR VALUES IN (${chainId->ChainId.toString});`
271
+ ),
272
+ )
273
+ }->Array.concat([getEntityHistory(~entityConfig).table->createTable])
274
+ }
275
+
225
276
  let makeInitializeTransaction = (
226
277
  ~pgSchema,
227
278
  ~pgUser,
@@ -239,15 +290,29 @@ let makeInitializeTransaction = (
239
290
  let generalTables = [
240
291
  InternalTable.Chains.table,
241
292
  InternalTable.EnvioInfo.table,
293
+ InternalTable.EnvioContracts.table,
294
+ InternalTable.EnvioAddresses.table,
242
295
  InternalTable.Checkpoints.table,
243
296
  InternalTable.RawEvents.table,
244
297
  ]
245
298
 
246
- let allTables = generalTables->Array.copy
247
- entities->Array.forEach((entityConfig: Internal.entityConfig) => {
248
- allTables->Array.push(entityConfig.table)->ignore
249
- allTables->Array.push(getEntityHistory(~entityConfig).table)->ignore
250
- })
299
+ let chainIds = chainConfigs->Array.map((chainConfig: Config.chain) => chainConfig.id)
300
+
301
+ let tableQueries =
302
+ generalTables
303
+ ->Array.map(table =>
304
+ makeCreateTableQuery(table, ~pgSchema, ~isNumericArrayAsText=isHasuraEnabled, ~chainIdMode)
305
+ )
306
+ ->Array.concat(
307
+ entities->Array.flatMap((entityConfig: Internal.entityConfig) =>
308
+ entityConfig->makeCreateEntityTableQueries(
309
+ ~pgSchema,
310
+ ~isNumericArrayAsText=isHasuraEnabled,
311
+ ~chainIdMode,
312
+ ~chainIds,
313
+ )
314
+ ),
315
+ )
251
316
 
252
317
  let schemaIndexes = getSchemaIndexes(~entities)
253
318
 
@@ -276,11 +341,8 @@ GRANT ALL ON SCHEMA "${pgSchema}" TO public;`,
276
341
  })
277
342
 
278
343
  // Batch all table creation first (optimal for PostgreSQL)
279
- allTables->Array.forEach((table: Table.table) => {
280
- query :=
281
- query.contents ++
282
- "\n" ++
283
- makeCreateTableQuery(table, ~pgSchema, ~isNumericArrayAsText=isHasuraEnabled, ~chainIdMode)
344
+ tableQueries->Array.forEach(tableQuery => {
345
+ query := query.contents ++ "\n" ++ tableQuery
284
346
  })
285
347
 
286
348
  // Then batch all indexes (better performance when tables exist)
@@ -360,6 +422,26 @@ let rec makeFilterCondition = (
360
422
  )}`
361
423
  }
362
424
  switch filter {
425
+ // A per-chain entity's table is partitioned by its chain-id column, and
426
+ // Postgres can only prune a plan it caches when that column is a constant in
427
+ // the SQL. Bound, the cached plan has to keep every partition, and the
428
+ // planner ends up throwing it away and re-planning on every execution
429
+ // instead — measured at 315us per load against 218us with the id written in,
430
+ // on 30 chains.
431
+ //
432
+ // The cost is that each chain gets its own query text, so Postgres caches a
433
+ // prepared statement per (entity, chain, filter shape) rather than per
434
+ // (entity, filter shape). Measured at ~8KB of plan cache each, which is ~10MB
435
+ // per connection for 40 entities across 30 chains — accepted, since the
436
+ // alternative is a cached plan that can't prune.
437
+ //
438
+ // `LoadLayer.scopeFilter` is what puts this filter here, and the value is
439
+ // range-checked to a non-negative safe integer, so it can carry nothing but
440
+ // digits.
441
+ | Eq({fieldName, fieldValue}) if (getQueryFieldOrThrow(fieldName)).isChainId =>
442
+ `"${(getQueryFieldOrThrow(fieldName)).pgDbFieldName}" = ${fieldValue
443
+ ->ChainId.normalizeOrThrow
444
+ ->ChainId.toString}`
363
445
  | Eq({fieldName, fieldValue}) => scalarCondition(~fieldName, ~fieldValue, ~op="=")
364
446
  | Gt({fieldName, fieldValue}) => scalarCondition(~fieldName, ~fieldValue, ~op=">")
365
447
  | Lt({fieldName, fieldValue}) => scalarCondition(~fieldName, ~fieldValue, ~op="<")
@@ -387,12 +469,16 @@ let rec makeFilterCondition = (
387
469
  }
388
470
 
389
471
  // The chain-id predicate a per-chain entity's row-level SQL needs, already
390
- // including the leading AND. The chain id is bound as $2 — after the id
391
- // params at $1 so one prepared statement serves every chain. Empty for
392
- // cross-chain entities and for internal tables, which have no such column.
472
+ // including the leading AND. Empty for cross-chain entities and for internal
473
+ // tables, which have no such column.
474
+ //
475
+ // The chain id is written into the SQL rather than bound, because the table is
476
+ // partitioned by it — see `makeFilterCondition` for why a partition key has to
477
+ // be a constant.
393
478
  let makeChainIdCondition = (~table: Table.table, ~chainId: option<ChainId.t>) =>
394
479
  switch (table->Table.getChainIdField, chainId) {
395
- | (Some(field), Some(_)) => ` AND "${field->Table.getPgDbFieldName}" = $2`
480
+ | (Some(field), Some(chainId)) =>
481
+ ` AND "${field->Table.getPgDbFieldName}" = ${chainId->ChainId.toString}`
396
482
  | _ => ""
397
483
  }
398
484
 
@@ -835,24 +921,16 @@ let deleteByIdsOrThrow = async (
835
921
  ~chainId: option<ChainId.t>=None,
836
922
  ) => {
837
923
  let chainIdCondition = makeChainIdCondition(~table, ~chainId)
838
- let chainIdParams = switch chainId {
839
- | Some(chainId) if chainIdCondition !== "" => [chainId->(Utils.magic: ChainId.t => unknown)]
840
- | _ => []
841
- }
842
924
  // A JSON array of the serialized ids. For a single id the query binds it as
843
925
  // `$1` directly (the array is the positional-params array); for many it binds
844
- // the whole array to `$1` behind an `ANY(...)`. The chain id, when the
845
- // condition needs it, rides along as $2.
926
+ // the whole array to `$1` behind an `ANY(...)`.
846
927
  let idsJson = table->Table.encodeIdsToJson(ids)
847
928
  switch await (
848
929
  switch ids {
849
930
  | [_] =>
850
931
  sql->Postgres.preparedUnsafe(
851
932
  makeDeleteByIdQuery(~pgSchema, ~tableName=table.tableName, ~chainIdCondition),
852
- idsJson
853
- ->(Utils.magic: JSON.t => array<unknown>)
854
- ->Array.concat(chainIdParams)
855
- ->Obj.magic,
933
+ idsJson->(Utils.magic: JSON.t => array<unknown>)->Obj.magic,
856
934
  )
857
935
  | _ =>
858
936
  sql->Postgres.preparedUnsafe(
@@ -862,7 +940,7 @@ let deleteByIdsOrThrow = async (
862
940
  ~idPgType=table->Table.getIdPgFieldType(~pgSchema),
863
941
  ~chainIdCondition,
864
942
  ),
865
- [idsJson->(Utils.magic: JSON.t => unknown)]->Array.concat(chainIdParams)->Obj.magic,
943
+ [idsJson->(Utils.magic: JSON.t => unknown)]->Obj.magic,
866
944
  )
867
945
  }
868
946
  ) {
@@ -961,6 +1039,7 @@ let rec writeBatch = async (
961
1039
  ~setQueryCache,
962
1040
  ~updatedEffectsCache,
963
1041
  ~updatedEntities: array<Persistence.updatedEntity>,
1042
+ ~registeredAddresses: array<AddressRows.staged>,
964
1043
  ~sinkPromise: option<promise<option<exn>>>,
965
1044
  ~chainMetaData: option<dict<InternalTable.Chains.metaFields>>,
966
1045
  ~escapeTables=?,
@@ -1217,7 +1296,7 @@ let rec writeBatch = async (
1217
1296
  //valid event identifier, where all rows created after this eventIdentifier should
1218
1297
  //be deleted
1219
1298
  let rollbackTables = switch rollback {
1220
- | Some({targetCheckpointId: rollbackTargetCheckpointId}) =>
1299
+ | Some({targetCheckpointId: rollbackTargetCheckpointId, rolledBackAddresses}) =>
1221
1300
  Some(
1222
1301
  sql => {
1223
1302
  // Postgres owns history tables only for Postgres-backed entities;
@@ -1238,6 +1317,22 @@ let rec writeBatch = async (
1238
1317
  sql->InternalTable.Checkpoints.rollback(~pgSchema, ~rollbackTargetCheckpointId),
1239
1318
  )
1240
1319
  ->ignore
1320
+
1321
+ // Addresses are insert-only, so undoing their registrations is a
1322
+ // delete rather than a history replay. It runs before the batch's own
1323
+ // inserts in the same transaction, so a re-registered address lands
1324
+ // after its old row is gone.
1325
+ if rolledBackAddresses->Utils.Array.notEmpty {
1326
+ promises
1327
+ ->Array.push(
1328
+ sql->InternalTable.EnvioAddresses.delete(
1329
+ ~pgSchema,
1330
+ ~keys=rolledBackAddresses,
1331
+ ~chainIdMode,
1332
+ ),
1333
+ )
1334
+ ->ignore
1335
+ }
1241
1336
  Promise.all(promises)
1242
1337
  },
1243
1338
  )
@@ -1280,6 +1375,16 @@ let rec writeBatch = async (
1280
1375
  | None => ()
1281
1376
  }
1282
1377
 
1378
+ if registeredAddresses->Utils.Array.notEmpty {
1379
+ setOperations->Array.push(sql =>
1380
+ sql->InternalTable.EnvioAddresses.insert(
1381
+ ~pgSchema,
1382
+ ~rows=registeredAddresses->Array.map(staged => staged.row),
1383
+ ~chainIdMode,
1384
+ )
1385
+ )
1386
+ }
1387
+
1283
1388
  if shouldSaveHistory {
1284
1389
  setOperations->Array.push(sql =>
1285
1390
  sql->InternalTable.Checkpoints.insert(
@@ -1354,6 +1459,7 @@ let rec writeBatch = async (
1354
1459
  ~updatedEffectsCache,
1355
1460
  ~allEntities,
1356
1461
  ~updatedEntities,
1462
+ ~registeredAddresses,
1357
1463
  ~sinkPromise,
1358
1464
  ~chainMetaData,
1359
1465
  )
@@ -1469,6 +1575,9 @@ let make = (
1469
1575
  ~pgPassword,
1470
1576
  ~isHasuraEnabled,
1471
1577
  ~chainIdMode: ChainId.mode=Int32,
1578
+ // Decides how wide an address key is, both when the config's addresses are
1579
+ // encoded at initialize and when stored rows are grouped on resume.
1580
+ ~ecosystem: Ecosystem.name,
1472
1581
  ~sink: option<Sink.t>=?,
1473
1582
  ~onInitialize=?,
1474
1583
  ~onNewTables=?,
@@ -1662,6 +1771,7 @@ let make = (
1662
1771
  ~chainConfigs=[],
1663
1772
  ~entities=[],
1664
1773
  ~enums=[],
1774
+ ~contractMapping,
1665
1775
  ~envioInfo,
1666
1776
  ): Persistence.initialState => {
1667
1777
  // Per-entity storage routing: PG owns tables only for entities that
@@ -1715,43 +1825,34 @@ let make = (
1715
1825
  // Execute all queries within a single transaction for integrity.
1716
1826
  // The envio_info row is written in the same transaction so a successful
1717
1827
  // initialize is atomic — no schema can come up without the matching row.
1828
+ let rowsByChain =
1829
+ chainConfigs->Array.map(chainConfig =>
1830
+ chainConfig->ChainState.configStorageRows(~ecosystem, ~contractMapping)
1831
+ )
1832
+ let configAddressRows = rowsByChain->Array.flat
1833
+
1834
+ // The contract mapping and the config's addresses join the schema in the
1835
+ // same transaction as envio_info: a schema that comes up without them would
1836
+ // resume against ids nothing assigned.
1718
1837
  let _ = await sql->Postgres.beginSql(async sql => {
1719
1838
  // Promise.all might be not safe to use here,
1720
1839
  // but it's just how it worked before.
1721
1840
  let _ = await Promise.all(queries->Array.map(query => sql->Postgres.unsafe(query)))
1722
1841
  await InternalTable.EnvioInfo.write(sql, ~pgSchema, ~envioInfo)
1723
- })
1724
-
1725
- // Populate config addresses into envio_addresses with registration_block/log = -1
1726
- let ids = []
1727
- let addrChainIds = []
1728
- let addrContractNames = []
1729
- chainConfigs->Array.forEach(chain => {
1730
- chain.contracts->Array.forEach(contract => {
1731
- contract.addresses->Array.forEach(
1732
- address => {
1733
- ids->Array.push(Config.EnvioAddresses.makeId(~chainId=chain.id, ~address))->ignore
1734
- addrChainIds->Array.push(chain.id)->ignore
1735
- addrContractNames->Array.push(contract.name)->ignore
1736
- },
1737
- )
1738
- })
1739
- })
1740
- if ids->Array.length > 0 {
1741
- let addrChainIdArrayType = Table.getPgFieldType(
1742
- ~fieldType=ChainId,
1842
+ await InternalTable.EnvioContracts.insert(
1843
+ sql,
1743
1844
  ~pgSchema,
1744
- ~isArray=true,
1745
- ~isNumericArrayAsText=false,
1746
- ~isNullable=false,
1747
- ~chainIdMode,
1845
+ ~contractNames=contractMapping->ContractMapping.names,
1748
1846
  )
1749
- await sql->Postgres.unpreparedUnsafe(
1750
- `INSERT INTO "${pgSchema}"."${Config.EnvioAddresses.table.tableName}" ("id", "chain_id", "registration_block", "registration_log_index", "contract_name")
1751
- SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::${addrChainIdArrayType},$3::text[]) AS t(id, chain_id, contract_name);`,
1752
- (ids, addrChainIds, addrContractNames)->(Utils.magic: _ => unknown),
1753
- )
1754
- }
1847
+ if configAddressRows->Utils.Array.notEmpty {
1848
+ await InternalTable.EnvioAddresses.insert(
1849
+ sql,
1850
+ ~pgSchema,
1851
+ ~rows=configAddressRows,
1852
+ ~chainIdMode,
1853
+ )
1854
+ }
1855
+ })
1755
1856
 
1756
1857
  let cache = await restoreEffectCache(~withUpload=true)
1757
1858
 
@@ -1767,10 +1868,12 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::${addrChai
1767
1868
  cleanRun: true,
1768
1869
  cache,
1769
1870
  reorgCheckpoints: [],
1770
- // Just-written row; resume's compat check would no-op on a clean run,
1771
- // but keep the field consistent with the resume path's shape.
1871
+ contractMapping,
1772
1872
  envioInfo: Some(envioInfo),
1773
- chains: chainConfigs->Array.map((chainConfig): Persistence.initialChainState => {
1873
+ chains: chainConfigs->Array.mapWithIndex((
1874
+ chainConfig,
1875
+ idx,
1876
+ ): Persistence.initialChainState => {
1774
1877
  id: chainConfig.id,
1775
1878
  startBlock: chainConfig.startBlock,
1776
1879
  endBlock: chainConfig.endBlock,
@@ -1779,7 +1882,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::${addrChai
1779
1882
  numEventsProcessed: 0.,
1780
1883
  firstEventBlockNumber: None,
1781
1884
  timestampCaughtUpToHeadOrEndblock: None,
1782
- indexingAddresses: ChainState.configAddresses(chainConfig),
1885
+ addressRows: rowsByChain->Array.getUnsafe(idx)->AddressRows.seedRowsOf,
1783
1886
  sourceBlockNumber: 0,
1784
1887
  }),
1785
1888
  checkpointId: InternalTable.Checkpoints.initialCheckpointId,
@@ -2169,7 +2272,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::${addrChai
2169
2272
  }
2170
2273
 
2171
2274
  let resumeInitialState = async (): Persistence.initialState => {
2172
- let (cache, chains, checkpointIdResult, reorgCheckpoints, envioInfo) = await Promise.all5((
2275
+ let (cache, chains, checkpointIdResult, reorgCheckpoints, (envioInfo, contractMapping)) = await Promise.all5((
2173
2276
  restoreEffectCache(~withUpload=false),
2174
2277
  InternalTable.Chains.getInitialState(
2175
2278
  sql,
@@ -2184,7 +2287,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::${addrChai
2184
2287
  timestampCaughtUpToHeadOrEndblock: rawInitialState.timestampCaughtUpToHeadOrEndblock->Null.toOption,
2185
2288
  numEventsProcessed: rawInitialState.numEventsProcessed,
2186
2289
  progressBlockNumber: rawInitialState.progressBlockNumber,
2187
- indexingAddresses: rawInitialState.indexingAddresses,
2290
+ addressRows: rawInitialState.addressRows,
2188
2291
  sourceBlockNumber: rawInitialState.sourceBlockNumber,
2189
2292
  })
2190
2293
  }),
@@ -2203,7 +2306,18 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::${addrChai
2203
2306
  }>,
2204
2307
  >
2205
2308
  ),
2206
- InternalTable.EnvioInfo.read(sql, ~pgSchema),
2309
+ Promise.all2((
2310
+ InternalTable.EnvioInfo.read(sql, ~pgSchema),
2311
+ InternalTable.EnvioContracts.read(sql, ~pgSchema),
2312
+ ))->Promise.thenResolve(((info, names)) =>
2313
+ // Both tables join the schema in one transaction. A missing mapping
2314
+ // means an older envio wrote this schema, so treat the snapshot as
2315
+ // unreadable rather than decoding address rows against ids nothing assigned.
2316
+ switch (info, names) {
2317
+ | (Some(info), Some(names)) => (Some(info), ContractMapping.fromStoredNames(names))
2318
+ | _ => (None, ContractMapping.empty)
2319
+ }
2320
+ ),
2207
2321
  ))
2208
2322
 
2209
2323
  await reloadIndexCatalog()
@@ -2230,6 +2344,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::${addrChai
2230
2344
  cache,
2231
2345
  chains,
2232
2346
  checkpointId,
2347
+ contractMapping,
2233
2348
  envioInfo,
2234
2349
  }
2235
2350
  }
@@ -2308,7 +2423,12 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::${addrChai
2308
2423
  }
2309
2424
  })
2310
2425
 
2311
- (removals, restoredEntitiesResult)
2426
+ (
2427
+ removals,
2428
+ restoredEntitiesResult
2429
+ ->S.parseOrThrow(entityConfig.table->Table.pgRowsSchema)
2430
+ ->(Utils.magic: array<unknown> => array<Internal.entity>),
2431
+ )
2312
2432
  }
2313
2433
 
2314
2434
  let writeBatchMethod = async (
@@ -2319,6 +2439,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::${addrChai
2319
2439
  ~allEntities,
2320
2440
  ~updatedEffectsCache,
2321
2441
  ~updatedEntities,
2442
+ ~registeredAddresses,
2322
2443
  ~chainMetaData,
2323
2444
  ~onWrite,
2324
2445
  ) => {
@@ -2365,6 +2486,7 @@ SELECT id, chain_id, -1, -1, contract_name FROM unnest($1::text[],$2::${addrChai
2365
2486
  ~setEffectCacheOrThrow,
2366
2487
  ~updatedEffectsCache,
2367
2488
  ~updatedEntities=pgUpdates,
2489
+ ~registeredAddresses,
2368
2490
  ~sinkPromise,
2369
2491
  ~chainMetaData,
2370
2492
  )
@@ -2410,6 +2532,7 @@ let makeStorageFromEnv = (
2410
2532
  ~pgDatabase=Env.Db.database,
2411
2533
  ~pgPassword=Env.Db.password,
2412
2534
  ~chainIdMode=config.chainIdMode,
2535
+ ~ecosystem=config.ecosystem.name,
2413
2536
  ~sink=?{
2414
2537
  // Internally ClickHouse storage is implemented as a sync of the
2415
2538
  // Postgres storage. Required env vars are validated here only when
@@ -2462,7 +2585,7 @@ let makeStorageFromEnv = (
2462
2585
  ~pgSchema,
2463
2586
  ~userEntities=config->Config.getPgUserEntities,
2464
2587
  ~responseLimit=Env.Hasura.responseLimit,
2465
- ~schema=Schema.make(config.allEntities->Array.map(e => e.table)),
2588
+ ~schema=Schema.make(config.userEntities->Array.map(e => e.table)),
2466
2589
  ~aggregateEntities=Env.Hasura.aggregateEntities,
2467
2590
  )->Promise.catch(err => {
2468
2591
  Logging.errorWithExn(err->Utils.prettifyExn, `Error tracking tables`)->Promise.resolve