From 56696e48a5e7b31d6a95b5370a0334e316bb84e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 13:27:36 +0000 Subject: [PATCH 01/11] Rename dynamic_contract_registry to envio_addresses and simplify schema Remove registering_event_block_timestamp, registering_event_src_address, registering_event_name, registering_event_contract_name (derivable from block+log_index). Remove contract_address in favor of id. Rename registering_event_block_number to registering_event_block. Make registering_event_log_index nullable for future block handler support. Use composite primary key (id, chain_id). https://claude.ai/code/session_01XQDfh8ZyX5TSyVDPKbMzx2 --- packages/envio/src/Config.gen.ts | 13 +--- packages/envio/src/Config.res | 52 +++++++------- packages/envio/src/InMemoryStore.res | 17 ++--- packages/envio/src/Persistence.res | 72 +++++++++---------- packages/envio/src/TestIndexer.res | 35 +++++---- packages/envio/src/db/InternalTable.gen.ts | 11 +-- packages/envio/src/db/InternalTable.res | 15 ++-- .../test/integration-raw-events.test.ts | 4 +- .../test/lib_tests/Persistence_test.res | 2 +- .../test/lib_tests/PgStorage_test.res | 8 +-- .../test/rollback/Rollback_test.res | 28 +++----- 11 files changed, 113 insertions(+), 144 deletions(-) diff --git a/packages/envio/src/Config.gen.ts b/packages/envio/src/Config.gen.ts index b68badf25f..8c653977b6 100644 --- a/packages/envio/src/Config.gen.ts +++ b/packages/envio/src/Config.gen.ts @@ -3,17 +3,10 @@ /* eslint-disable */ /* tslint:disable */ -import type {t as Address_t} from './Address.gen.js'; - -export type DynamicContractRegistry_t = { +export type EnvioAddresses_t = { readonly id: string; readonly chain_id: number; - readonly registering_event_block_number: number; - readonly registering_event_log_index: number; - readonly registering_event_block_timestamp: number; - readonly registering_event_contract_name: string; - readonly registering_event_name: string; - readonly registering_event_src_address: Address_t; - readonly contract_address: Address_t; + readonly registering_event_block: number; + readonly registering_event_log_index: (undefined | number); readonly contract_name: string }; diff --git a/packages/envio/src/Config.res b/packages/envio/src/Config.res index abf417536d..c0f8647da8 100644 --- a/packages/envio/src/Config.res +++ b/packages/envio/src/Config.res @@ -89,38 +89,36 @@ type t = { allEnums: array>, } -module DynamicContractRegistry = { - let name = "dynamic_contract_registry" +module EnvioAddresses = { + let name = "envio_addresses" let index = -1 - let makeId = (~chainId, ~contractAddress) => { - chainId->Belt.Int.toString ++ "-" ++ contractAddress->Address.toString + let makeId = (~chainId, ~address) => { + chainId->Belt.Int.toString ++ "-" ++ address->Address.toString } @genType type t = { id: string, @as("chain_id") chainId: int, - @as("registering_event_block_number") registeringEventBlockNumber: int, - @as("registering_event_log_index") registeringEventLogIndex: int, - @as("registering_event_block_timestamp") registeringEventBlockTimestamp: int, - @as("registering_event_contract_name") registeringEventContractName: string, - @as("registering_event_name") registeringEventName: string, - @as("registering_event_src_address") registeringEventSrcAddress: Address.t, - @as("contract_address") contractAddress: Address.t, + @as("registering_event_block") registeringEventBlock: int, + @as("registering_event_log_index") registeringEventLogIndex: option, @as("contract_name") contractName: string, } + // Extract address from composite id ({chainId}-{address}) + let getAddress = (entity: t): Address.t => { + let chainIdStr = entity.chainId->Belt.Int.toString + entity.id + ->Js.String2.sliceToEnd(~from=Js.String2.length(chainIdStr) + 1) + ->Address.unsafeFromString + } + let schema = S.schema(s => { id: s.matches(S.string), chainId: s.matches(S.int), - registeringEventBlockNumber: s.matches(S.int), - registeringEventLogIndex: s.matches(S.int), - registeringEventContractName: s.matches(S.string), - registeringEventName: s.matches(S.string), - registeringEventSrcAddress: s.matches(Address.schema), - registeringEventBlockTimestamp: s.matches(S.int), - contractAddress: s.matches(Address.schema), + registeringEventBlock: s.matches(S.int), + registeringEventLogIndex: s.matches(S.null(S.int)), contractName: s.matches(S.string), }) @@ -130,14 +128,14 @@ module DynamicContractRegistry = { name, ~fields=[ Table.mkField("id", String, ~isPrimaryKey=true, ~fieldSchema=S.string), - Table.mkField("chain_id", Int32, ~fieldSchema=S.int), - Table.mkField("registering_event_block_number", Int32, ~fieldSchema=S.int), - Table.mkField("registering_event_log_index", Int32, ~fieldSchema=S.int), - Table.mkField("registering_event_block_timestamp", Int32, ~fieldSchema=S.int), - Table.mkField("registering_event_contract_name", String, ~fieldSchema=S.string), - Table.mkField("registering_event_name", String, ~fieldSchema=S.string), - Table.mkField("registering_event_src_address", String, ~fieldSchema=Address.schema), - Table.mkField("contract_address", String, ~fieldSchema=Address.schema), + Table.mkField("chain_id", Int32, ~isPrimaryKey=true, ~fieldSchema=S.int), + Table.mkField("registering_event_block", Int32, ~fieldSchema=S.int), + Table.mkField( + "registering_event_log_index", + Int32, + ~isNullable=true, + ~fieldSchema=S.null(S.int), + ), Table.mkField("contract_name", String, ~fieldSchema=S.string), ], ) @@ -745,7 +743,7 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => { ->Option.getWithDefault([]) ->parseEntitiesFromJson(~enumConfigsByName) - let allEntities = userEntities->Js.Array2.concat([DynamicContractRegistry.entityConfig]) + let allEntities = userEntities->Js.Array2.concat([EnvioAddresses.entityConfig]) let userEntitiesByName = userEntities diff --git a/packages/envio/src/InMemoryStore.res b/packages/envio/src/InMemoryStore.res index aaab6ae05d..8d5d0d68ae 100644 --- a/packages/envio/src/InMemoryStore.res +++ b/packages/envio/src/InMemoryStore.res @@ -100,7 +100,7 @@ let isRollingBack = (inMemoryStore: t) => inMemoryStore.rollbackTargetCheckpoint let setBatchDcs = (inMemoryStore: t, ~batch: Batch.t, ~shouldSaveHistory) => { let inMemTable = - inMemoryStore->getInMemTable(~entityConfig=InternalTable.DynamicContractRegistry.entityConfig) + inMemoryStore->getInMemTable(~entityConfig=InternalTable.EnvioAddresses.entityConfig) let itemIdx = ref(0) @@ -119,24 +119,19 @@ let setBatchDcs = (inMemoryStore: t, ~batch: Batch.t, ~shouldSaveHistory) => { let eventItem = item->Internal.castUnsafeEventItem for dcIdx in 0 to dcs->Array.length - 1 { let dc = dcs->Js.Array2.unsafe_get(dcIdx) - let entity: InternalTable.DynamicContractRegistry.t = { - id: InternalTable.DynamicContractRegistry.makeId(~chainId, ~contractAddress=dc.address), + let entity: InternalTable.EnvioAddresses.t = { + id: InternalTable.EnvioAddresses.makeId(~chainId, ~address=dc.address), chainId, - contractAddress: dc.address, contractName: dc.contractName, - registeringEventBlockNumber: eventItem.blockNumber, - registeringEventLogIndex: eventItem.logIndex, - registeringEventBlockTimestamp: eventItem.timestamp, - registeringEventContractName: eventItem.eventConfig.contractName, - registeringEventName: eventItem.eventConfig.name, - registeringEventSrcAddress: eventItem.event.srcAddress, + registeringEventBlock: eventItem.blockNumber, + registeringEventLogIndex: Some(eventItem.logIndex), } inMemTable->InMemoryTable.Entity.set( Set({ entityId: entity.id, checkpointId, - entity: entity->InternalTable.DynamicContractRegistry.castToInternal, + entity: entity->InternalTable.EnvioAddresses.castToInternal, }), ~shouldSaveHistory, ) diff --git a/packages/envio/src/Persistence.res b/packages/envio/src/Persistence.res index 2738bb9eb7..ef388cfaf0 100644 --- a/packages/envio/src/Persistence.res +++ b/packages/envio/src/Persistence.res @@ -142,8 +142,7 @@ let make = ( ~allEnums, ~storage, ) => { - let allEntities = - userEntities->Js.Array2.concat([InternalTable.DynamicContractRegistry.entityConfig]) + let allEntities = userEntities->Js.Array2.concat([InternalTable.EnvioAddresses.entityConfig]) let allEnums = allEnums->Js.Array2.concat([EntityHistory.RowAction.config->Table.fromGenericEnumConfig]) { @@ -317,46 +316,45 @@ let prepareRollbackDiff = async ( let deletedEntities = Js.Dict.empty() let setEntities = Js.Dict.empty() - let _ = - await persistence.allEntities - ->Belt.Array.map(async entityConfig => { - let entityTable = inMemStore->InMemoryStore.getInMemTable(~entityConfig) + let _ = await persistence.allEntities + ->Belt.Array.map(async entityConfig => { + let entityTable = inMemStore->InMemoryStore.getInMemTable(~entityConfig) - let (removedIdsResult, restoredEntitiesResult) = await persistence.storage.getRollbackData( - ~entityConfig, - ~rollbackTargetCheckpointId, - ) + let (removedIdsResult, restoredEntitiesResult) = await persistence.storage.getRollbackData( + ~entityConfig, + ~rollbackTargetCheckpointId, + ) - // Process removed IDs - removedIdsResult->Js.Array2.forEach(data => { - deletedEntities->Utils.Dict.push(entityConfig.name, data["id"]) - entityTable->InMemoryTable.Entity.set( - Delete({ - entityId: data["id"], - checkpointId: rollbackDiffCheckpointId, - }), - ~shouldSaveHistory=false, - ~containsRollbackDiffChange=true, - ) - }) + // Process removed IDs + removedIdsResult->Js.Array2.forEach(data => { + deletedEntities->Utils.Dict.push(entityConfig.name, data["id"]) + entityTable->InMemoryTable.Entity.set( + Delete({ + entityId: data["id"], + checkpointId: rollbackDiffCheckpointId, + }), + ~shouldSaveHistory=false, + ~containsRollbackDiffChange=true, + ) + }) - let restoredEntities = restoredEntitiesResult->S.parseOrThrow(entityConfig.rowsSchema) + let restoredEntities = restoredEntitiesResult->S.parseOrThrow(entityConfig.rowsSchema) - // Process restored entities - restoredEntities->Belt.Array.forEach((entity: Internal.entity) => { - setEntities->Utils.Dict.push(entityConfig.name, entity.id) - entityTable->InMemoryTable.Entity.set( - Set({ - entityId: entity.id, - checkpointId: rollbackDiffCheckpointId, - entity, - }), - ~shouldSaveHistory=false, - ~containsRollbackDiffChange=true, - ) - }) + // Process restored entities + restoredEntities->Belt.Array.forEach((entity: Internal.entity) => { + setEntities->Utils.Dict.push(entityConfig.name, entity.id) + entityTable->InMemoryTable.Entity.set( + Set({ + entityId: entity.id, + checkpointId: rollbackDiffCheckpointId, + entity, + }), + ~shouldSaveHistory=false, + ~containsRollbackDiffChange=true, + ) }) - ->Promise.all + }) + ->Promise.all { "inMemStore": inMemStore, diff --git a/packages/envio/src/TestIndexer.res b/packages/envio/src/TestIndexer.res index 6199f3ffcf..b87311d4e9 100644 --- a/packages/envio/src/TestIndexer.res +++ b/packages/envio/src/TestIndexer.res @@ -36,18 +36,15 @@ type testIndexerState = { mutable processChanges: array, } -// Cast Internal.entity back to DynamicContractRegistry.t -external castFromDcRegistry: Internal.entity => InternalTable.DynamicContractRegistry.t = - "%identity" - -// Convert DynamicContractRegistry.t to Internal.indexingContract -let toIndexingContract = ( - dc: InternalTable.DynamicContractRegistry.t, -): Internal.indexingContract => { - address: dc.contractAddress, +// Cast Internal.entity back to EnvioAddresses.t +external castFromDcRegistry: Internal.entity => InternalTable.EnvioAddresses.t = "%identity" + +// Convert EnvioAddresses.t to Internal.indexingContract +let toIndexingContract = (dc: InternalTable.EnvioAddresses.t): Internal.indexingContract => { + address: dc->Config.EnvioAddresses.getAddress, contractName: dc.contractName, - startBlock: dc.registeringEventBlockNumber, - registrationBlock: Some(dc.registeringEventBlockNumber), + startBlock: dc.registeringEventBlock, + registrationBlock: Some(dc.registeringEventBlock), } let handleLoadByIds = ( @@ -234,14 +231,14 @@ let handleWriteBatch = ( entityChanges ->Js.Dict.entries ->Array.forEach(((entityName, {sets, deleted})) => { - // Transform dynamic_contract_registry to addresses with simplified structure - if entityName === InternalTable.DynamicContractRegistry.name { + // Transform envio_addresses to addresses with simplified structure + if entityName === InternalTable.EnvioAddresses.name { let entityObj: dict = Js.Dict.empty() if sets->Array.length > 0 { // Transform sets to simplified {address, contract} objects let simplifiedSets = sets->Array.map(entity => { let dc = entity->Utils.magic->castFromDcRegistry - {"address": dc.contractAddress, "contract": dc.contractName} + {"address": dc->Config.EnvioAddresses.getAddress, "contract": dc.contractName} }) entityObj->Js.Dict.set("sets", simplifiedSets->Utils.magic) } @@ -532,8 +529,8 @@ let makeCreateTestIndexer = ( // Build entity operations for each user entity let entityOpsDict: Js.Dict.t = Js.Dict.empty() allEntities->Array.forEach(entityConfig => { - // Only create ops for user entities (not internal tables like dynamic_contract_registry) - if entityConfig.name !== InternalTable.DynamicContractRegistry.name { + // Only create ops for user entities (not internal tables like envio_addresses) + if entityConfig.name !== InternalTable.EnvioAddresses.name { entityOpsDict->Js.Dict.set( entityConfig.name, { @@ -589,7 +586,7 @@ let makeCreateTestIndexer = ( // Start with static config addresses let addresses = contract.addresses->Array.copy // Add accumulated dynamic contract addresses - switch state.entities->Js.Dict.get(InternalTable.DynamicContractRegistry.name) { + switch state.entities->Js.Dict.get(InternalTable.EnvioAddresses.name) { | Some(dcDict) => dcDict ->Js.Dict.values @@ -597,7 +594,7 @@ let makeCreateTestIndexer = ( entity => { let dc = entity->castFromDcRegistry if dc.contractName === contract.name && dc.chainId === chainConfig.id { - addresses->Array.push(dc.contractAddress)->ignore + addresses->Array.push(dc->Config.EnvioAddresses.getAddress)->ignore } }, ) @@ -697,7 +694,7 @@ let makeCreateTestIndexer = ( // Extract dynamic contracts from state.entities for each chain let dynamicContractsByChain: dict> = Js.Dict.empty() - switch state.entities->Js.Dict.get(InternalTable.DynamicContractRegistry.name) { + switch state.entities->Js.Dict.get(InternalTable.EnvioAddresses.name) { | Some(dcDict) => dcDict ->Js.Dict.values diff --git a/packages/envio/src/db/InternalTable.gen.ts b/packages/envio/src/db/InternalTable.gen.ts index c3108190ca..9a74e6ca99 100644 --- a/packages/envio/src/db/InternalTable.gen.ts +++ b/packages/envio/src/db/InternalTable.gen.ts @@ -7,16 +7,11 @@ import type {Json_t as Js_Json_t} from '../../src/Js.shim.js'; import type {t as Address_t} from '../../src/Address.gen.js'; -export type DynamicContractRegistry_t = { +export type EnvioAddresses_t = { readonly id: string; readonly chain_id: number; - readonly registering_event_block_number: number; - readonly registering_event_log_index: number; - readonly registering_event_block_timestamp: number; - readonly registering_event_contract_name: string; - readonly registering_event_name: string; - readonly registering_event_src_address: Address_t; - readonly contract_address: Address_t; + readonly registering_event_block: number; + readonly registering_event_log_index: (undefined | number); readonly contract_name: string }; diff --git a/packages/envio/src/db/InternalTable.res b/packages/envio/src/db/InternalTable.res index d1a2234063..bc0c4de717 100644 --- a/packages/envio/src/db/InternalTable.res +++ b/packages/envio/src/db/InternalTable.res @@ -5,7 +5,7 @@ let isPrimaryKey = true let isNullable = true let isIndex = true -module DynamicContractRegistry = Config.DynamicContractRegistry +module EnvioAddresses = Config.EnvioAddresses module Chains = { type progressFields = [ @@ -188,12 +188,12 @@ WHERE "${(#id: field :> string)}" = $1;` "${(#source_block: field :> string)}" as "sourceBlockNumber", ( SELECT COALESCE(json_agg(json_build_object( - 'address', "contract_address", + 'address', SUBSTRING("id" FROM POSITION('-' IN "id") + 1), 'contractName', "contract_name", - 'startBlock', "registering_event_block_number", - 'registrationBlock', "registering_event_block_number" + 'startBlock', "registering_event_block", + 'registrationBlock', "registering_event_block" )), '[]'::json) - FROM "${pgSchema}"."${DynamicContractRegistry.table.tableName}" + FROM "${pgSchema}"."${EnvioAddresses.table.tableName}" WHERE "chain_id" = chains."${(#id: field :> string)}" ) as "dynamicContracts" FROM "${pgSchema}"."${table.tableName}" as chains;` @@ -330,7 +330,10 @@ module Checkpoints = { blockNumber: s.field("block_number", S.int), blockHash: s.field( "block_hash", - S.union([S.string->(Utils.magic: S.t => S.t>), S.literal(%raw(`null`))]), + S.union([ + S.string->(Utils.magic: S.t => S.t>), + S.literal(%raw(`null`)), + ]), ), eventsProcessed: s.field("events_processed", S.int), }) diff --git a/scenarios/test_codegen/test/integration-raw-events.test.ts b/scenarios/test_codegen/test/integration-raw-events.test.ts index 6b51d49776..3842db0c90 100644 --- a/scenarios/test_codegen/test/integration-raw-events.test.ts +++ b/scenarios/test_codegen/test/integration-raw-events.test.ts @@ -108,8 +108,8 @@ // expect(rowsToken.count).to.be.gt(0); // }); -// it("should have 1 row in the dynamic_contract_registry table", async function () { -// let rowsDCR = await sql`SELECT * FROM public.dynamic_contract_registry`; +// it("should have 1 row in the envio_addresses table", async function () { +// let rowsDCR = await sql`SELECT * FROM public.envio_addresses`; // console.log(rowsDCR); // expect(rowsDCR.count).to.be.eq(1); // }); diff --git a/scenarios/test_codegen/test/lib_tests/Persistence_test.res b/scenarios/test_codegen/test/lib_tests/Persistence_test.res index b6302e3972..7db9ac3624 100644 --- a/scenarios/test_codegen/test/lib_tests/Persistence_test.res +++ b/scenarios/test_codegen/test/lib_tests/Persistence_test.res @@ -9,7 +9,7 @@ describe("Test Persistence layer init", () => { t.expect( persistence.allEntities, ~message=`All entities should automatically include the indexer core ones`, - ).toEqual([InternalTable.DynamicContractRegistry.entityConfig]) + ).toEqual([InternalTable.EnvioAddresses.entityConfig]) t.expect( persistence.allEnums, ~message=`All enums should automatically include the indexer core ones`, diff --git a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res index 9edf83a1c0..94c46f72d5 100644 --- a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res +++ b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res @@ -818,12 +818,12 @@ VALUES (1, 100, 200, 5, 0, NULL, -1, -1, NULL, 0, false), "source_block" as "sourceBlockNumber", ( SELECT COALESCE(json_agg(json_build_object( - 'address', "contract_address", + 'address', SUBSTRING("id" FROM POSITION('-' IN "id") + 1), 'contractName', "contract_name", - 'startBlock', "registering_event_block_number", - 'registrationBlock', "registering_event_block_number" + 'startBlock', "registering_event_block", + 'registrationBlock', "registering_event_block" )), '[]'::json) - FROM "test_schema"."dynamic_contract_registry" + FROM "test_schema"."envio_addresses" WHERE "chain_id" = chains."id" ) as "dynamicContracts" FROM "test_schema"."envio_chains" as chains;` diff --git a/scenarios/test_codegen/test/rollback/Rollback_test.res b/scenarios/test_codegen/test/rollback/Rollback_test.res index 966e7ebc60..b1920df516 100644 --- a/scenarios/test_codegen/test/rollback/Rollback_test.res +++ b/scenarios/test_codegen/test/rollback/Rollback_test.res @@ -692,7 +692,7 @@ describe("E2E rollback tests", () => { ), ) t.expect( - await (indexerMock.queryRaw(InternalTable.DynamicContractRegistry.entityConfig): promise>), + await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>), ~message="Shouldn't store dynamic contracts at this point", ).toEqual( [], @@ -733,20 +733,15 @@ describe("E2E rollback tests", () => { ), ) t.expect( - await (indexerMock.queryRaw(InternalTable.DynamicContractRegistry.entityConfig): promise>), + await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>), ~message="Added the processed dynamic contract to the db", ).toEqual( [ { id: `1337-${Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0)->Address.toString}`, chainId: 1337, - registeringEventBlockNumber: 102, - registeringEventLogIndex: 2, - registeringEventBlockTimestamp: 102, - registeringEventContractName: "MockContract", - registeringEventName: "MockEvent", - registeringEventSrcAddress: "0x0000000000000000000000000000000000000000"->Address.unsafeFromString, - contractAddress: Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0), + registeringEventBlock: 102, + registeringEventLogIndex: Some(2), contractName: "SimpleNft", }, ], @@ -755,7 +750,7 @@ describe("E2E rollback tests", () => { sourceMock.resolveGetItemsOrThrow([], ~resolveAt=#last, ~latestFetchedBlockNumber=103) await indexerMock.getBatchWritePromise() t.expect( - (await (indexerMock.queryRaw(InternalTable.DynamicContractRegistry.entityConfig): promise>))->Array.length, + (await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>))->Array.length, ~message="Should add the processed dynamic contracts to the db", ).toEqual( 2, @@ -818,7 +813,7 @@ describe("E2E rollback tests", () => { await Utils.delay(0) await Utils.delay(0) t.expect( - (await (indexerMock.queryRaw(InternalTable.DynamicContractRegistry.entityConfig): promise>))->Array.length, + (await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>))->Array.length, ~message=`Nothing won't be rollbacked at this point. Since we need to process an event for this (rollback db only on batch write). This might be wrong after we start exposing a block hash for progress block.`, ).toEqual( @@ -840,20 +835,15 @@ This might be wrong after we start exposing a block hash for progress block.`, await indexerMock.getBatchWritePromise() t.expect( - await (indexerMock.queryRaw(InternalTable.DynamicContractRegistry.entityConfig): promise>), + await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>), ~message="Should have only one dynamic contract in the db. The second one rollbacked from db, the third one rollbacked from fetch state", ).toEqual( [ { id: `1337-${Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0)->Address.toString}`, chainId: 1337, - registeringEventBlockNumber: 102, - registeringEventLogIndex: 2, - registeringEventBlockTimestamp: 102, - registeringEventContractName: "MockContract", - registeringEventName: "MockEvent", - registeringEventSrcAddress: "0x0000000000000000000000000000000000000000"->Address.unsafeFromString, - contractAddress: Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0), + registeringEventBlock: 102, + registeringEventLogIndex: Some(2), contractName: "SimpleNft", }, ], From 51cca5147673df4344f3af7fbec1354bd78b2c27 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 13:28:02 +0000 Subject: [PATCH 02/11] Update pnpm-lock.yaml https://claude.ai/code/session_01XQDfh8ZyX5TSyVDPKbMzx2 --- pnpm-lock.yaml | 269 ------------------------------------------------- 1 file changed, 269 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d32e38459d..fef77d56bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -226,39 +226,6 @@ importers: specifier: ./generated version: link:generated - scenarios/test_codegen/generated: - dependencies: - bignumber.js: - specifier: 9.3.1 - version: 9.3.1 - date-fns: - specifier: 3.3.1 - version: 3.3.1 - envio: - specifier: file:/home/user/hyperindex/packages/envio - version: file:packages/envio(react-dom@19.2.3(react@19.2.0))(react@19.2.0)(typescript@5.9.3) - js-sdsl: - specifier: 4.4.2 - version: 4.4.2 - pino: - specifier: 8.16.1 - version: 8.16.1 - prom-client: - specifier: 15.1.3 - version: 15.1.3 - react: - specifier: 19.2.0 - version: 19.2.0 - rescript: - specifier: 11.1.3 - version: 11.1.3 - rescript-schema: - specifier: 9.3.4 - version: 9.3.4(rescript@11.1.3) - viem: - specifier: 2.46.2 - version: 2.46.2(typescript@5.9.3) - packages: '@adraffy/ens-normalize@1.11.1': @@ -1075,10 +1042,6 @@ packages: zod: optional: true - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1188,9 +1151,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.9.14: resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} hasBin: true @@ -1229,9 +1189,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1482,11 +1439,6 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - envio@file:packages/envio: - resolution: {directory: packages/envio, type: directory} - engines: {node: '>=22.0.0'} - hasBin: true - environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -1557,17 +1509,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -1609,10 +1553,6 @@ packages: resolution: {integrity: sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA==} engines: {node: '>= 10.0.0'} - fast-redact@3.5.0: - resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} - engines: {node: '>=6'} - fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} @@ -1758,9 +1698,6 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} @@ -2039,9 +1976,6 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-sdsl@4.4.2: - resolution: {integrity: sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2286,9 +2220,6 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pino-abstract-transport@1.1.0: - resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==} - pino-abstract-transport@3.0.0: resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} @@ -2296,9 +2227,6 @@ packages: resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} hasBin: true - pino-std-serializers@6.2.2: - resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} - pino-std-serializers@7.1.0: resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} @@ -2306,10 +2234,6 @@ packages: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true - pino@8.16.1: - resolution: {integrity: sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==} - hasBin: true - pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -2330,16 +2254,9 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - process-warning@2.3.2: - resolution: {integrity: sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==} - process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} - process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - prom-client@15.1.3: resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} engines: {node: ^16 || ^18 || >=20} @@ -2400,18 +2317,10 @@ packages: peerDependencies: react: ^19.2.0 - react@19.2.0: - resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} - engines: {node: '>=0.10.0'} - react@19.2.3: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} - readable-stream@4.7.0: - resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -2564,9 +2473,6 @@ packages: resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} engines: {node: '>=20'} - sonic-boom@3.8.1: - resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==} - sonic-boom@4.2.0: resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} @@ -2626,9 +2532,6 @@ packages: resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} engines: {node: '>=20'} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2691,9 +2594,6 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - thread-stream@2.7.0: - resolution: {integrity: sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==} - thread-stream@4.0.0: resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} engines: {node: '>=20'} @@ -3594,11 +3494,6 @@ snapshots: '@pinojs/redact@0.4.0': {} - '@rescript/react@0.14.1(react-dom@19.2.3(react@19.2.0))(react@19.2.0)': - dependencies: - react: 19.2.0 - react-dom: 19.2.3(react@19.2.0) - '@rescript/react@0.14.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: react: 19.2.3 @@ -3825,10 +3720,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -3955,8 +3846,6 @@ snapshots: balanced-match@1.0.2: {} - base64-js@1.5.1: {} - baseline-browser-mapping@2.9.14: {} bignumber.js@9.3.1: {} @@ -4007,11 +3896,6 @@ snapshots: buffer-from@1.1.2: {} - buffer@6.0.3: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -4201,47 +4085,6 @@ snapshots: dependencies: once: 1.4.0 - envio@file:packages/envio(react-dom@19.2.3(react@19.2.0))(react@19.2.0)(typescript@5.9.3): - dependencies: - '@clickhouse/client': 1.17.0 - '@elastic/ecs-pino-format': 1.4.0 - '@envio-dev/hyperfuel-client': 1.2.2 - '@envio-dev/hypersync-client': 1.3.0 - '@fuel-ts/crypto': 0.96.1 - '@fuel-ts/errors': 0.96.1 - '@fuel-ts/hasher': 0.96.1 - '@fuel-ts/math': 0.96.1 - '@fuel-ts/utils': 0.96.1 - '@rescript/react': 0.14.1(react-dom@19.2.3(react@19.2.0))(react@19.2.0) - bignumber.js: 9.3.1 - date-fns: 3.3.1 - dotenv: 16.4.5 - eventsource: 4.1.0 - express: 4.19.2 - ink: 6.8.0(react@19.2.0) - ink-big-text: 2.0.0(ink@6.8.0(react@19.2.0))(react@19.2.0) - ink-spinner: 5.0.0(ink@6.8.0(react@19.2.0))(react@19.2.0) - pino: 10.3.1 - pino-pretty: 13.1.3 - postgres: 3.4.8 - prom-client: 15.1.3 - rescript: 11.1.3 - rescript-envsafe: 5.0.0(rescript-schema@9.3.4(rescript@11.1.3))(rescript@11.1.3) - rescript-schema: 9.3.4(rescript@11.1.3) - tsx: 4.21.0 - viem: 2.46.2(typescript@5.9.3) - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/react' - - bufferutil - - react - - react-devtools-core - - react-dom - - supports-color - - typescript - - utf-8-validate - - zod - environment@1.1.0: {} error-ex@1.3.4: @@ -4322,12 +4165,8 @@ snapshots: etag@1.8.1: {} - event-target-shim@5.0.1: {} - eventemitter3@5.0.1: {} - events@3.3.0: {} - eventsource-parser@3.0.6: {} eventsource@4.1.0: @@ -4406,8 +4245,6 @@ snapshots: rfdc: 1.4.1 string-similarity: 4.0.4 - fast-redact@3.5.0: {} - fast-safe-stringify@2.1.1: {} fb-watchman@2.0.2: @@ -4556,8 +4393,6 @@ snapshots: dependencies: safer-buffer: 2.1.2 - ieee754@1.2.1: {} - import-local@3.2.0: dependencies: pkg-dir: 4.2.0 @@ -4574,13 +4409,6 @@ snapshots: inherits@2.0.4: {} - ink-big-text@2.0.0(ink@6.8.0(react@19.2.0))(react@19.2.0): - dependencies: - cfonts: 3.3.1 - ink: 6.8.0(react@19.2.0) - prop-types: 15.8.1 - react: 19.2.0 - ink-big-text@2.0.0(ink@6.8.0(react@19.2.3))(react@19.2.3): dependencies: cfonts: 3.3.1 @@ -4588,50 +4416,12 @@ snapshots: prop-types: 15.8.1 react: 19.2.3 - ink-spinner@5.0.0(ink@6.8.0(react@19.2.0))(react@19.2.0): - dependencies: - cli-spinners: 2.9.2 - ink: 6.8.0(react@19.2.0) - react: 19.2.0 - ink-spinner@5.0.0(ink@6.8.0(react@19.2.3))(react@19.2.3): dependencies: cli-spinners: 2.9.2 ink: 6.8.0(react@19.2.3) react: 19.2.3 - ink@6.8.0(react@19.2.0): - dependencies: - '@alcalzone/ansi-tokenize': 0.2.5 - ansi-escapes: 7.3.0 - ansi-styles: 6.2.3 - auto-bind: 5.0.1 - chalk: 5.6.2 - cli-boxes: 3.0.0 - cli-cursor: 4.0.0 - cli-truncate: 5.1.1 - code-excerpt: 4.0.0 - es-toolkit: 1.44.0 - indent-string: 5.0.0 - is-in-ci: 2.0.0 - patch-console: 2.0.0 - react: 19.2.0 - react-reconciler: 0.33.0(react@19.2.0) - scheduler: 0.27.0 - signal-exit: 3.0.7 - slice-ansi: 8.0.0 - stack-utils: 2.0.6 - string-width: 8.2.0 - terminal-size: 4.0.1 - type-fest: 5.4.4 - widest-line: 6.0.0 - wrap-ansi: 9.0.2 - ws: 8.19.0 - yoga-layout: 3.2.1 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - ink@6.8.0(react@19.2.3): dependencies: '@alcalzone/ansi-tokenize': 0.2.5 @@ -5105,8 +4895,6 @@ snapshots: joycon@3.1.1: {} - js-sdsl@4.4.2: {} - js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -5321,11 +5109,6 @@ snapshots: picomatch@4.0.3: {} - pino-abstract-transport@1.1.0: - dependencies: - readable-stream: 4.7.0 - split2: 4.2.0 - pino-abstract-transport@3.0.0: dependencies: split2: 4.2.0 @@ -5346,8 +5129,6 @@ snapshots: sonic-boom: 4.2.0 strip-json-comments: 5.0.3 - pino-std-serializers@6.2.2: {} - pino-std-serializers@7.1.0: {} pino@10.3.1: @@ -5364,20 +5145,6 @@ snapshots: sonic-boom: 4.2.0 thread-stream: 4.0.0 - pino@8.16.1: - dependencies: - atomic-sleep: 1.0.0 - fast-redact: 3.5.0 - on-exit-leak-free: 2.1.2 - pino-abstract-transport: 1.1.0 - pino-std-serializers: 6.2.2 - process-warning: 2.3.2 - quick-format-unescaped: 4.0.4 - real-require: 0.2.0 - safe-stable-stringify: 2.5.0 - sonic-boom: 3.8.1 - thread-stream: 2.7.0 - pirates@4.0.7: {} pkg-dir@4.2.0: @@ -5398,12 +5165,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - process-warning@2.3.2: {} - process-warning@5.0.0: {} - process@0.11.10: {} - prom-client@15.1.3: dependencies: '@opentelemetry/api': 1.9.0 @@ -5453,11 +5216,6 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - react-dom@19.2.3(react@19.2.0): - dependencies: - react: 19.2.0 - scheduler: 0.27.0 - react-dom@19.2.3(react@19.2.3): dependencies: react: 19.2.3 @@ -5467,28 +5225,13 @@ snapshots: react-is@17.0.2: {} - react-reconciler@0.33.0(react@19.2.0): - dependencies: - react: 19.2.0 - scheduler: 0.27.0 - react-reconciler@0.33.0(react@19.2.3): dependencies: react: 19.2.3 scheduler: 0.27.0 - react@19.2.0: {} - react@19.2.3: {} - readable-stream@4.7.0: - dependencies: - abort-controller: 3.0.0 - buffer: 6.0.3 - events: 3.3.0 - process: 0.11.10 - string_decoder: 1.3.0 - real-require@0.2.0: {} require-directory@2.1.1: {} @@ -5663,10 +5406,6 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - sonic-boom@3.8.1: - dependencies: - atomic-sleep: 1.0.0 - sonic-boom@4.2.0: dependencies: atomic-sleep: 1.0.0 @@ -5720,10 +5459,6 @@ snapshots: get-east-asian-width: 1.5.0 strip-ansi: 7.1.2 - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -5776,10 +5511,6 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 - thread-stream@2.7.0: - dependencies: - real-require: 0.2.0 - thread-stream@4.0.0: dependencies: real-require: 0.2.0 From 9e13ca88f828e2409cc8bd1533ae79c3ecfa13d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 14:03:25 +0000 Subject: [PATCH 03/11] Make address id contain only the address, split in-memory store by chain Remove EnvioAddresses from the entity framework entirely. The id column now stores the raw address string. Addresses are persisted via direct SQL INSERT with ON CONFLICT DO NOTHING and rolled back via DELETE WHERE envio_checkpoint_id > target. A new envio_checkpoint_id column tracks when each address was registered. In-memory: InMemoryStore.addressesToWrite accumulates per-batch entries. TestIndexer.addressesByChain provides per-chain lookup (chainId -> address -> entry) to avoid id collision across chains. https://claude.ai/code/session_01XQDfh8ZyX5TSyVDPKbMzx2 --- packages/envio/src/Config.gen.ts | 3 +- packages/envio/src/Config.res | 37 +---- packages/envio/src/InMemoryStore.res | 24 ++-- packages/envio/src/Persistence.res | 73 +++++----- packages/envio/src/PgStorage.res | 70 +++++++++- packages/envio/src/TestIndexer.res | 129 +++++++++--------- .../envio/src/TestIndexerProxyStorage.res | 3 + packages/envio/src/db/InternalTable.gen.ts | 3 +- packages/envio/src/db/InternalTable.res | 2 +- scenarios/test_codegen/test/helpers/Mock.res | 12 ++ .../test/lib_tests/Persistence_test.res | 4 +- .../test/lib_tests/PgStorage_test.res | 5 +- .../test/rollback/Rollback_test.res | 36 +++-- 13 files changed, 226 insertions(+), 175 deletions(-) diff --git a/packages/envio/src/Config.gen.ts b/packages/envio/src/Config.gen.ts index 8c653977b6..64303e1e10 100644 --- a/packages/envio/src/Config.gen.ts +++ b/packages/envio/src/Config.gen.ts @@ -8,5 +8,6 @@ export type EnvioAddresses_t = { readonly chain_id: number; readonly registering_event_block: number; readonly registering_event_log_index: (undefined | number); - readonly contract_name: string + readonly contract_name: string; + readonly envio_checkpoint_id: bigint }; diff --git a/packages/envio/src/Config.res b/packages/envio/src/Config.res index c0f8647da8..3f1ea81a6a 100644 --- a/packages/envio/src/Config.res +++ b/packages/envio/src/Config.res @@ -91,11 +91,6 @@ type t = { module EnvioAddresses = { let name = "envio_addresses" - let index = -1 - - let makeId = (~chainId, ~address) => { - chainId->Belt.Int.toString ++ "-" ++ address->Address.toString - } @genType type t = { @@ -104,25 +99,10 @@ module EnvioAddresses = { @as("registering_event_block") registeringEventBlock: int, @as("registering_event_log_index") registeringEventLogIndex: option, @as("contract_name") contractName: string, + @as("envio_checkpoint_id") checkpointId: bigint, } - // Extract address from composite id ({chainId}-{address}) - let getAddress = (entity: t): Address.t => { - let chainIdStr = entity.chainId->Belt.Int.toString - entity.id - ->Js.String2.sliceToEnd(~from=Js.String2.length(chainIdStr) + 1) - ->Address.unsafeFromString - } - - let schema = S.schema(s => { - id: s.matches(S.string), - chainId: s.matches(S.int), - registeringEventBlock: s.matches(S.int), - registeringEventLogIndex: s.matches(S.null(S.int)), - contractName: s.matches(S.string), - }) - - let rowsSchema = S.array(schema) + let getAddress = (entity: t): Address.t => entity.id->Address.unsafeFromString let table = Table.mkTable( name, @@ -137,18 +117,9 @@ module EnvioAddresses = { ~fieldSchema=S.null(S.int), ), Table.mkField("contract_name", String, ~fieldSchema=S.string), + Table.mkField("envio_checkpoint_id", Table.UInt64, ~fieldSchema=S.bigint), ], ) - - external castToInternal: t => Internal.entity = "%identity" - - let entityConfig = { - name, - index, - schema, - rowsSchema, - table, - }->Internal.fromGenericEntityConfig } // Types for parsing source config from internal.config.json @@ -743,7 +714,7 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => { ->Option.getWithDefault([]) ->parseEntitiesFromJson(~enumConfigsByName) - let allEntities = userEntities->Js.Array2.concat([EnvioAddresses.entityConfig]) + let allEntities = userEntities let userEntitiesByName = userEntities diff --git a/packages/envio/src/InMemoryStore.res b/packages/envio/src/InMemoryStore.res index 8d5d0d68ae..e7093e1976 100644 --- a/packages/envio/src/InMemoryStore.res +++ b/packages/envio/src/InMemoryStore.res @@ -52,6 +52,8 @@ type t = { entities: dict>, effects: dict, rollbackTargetCheckpointId: option, + // Addresses accumulated during this batch, to be written to DB + addressesToWrite: array, } let make = (~entities: array, ~rollbackTargetCheckpointId=?): t => { @@ -59,6 +61,7 @@ let make = (~entities: array, ~rollbackTargetCheckpointId entities: EntityTables.make(entities), effects: Js.Dict.empty(), rollbackTargetCheckpointId, + addressesToWrite: [], } let clone = (self: t) => { @@ -71,6 +74,7 @@ let clone = (self: t) => { effect: table.effect, }, self.effects), rollbackTargetCheckpointId: self.rollbackTargetCheckpointId, + addressesToWrite: self.addressesToWrite->Array.copy, } let getEffectInMemTable = (inMemoryStore: t, ~effect: Internal.effect) => { @@ -98,10 +102,7 @@ let getInMemTable = ( let isRollingBack = (inMemoryStore: t) => inMemoryStore.rollbackTargetCheckpointId !== None -let setBatchDcs = (inMemoryStore: t, ~batch: Batch.t, ~shouldSaveHistory) => { - let inMemTable = - inMemoryStore->getInMemTable(~entityConfig=InternalTable.EnvioAddresses.entityConfig) - +let setBatchDcs = (inMemoryStore: t, ~batch: Batch.t, ~shouldSaveHistory as _) => { let itemIdx = ref(0) for checkpoint in 0 to batch.checkpointIds->Array.length - 1 { @@ -119,22 +120,15 @@ let setBatchDcs = (inMemoryStore: t, ~batch: Batch.t, ~shouldSaveHistory) => { let eventItem = item->Internal.castUnsafeEventItem for dcIdx in 0 to dcs->Array.length - 1 { let dc = dcs->Js.Array2.unsafe_get(dcIdx) - let entity: InternalTable.EnvioAddresses.t = { - id: InternalTable.EnvioAddresses.makeId(~chainId, ~address=dc.address), + let entry: Config.EnvioAddresses.t = { + id: dc.address->Address.toString, chainId, contractName: dc.contractName, registeringEventBlock: eventItem.blockNumber, registeringEventLogIndex: Some(eventItem.logIndex), + checkpointId, } - - inMemTable->InMemoryTable.Entity.set( - Set({ - entityId: entity.id, - checkpointId, - entity: entity->InternalTable.EnvioAddresses.castToInternal, - }), - ~shouldSaveHistory, - ) + inMemoryStore.addressesToWrite->Js.Array2.push(entry)->ignore } } } diff --git a/packages/envio/src/Persistence.res b/packages/envio/src/Persistence.res index ef388cfaf0..d4fc6689e6 100644 --- a/packages/envio/src/Persistence.res +++ b/packages/envio/src/Persistence.res @@ -118,6 +118,7 @@ type storage = { ~allEntities: array, ~updatedEffectsCache: array, ~updatedEntities: array, + ~addressesToWrite: array, ) => promise, } @@ -142,7 +143,7 @@ let make = ( ~allEnums, ~storage, ) => { - let allEntities = userEntities->Js.Array2.concat([InternalTable.EnvioAddresses.entityConfig]) + let allEntities = userEntities let allEnums = allEnums->Js.Array2.concat([EntityHistory.RowAction.config->Table.fromGenericEnumConfig]) { @@ -257,6 +258,7 @@ let writeBatch = ( ~config, ~allEntities=persistence.allEntities, ~updatedEntities, + ~addressesToWrite=inMemoryStore.addressesToWrite, ~updatedEffectsCache={ inMemoryStore.effects ->Js.Dict.keys @@ -316,45 +318,46 @@ let prepareRollbackDiff = async ( let deletedEntities = Js.Dict.empty() let setEntities = Js.Dict.empty() - let _ = await persistence.allEntities - ->Belt.Array.map(async entityConfig => { - let entityTable = inMemStore->InMemoryStore.getInMemTable(~entityConfig) + let _ = + await persistence.allEntities + ->Belt.Array.map(async entityConfig => { + let entityTable = inMemStore->InMemoryStore.getInMemTable(~entityConfig) - let (removedIdsResult, restoredEntitiesResult) = await persistence.storage.getRollbackData( - ~entityConfig, - ~rollbackTargetCheckpointId, - ) - - // Process removed IDs - removedIdsResult->Js.Array2.forEach(data => { - deletedEntities->Utils.Dict.push(entityConfig.name, data["id"]) - entityTable->InMemoryTable.Entity.set( - Delete({ - entityId: data["id"], - checkpointId: rollbackDiffCheckpointId, - }), - ~shouldSaveHistory=false, - ~containsRollbackDiffChange=true, + let (removedIdsResult, restoredEntitiesResult) = await persistence.storage.getRollbackData( + ~entityConfig, + ~rollbackTargetCheckpointId, ) - }) - let restoredEntities = restoredEntitiesResult->S.parseOrThrow(entityConfig.rowsSchema) + // Process removed IDs + removedIdsResult->Js.Array2.forEach(data => { + deletedEntities->Utils.Dict.push(entityConfig.name, data["id"]) + entityTable->InMemoryTable.Entity.set( + Delete({ + entityId: data["id"], + checkpointId: rollbackDiffCheckpointId, + }), + ~shouldSaveHistory=false, + ~containsRollbackDiffChange=true, + ) + }) - // Process restored entities - restoredEntities->Belt.Array.forEach((entity: Internal.entity) => { - setEntities->Utils.Dict.push(entityConfig.name, entity.id) - entityTable->InMemoryTable.Entity.set( - Set({ - entityId: entity.id, - checkpointId: rollbackDiffCheckpointId, - entity, - }), - ~shouldSaveHistory=false, - ~containsRollbackDiffChange=true, - ) + let restoredEntities = restoredEntitiesResult->S.parseOrThrow(entityConfig.rowsSchema) + + // Process restored entities + restoredEntities->Belt.Array.forEach((entity: Internal.entity) => { + setEntities->Utils.Dict.push(entityConfig.name, entity.id) + entityTable->InMemoryTable.Entity.set( + Set({ + entityId: entity.id, + checkpointId: rollbackDiffCheckpointId, + entity, + }), + ~shouldSaveHistory=false, + ~containsRollbackDiffChange=true, + ) + }) }) - }) - ->Promise.all + ->Promise.all { "inMemStore": inMemStore, diff --git a/packages/envio/src/PgStorage.res b/packages/envio/src/PgStorage.res index 5669fe5ccf..ff4b1bde13 100644 --- a/packages/envio/src/PgStorage.res +++ b/packages/envio/src/PgStorage.res @@ -194,6 +194,7 @@ let makeInitializeTransaction = ( InternalTable.PersistedState.table, InternalTable.Checkpoints.table, InternalTable.RawEvents.table, + InternalTable.EnvioAddresses.table, ] let allTables = generalTables->Array.copy @@ -512,9 +513,7 @@ let setOrThrow = async (sql, ~items, ~table: Table.table, ~itemSchema, ~pgSchema let chunkSize = chunk->Array.length let isFullChunk = chunkSize === maxItemsPerQuery - let params = data["convertOrThrow"]( - chunk->(Utils.magic: array<'item> => array), - ) + let params = data["convertOrThrow"](chunk->(Utils.magic: array<'item> => array)) // Use prepared query only for full batches where the cached query is reused. // Partial chunks generate unique SQL each time, so preparation has no benefit. let response = isFullChunk @@ -739,6 +738,7 @@ let rec writeBatch = async ( ~setEffectCacheOrThrow, ~updatedEffectsCache, ~updatedEntities: array, + ~addressesToWrite: array, ~sinkPromise: option>>, ~escapeTables=?, ) => { @@ -828,7 +828,10 @@ let rec writeBatch = async ( sql ->Postgres.preparedUnsafe( makeInsertDeleteUpdatesQuery(~entityConfig, ~pgSchema), - (batchDeleteEntityIds, batchDeleteCheckpointIds->BigInt.arrayToStringArray)->Obj.magic, + ( + batchDeleteEntityIds, + batchDeleteCheckpointIds->BigInt.arrayToStringArray, + )->Obj.magic, ) ->Promise.ignoreValue, ) @@ -945,6 +948,19 @@ let rec writeBatch = async ( sql->InternalTable.Checkpoints.rollback(~pgSchema, ~rollbackTargetCheckpointId), ) ->ignore + // Rollback addresses: delete entries created after the rollback target + promises + ->Js.Array2.push( + sql + ->Postgres.preparedUnsafe( + `DELETE FROM "${pgSchema}"."${InternalTable.EnvioAddresses.table.tableName}" WHERE "envio_checkpoint_id" > $1;`, + [rollbackTargetCheckpointId->BigInt.toString]->( + Utils.magic: array => unknown + ), + ) + ->Promise.ignoreValue, + ) + ->ignore Promise.all(promises) }, ) @@ -977,6 +993,42 @@ let rec writeBatch = async ( setRawEvents, ]->Belt.Array.concat(setEntities) + // Insert new addresses + if addressesToWrite->Array.length > 0 { + setOperations->Belt.Array.push(sql => { + sql + ->Postgres.preparedUnsafe( + `INSERT INTO "${pgSchema}"."${InternalTable.EnvioAddresses.table.tableName}" ("id", "chain_id", "registering_event_block", "registering_event_log_index", "contract_name", "envio_checkpoint_id") +SELECT * FROM UNNEST($1::text[], $2::${(Postgres.Integer :> string)}[], $3::${(Postgres.Integer :> string)}[], $4::${(Postgres.Integer :> string)}[], $5::text[], $6::${(Postgres.BigInt :> string)}[]) +ON CONFLICT ("id", "chain_id") DO NOTHING;`, + { + let ids = [] + let chainIds = [] + let blocks = [] + let logIndices = [] + let contractNames = [] + let checkpointIds = [] + addressesToWrite->Js.Array2.forEach( + addr => { + ids->Js.Array2.push(addr.id)->ignore + chainIds->Js.Array2.push(addr.chainId)->ignore + blocks->Js.Array2.push(addr.registeringEventBlock)->ignore + logIndices + ->Js.Array2.push( + addr.registeringEventLogIndex->(Utils.magic: option => Js.Null.t), + ) + ->ignore + contractNames->Js.Array2.push(addr.contractName)->ignore + checkpointIds->Js.Array2.push(addr.checkpointId->BigInt.toString)->ignore + }, + ) + (ids, chainIds, blocks, logIndices, contractNames, checkpointIds)->Obj.magic + }, + ) + ->Promise.ignoreValue + }) + } + if shouldSaveHistory { setOperations->Belt.Array.push(sql => sql->InternalTable.Checkpoints.insert( @@ -1048,6 +1100,7 @@ let rec writeBatch = async ( ~updatedEffectsCache, ~allEntities, ~updatedEntities, + ~addressesToWrite, ~sinkPromise, ) } @@ -1500,7 +1553,12 @@ let make = ( ->Postgres.unsafe(InternalTable.Checkpoints.makeGetReorgCheckpointsQuery(~pgSchema)) ->( Utils.magic: promise> => promise< - array<{"id": string, "chain_id": int, "block_number": int, "block_hash": string}>, + array<{ + "id": string, + "chain_id": int, + "block_number": int, + "block_hash": string, + }>, > ), )) @@ -1594,6 +1652,7 @@ let make = ( ~allEntities, ~updatedEffectsCache, ~updatedEntities, + ~addressesToWrite, ) => { // Initialize sink if configured let sinkPromise = switch sink { @@ -1627,6 +1686,7 @@ let make = ( ~setEffectCacheOrThrow, ~updatedEffectsCache, ~updatedEntities, + ~addressesToWrite, ~sinkPromise, ) } diff --git a/packages/envio/src/TestIndexer.res b/packages/envio/src/TestIndexer.res index b87311d4e9..5b57cb308c 100644 --- a/packages/envio/src/TestIndexer.res +++ b/packages/envio/src/TestIndexer.res @@ -34,13 +34,11 @@ type testIndexerState = { entities: dict>, entityConfigs: dict, mutable processChanges: array, + // Addresses split by chain to avoid id collision (chainIdStr -> address -> entry) + addressesByChain: dict>, } -// Cast Internal.entity back to EnvioAddresses.t -external castFromDcRegistry: Internal.entity => InternalTable.EnvioAddresses.t = "%identity" - -// Convert EnvioAddresses.t to Internal.indexingContract -let toIndexingContract = (dc: InternalTable.EnvioAddresses.t): Internal.indexingContract => { +let toIndexingContract = (dc: Config.EnvioAddresses.t): Internal.indexingContract => { address: dc->Config.EnvioAddresses.getAddress, contractName: dc.contractName, startBlock: dc.registeringEventBlock, @@ -117,6 +115,7 @@ let handleLoadByField = ( let handleWriteBatch = ( state: testIndexerState, ~updatedEntities: array, + ~addressesToWrite: array, ~checkpointIds: array, ~checkpointChainIds: array, ~checkpointBlockNumbers: array, @@ -231,35 +230,46 @@ let handleWriteBatch = ( entityChanges ->Js.Dict.entries ->Array.forEach(((entityName, {sets, deleted})) => { - // Transform envio_addresses to addresses with simplified structure - if entityName === InternalTable.EnvioAddresses.name { - let entityObj: dict = Js.Dict.empty() - if sets->Array.length > 0 { - // Transform sets to simplified {address, contract} objects - let simplifiedSets = sets->Array.map(entity => { - let dc = entity->Utils.magic->castFromDcRegistry - {"address": dc->Config.EnvioAddresses.getAddress, "contract": dc.contractName} - }) - entityObj->Js.Dict.set("sets", simplifiedSets->Utils.magic) - } - // Note: deleted is not relevant for addresses since we use address string directly - change->Js.Dict.set("addresses", entityObj->Utils.magic) - } else { - let entityObj: dict = Js.Dict.empty() - if sets->Array.length > 0 { - entityObj->Js.Dict.set("sets", sets->Utils.magic) - } - if deleted->Array.length > 0 { - entityObj->Js.Dict.set("deleted", deleted->Utils.magic) - } - change->Js.Dict.set(entityName, entityObj->Utils.magic) + let entityObj: dict = Js.Dict.empty() + if sets->Array.length > 0 { + entityObj->Js.Dict.set("sets", sets->Utils.magic) + } + if deleted->Array.length > 0 { + entityObj->Js.Dict.set("deleted", deleted->Utils.magic) } + change->Js.Dict.set(entityName, entityObj->Utils.magic) }) | None => () } + // Add address changes for this checkpoint + let addressSets = + addressesToWrite->Array.keepMap(addr => + addr.checkpointId === checkpointId + ? Some({"address": addr->Config.EnvioAddresses.getAddress, "contract": addr.contractName}) + : None + ) + if addressSets->Array.length > 0 { + let entityObj: dict = Js.Dict.empty() + entityObj->Js.Dict.set("sets", addressSets->Utils.magic) + change->Js.Dict.set("addresses", entityObj->Utils.magic) + } + state.processChanges->Array.push(change->Utils.magic)->ignore } + + // Store addresses into per-chain structure + addressesToWrite->Array.forEach(addr => { + let chainIdStr = addr.chainId->Int.toString + let chainDict = switch state.addressesByChain->Js.Dict.get(chainIdStr) { + | Some(dict) => dict + | None => + let dict = Js.Dict.empty() + state.addressesByChain->Js.Dict.set(chainIdStr, dict) + dict + } + chainDict->Js.Dict.set(addr.id, addr) + }) } let makeInitialState = ( @@ -524,23 +534,21 @@ let makeCreateTestIndexer = ( entities, entityConfigs, processChanges: [], + addressesByChain: Js.Dict.empty(), } // Build entity operations for each user entity let entityOpsDict: Js.Dict.t = Js.Dict.empty() allEntities->Array.forEach(entityConfig => { - // Only create ops for user entities (not internal tables like envio_addresses) - if entityConfig.name !== InternalTable.EnvioAddresses.name { - entityOpsDict->Js.Dict.set( - entityConfig.name, - { - get: makeEntityGet(~state, ~entityConfig), - getAll: makeEntityGetAll(~state, ~entityConfig), - getOrThrow: makeEntityGetOrThrow(~state, ~entityConfig), - set: makeEntitySet(~state, ~entityConfig), - }, - ) - } + entityOpsDict->Js.Dict.set( + entityConfig.name, + { + get: makeEntityGet(~state, ~entityConfig), + getAll: makeEntityGetAll(~state, ~entityConfig), + getOrThrow: makeEntityGetOrThrow(~state, ~entityConfig), + set: makeEntitySet(~state, ~entityConfig), + }, + ) }) // Build chain info from config (similar to Main.getGlobalIndexer but static) @@ -585,15 +593,15 @@ let makeCreateTestIndexer = ( } // Start with static config addresses let addresses = contract.addresses->Array.copy - // Add accumulated dynamic contract addresses - switch state.entities->Js.Dict.get(InternalTable.EnvioAddresses.name) { - | Some(dcDict) => - dcDict + // Add accumulated dynamic contract addresses from this chain + let chainIdStr = chainConfig.id->Int.toString + switch state.addressesByChain->Js.Dict.get(chainIdStr) { + | Some(chainDict) => + chainDict ->Js.Dict.values ->Array.forEach( - entity => { - let dc = entity->castFromDcRegistry - if dc.contractName === contract.name && dc.chainId === chainConfig.id { + dc => { + if dc.contractName === contract.name { addresses->Array.push(dc->Config.EnvioAddresses.getAddress)->ignore } }, @@ -692,26 +700,21 @@ let makeCreateTestIndexer = ( let chains: Js.Dict.t = Js.Dict.empty() chains->Js.Dict.set(chainIdStr, processChainConfig) - // Extract dynamic contracts from state.entities for each chain + // Extract dynamic contracts from addressesByChain let dynamicContractsByChain: dict> = Js.Dict.empty() - switch state.entities->Js.Dict.get(InternalTable.EnvioAddresses.name) { - | Some(dcDict) => - dcDict + state.addressesByChain + ->Js.Dict.entries + ->Array.forEach(((dcChainIdStr, chainDict)) => { + let contracts = [] + chainDict ->Js.Dict.values - ->Array.forEach(entity => { - let dc = entity->castFromDcRegistry - let dcChainIdStr = dc.chainId->Int.toString - let contracts = switch dynamicContractsByChain->Js.Dict.get(dcChainIdStr) { - | Some(arr) => arr - | None => - let arr = [] - dynamicContractsByChain->Js.Dict.set(dcChainIdStr, arr) - arr - } + ->Array.forEach(dc => { contracts->Array.push(dc->toIndexingContract)->ignore }) - | None => () - } + if contracts->Array.length > 0 { + dynamicContractsByChain->Js.Dict.set(dcChainIdStr, contracts) + } + }) let initialState = makeInitialState( ~config, @@ -761,6 +764,7 @@ let makeCreateTestIndexer = ( | WriteBatch({ updatedEntities, + addressesToWrite, checkpointIds, checkpointChainIds, checkpointBlockNumbers, @@ -769,6 +773,7 @@ let makeCreateTestIndexer = ( }) => state->handleWriteBatch( ~updatedEntities, + ~addressesToWrite, ~checkpointIds, ~checkpointChainIds, ~checkpointBlockNumbers, diff --git a/packages/envio/src/TestIndexerProxyStorage.res b/packages/envio/src/TestIndexerProxyStorage.res index 846c12e7f8..075bd9eb63 100644 --- a/packages/envio/src/TestIndexerProxyStorage.res +++ b/packages/envio/src/TestIndexerProxyStorage.res @@ -34,6 +34,7 @@ type workerPayload = | @as("writeBatch") WriteBatch({ updatedEntities: array, + addressesToWrite: array, checkpointIds: array, checkpointChainIds: array, checkpointBlockNumbers: array, @@ -145,6 +146,7 @@ let makeStorage = (proxy: t): Persistence.storage => { ~allEntities as _, ~updatedEffectsCache as _, ~updatedEntities, + ~addressesToWrite, ) => { // Encode entities to JSON for serialization across worker boundary let serializableEntities = updatedEntities->Array.map(( @@ -173,6 +175,7 @@ let makeStorage = (proxy: t): Persistence.storage => { let _ = await proxy->sendRequest( ~payload=WriteBatch({ updatedEntities: serializableEntities, + addressesToWrite, checkpointIds: batch.checkpointIds, checkpointChainIds: batch.checkpointChainIds, checkpointBlockNumbers: batch.checkpointBlockNumbers, diff --git a/packages/envio/src/db/InternalTable.gen.ts b/packages/envio/src/db/InternalTable.gen.ts index 9a74e6ca99..755b14963c 100644 --- a/packages/envio/src/db/InternalTable.gen.ts +++ b/packages/envio/src/db/InternalTable.gen.ts @@ -12,7 +12,8 @@ export type EnvioAddresses_t = { readonly chain_id: number; readonly registering_event_block: number; readonly registering_event_log_index: (undefined | number); - readonly contract_name: string + readonly contract_name: string; + readonly envio_checkpoint_id: bigint }; export type RawEvents_t = { diff --git a/packages/envio/src/db/InternalTable.res b/packages/envio/src/db/InternalTable.res index bc0c4de717..7f81df5aec 100644 --- a/packages/envio/src/db/InternalTable.res +++ b/packages/envio/src/db/InternalTable.res @@ -188,7 +188,7 @@ WHERE "${(#id: field :> string)}" = $1;` "${(#source_block: field :> string)}" as "sourceBlockNumber", ( SELECT COALESCE(json_agg(json_build_object( - 'address', SUBSTRING("id" FROM POSITION('-' IN "id") + 1), + 'address', "id", 'contractName', "contract_name", 'startBlock', "registering_event_block", 'registrationBlock', "registering_event_block" diff --git a/scenarios/test_codegen/test/helpers/Mock.res b/scenarios/test_codegen/test/helpers/Mock.res index 42eb81fff5..a4511dec4c 100644 --- a/scenarios/test_codegen/test/helpers/Mock.res +++ b/scenarios/test_codegen/test/helpers/Mock.res @@ -197,6 +197,7 @@ module Storage = { ~allEntities as _, ~updatedEffectsCache as _, ~updatedEntities as _, + ~addressesToWrite as _, ) => Js.Exn.raiseError("Not implemented"), }, } @@ -235,6 +236,7 @@ module Indexer = { query: 'entity. Indexer.Entities.name<'entity> => promise>, queryHistory: 'entity. Indexer.Entities.name<'entity> => promise>>, queryRaw: 'entity. Internal.entityConfig => promise>, + queryAddresses: unit => promise>, queryCheckpoints: unit => promise>, queryEffectCache: string => promise>, metric: string => promise>, @@ -426,6 +428,16 @@ module Indexer = { }) ->(Utils.magic: promise> => promise>) }, + queryAddresses: () => { + sql + ->Postgres.unsafe( + PgStorage.makeLoadAllQuery( + ~pgSchema, + ~tableName=InternalTable.EnvioAddresses.table.tableName, + ), + ) + ->(Utils.magic: promise> => promise>) + }, queryCheckpoints: () => { sql ->Postgres.unsafe( diff --git a/scenarios/test_codegen/test/lib_tests/Persistence_test.res b/scenarios/test_codegen/test/lib_tests/Persistence_test.res index 7db9ac3624..70ec8cce93 100644 --- a/scenarios/test_codegen/test/lib_tests/Persistence_test.res +++ b/scenarios/test_codegen/test/lib_tests/Persistence_test.res @@ -8,8 +8,8 @@ describe("Test Persistence layer init", () => { t.expect( persistence.allEntities, - ~message=`All entities should automatically include the indexer core ones`, - ).toEqual([InternalTable.EnvioAddresses.entityConfig]) + ~message=`All entities should be empty when no user entities are provided`, + ).toEqual([]) t.expect( persistence.allEnums, ~message=`All enums should automatically include the indexer core ones`, diff --git a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res index 94c46f72d5..b963489486 100644 --- a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res +++ b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res @@ -265,6 +265,7 @@ CREATE TABLE IF NOT EXISTS "test_schema"."envio_chains"("id" INTEGER NOT NULL, " CREATE TABLE IF NOT EXISTS "test_schema"."persisted_state"("id" SERIAL NOT NULL, "envio_version" TEXT NOT NULL, "config_hash" TEXT NOT NULL, "schema_hash" TEXT NOT NULL, "abi_files_hash" TEXT NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_checkpoints"("id" BIGINT NOT NULL, "chain_id" INTEGER NOT NULL, "block_number" INTEGER NOT NULL, "block_hash" TEXT, "events_processed" INTEGER NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."raw_events"("chain_id" INTEGER NOT NULL, "event_id" BIGINT NOT NULL, "event_name" TEXT NOT NULL, "contract_name" TEXT NOT NULL, "block_number" INTEGER NOT NULL, "log_index" INTEGER NOT NULL, "src_address" TEXT NOT NULL, "block_hash" TEXT NOT NULL, "block_timestamp" INTEGER NOT NULL, "block_fields" JSONB NOT NULL, "transaction_fields" JSONB NOT NULL, "params" JSONB NOT NULL, "serial" BIGSERIAL, PRIMARY KEY("serial")); +CREATE TABLE IF NOT EXISTS "test_schema"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id")); CREATE TABLE IF NOT EXISTS "test_schema"."A"("id" TEXT NOT NULL, "b_id" TEXT NOT NULL, "optionalStringToTestLinkedEntities" TEXT, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_A"("id" TEXT NOT NULL, "b_id" TEXT, "optionalStringToTestLinkedEntities" TEXT, "envio_checkpoint_id" BIGINT NOT NULL, "envio_change" "test_schema".ENVIO_HISTORY_CHANGE NOT NULL, PRIMARY KEY("id", "envio_checkpoint_id")); CREATE TABLE IF NOT EXISTS "test_schema"."B"("id" TEXT NOT NULL, "c_id" TEXT, PRIMARY KEY("id")); @@ -342,6 +343,7 @@ CREATE TABLE IF NOT EXISTS "test_schema"."envio_chains"("id" INTEGER NOT NULL, " CREATE TABLE IF NOT EXISTS "test_schema"."persisted_state"("id" SERIAL NOT NULL, "envio_version" TEXT NOT NULL, "config_hash" TEXT NOT NULL, "schema_hash" TEXT NOT NULL, "abi_files_hash" TEXT NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_checkpoints"("id" BIGINT NOT NULL, "chain_id" INTEGER NOT NULL, "block_number" INTEGER NOT NULL, "block_hash" TEXT, "events_processed" INTEGER NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."raw_events"("chain_id" INTEGER NOT NULL, "event_id" BIGINT NOT NULL, "event_name" TEXT NOT NULL, "contract_name" TEXT NOT NULL, "block_number" INTEGER NOT NULL, "log_index" INTEGER NOT NULL, "src_address" TEXT NOT NULL, "block_hash" TEXT NOT NULL, "block_timestamp" INTEGER NOT NULL, "block_fields" JSONB NOT NULL, "transaction_fields" JSONB NOT NULL, "params" JSONB NOT NULL, "serial" BIGSERIAL, PRIMARY KEY("serial")); +CREATE TABLE IF NOT EXISTS "test_schema"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id")); CREATE VIEW "test_schema"."_meta" AS SELECT "id" AS "chainId", @@ -421,6 +423,7 @@ CREATE TABLE IF NOT EXISTS "public"."envio_chains"("id" INTEGER NOT NULL, "start CREATE TABLE IF NOT EXISTS "public"."persisted_state"("id" SERIAL NOT NULL, "envio_version" TEXT NOT NULL, "config_hash" TEXT NOT NULL, "schema_hash" TEXT NOT NULL, "abi_files_hash" TEXT NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "public"."envio_checkpoints"("id" BIGINT NOT NULL, "chain_id" INTEGER NOT NULL, "block_number" INTEGER NOT NULL, "block_hash" TEXT, "events_processed" INTEGER NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "public"."raw_events"("chain_id" INTEGER NOT NULL, "event_id" BIGINT NOT NULL, "event_name" TEXT NOT NULL, "contract_name" TEXT NOT NULL, "block_number" INTEGER NOT NULL, "log_index" INTEGER NOT NULL, "src_address" TEXT NOT NULL, "block_hash" TEXT NOT NULL, "block_timestamp" INTEGER NOT NULL, "block_fields" JSONB NOT NULL, "transaction_fields" JSONB NOT NULL, "params" JSONB NOT NULL, "serial" BIGSERIAL, PRIMARY KEY("serial")); +CREATE TABLE IF NOT EXISTS "public"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id")); CREATE TABLE IF NOT EXISTS "public"."A"("id" TEXT NOT NULL, "b_id" TEXT NOT NULL, "optionalStringToTestLinkedEntities" TEXT, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "public"."envio_history_A"("id" TEXT NOT NULL, "b_id" TEXT, "optionalStringToTestLinkedEntities" TEXT, "envio_checkpoint_id" BIGINT NOT NULL, "envio_change" "public".ENVIO_HISTORY_CHANGE NOT NULL, PRIMARY KEY("id", "envio_checkpoint_id")); CREATE INDEX IF NOT EXISTS "A_b_id" ON "public"."A"("b_id"); @@ -818,7 +821,7 @@ VALUES (1, 100, 200, 5, 0, NULL, -1, -1, NULL, 0, false), "source_block" as "sourceBlockNumber", ( SELECT COALESCE(json_agg(json_build_object( - 'address', SUBSTRING("id" FROM POSITION('-' IN "id") + 1), + 'address', "id", 'contractName', "contract_name", 'startBlock', "registering_event_block", 'registrationBlock', "registering_event_block" diff --git a/scenarios/test_codegen/test/rollback/Rollback_test.res b/scenarios/test_codegen/test/rollback/Rollback_test.res index b1920df516..52fe3ec328 100644 --- a/scenarios/test_codegen/test/rollback/Rollback_test.res +++ b/scenarios/test_codegen/test/rollback/Rollback_test.res @@ -692,7 +692,7 @@ describe("E2E rollback tests", () => { ), ) t.expect( - await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>), + await (indexerMock.queryAddresses()), ~message="Shouldn't store dynamic contracts at this point", ).toEqual( [], @@ -733,24 +733,23 @@ describe("E2E rollback tests", () => { ), ) t.expect( - await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>), + (await indexerMock.queryAddresses()) + ->Array.map(a => (a.id, a.chainId, a.contractName)), ~message="Added the processed dynamic contract to the db", ).toEqual( [ - { - id: `1337-${Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0)->Address.toString}`, - chainId: 1337, - registeringEventBlock: 102, - registeringEventLogIndex: Some(2), - contractName: "SimpleNft", - }, + ( + Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0)->Address.toString, + 1337, + "SimpleNft", + ), ], ) sourceMock.resolveGetItemsOrThrow([], ~resolveAt=#last, ~latestFetchedBlockNumber=103) await indexerMock.getBatchWritePromise() t.expect( - (await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>))->Array.length, + (await (indexerMock.queryAddresses()))->Array.length, ~message="Should add the processed dynamic contracts to the db", ).toEqual( 2, @@ -813,7 +812,7 @@ describe("E2E rollback tests", () => { await Utils.delay(0) await Utils.delay(0) t.expect( - (await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>))->Array.length, + (await (indexerMock.queryAddresses()))->Array.length, ~message=`Nothing won't be rollbacked at this point. Since we need to process an event for this (rollback db only on batch write). This might be wrong after we start exposing a block hash for progress block.`, ).toEqual( @@ -835,17 +834,16 @@ This might be wrong after we start exposing a block hash for progress block.`, await indexerMock.getBatchWritePromise() t.expect( - await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>), + (await indexerMock.queryAddresses()) + ->Array.map(a => (a.id, a.chainId, a.contractName)), ~message="Should have only one dynamic contract in the db. The second one rollbacked from db, the third one rollbacked from fetch state", ).toEqual( [ - { - id: `1337-${Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0)->Address.toString}`, - chainId: 1337, - registeringEventBlock: 102, - registeringEventLogIndex: Some(2), - contractName: "SimpleNft", - }, + ( + Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0)->Address.toString, + 1337, + "SimpleNft", + ), ], ) // After the db rollback, both partitions continue from block 105 (no chunk history yet) From 3ccb5bfc27d69e8ead3086e9982ee44999f72360 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 14:46:49 +0000 Subject: [PATCH 04/11] Address CodeRabbit review: add indexes, first-write-wins, schema decoding - Add indexes on chain_id and envio_checkpoint_id for envio_addresses table to avoid seq scans on bootstrap and rollback queries - Make TestIndexer address writes first-write-wins to match DB's ON CONFLICT DO NOTHING semantics - Use schema-based decoding in queryAddresses for type-safe bigint parsing of envio_checkpoint_id https://claude.ai/code/session_01XQDfh8ZyX5TSyVDPKbMzx2 --- packages/envio/src/Config.res | 4 +-- packages/envio/src/TestIndexer.res | 29 +++++++++++++------ scenarios/test_codegen/test/helpers/Mock.res | 22 +++++++++++++- .../test/lib_tests/PgStorage_test.res | 6 ++++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/packages/envio/src/Config.res b/packages/envio/src/Config.res index 3f1ea81a6a..8f524461d3 100644 --- a/packages/envio/src/Config.res +++ b/packages/envio/src/Config.res @@ -108,7 +108,7 @@ module EnvioAddresses = { name, ~fields=[ Table.mkField("id", String, ~isPrimaryKey=true, ~fieldSchema=S.string), - Table.mkField("chain_id", Int32, ~isPrimaryKey=true, ~fieldSchema=S.int), + Table.mkField("chain_id", Int32, ~isPrimaryKey=true, ~isIndex=true, ~fieldSchema=S.int), Table.mkField("registering_event_block", Int32, ~fieldSchema=S.int), Table.mkField( "registering_event_log_index", @@ -117,7 +117,7 @@ module EnvioAddresses = { ~fieldSchema=S.null(S.int), ), Table.mkField("contract_name", String, ~fieldSchema=S.string), - Table.mkField("envio_checkpoint_id", Table.UInt64, ~fieldSchema=S.bigint), + Table.mkField("envio_checkpoint_id", Table.UInt64, ~isIndex=true, ~fieldSchema=S.bigint), ], ) } diff --git a/packages/envio/src/TestIndexer.res b/packages/envio/src/TestIndexer.res index 3b12a642a6..7903343d6c 100644 --- a/packages/envio/src/TestIndexer.res +++ b/packages/envio/src/TestIndexer.res @@ -242,13 +242,21 @@ let handleWriteBatch = ( | None => () } - // Add address changes for this checkpoint - let addressSets = - addressesToWrite->Array.keepMap(addr => - addr.checkpointId === checkpointId - ? Some({"address": addr->Config.EnvioAddresses.getAddress, "contract": addr.contractName}) - : None - ) + // Add address changes for this checkpoint (skip already-stored addresses to match ON CONFLICT DO NOTHING) + let addressSets = addressesToWrite->Array.keepMap(addr => { + if addr.checkpointId !== checkpointId { + None + } else { + let chainIdStr = addr.chainId->Int.toString + let alreadyExists = switch state.addressesByChain->Js.Dict.get(chainIdStr) { + | Some(chainDict) => chainDict->Js.Dict.get(addr.id) !== None + | None => false + } + alreadyExists + ? None + : Some({"address": addr->Config.EnvioAddresses.getAddress, "contract": addr.contractName}) + } + }) if addressSets->Array.length > 0 { let entityObj: dict = Js.Dict.empty() entityObj->Js.Dict.set("sets", addressSets->Utils.magic) @@ -258,7 +266,7 @@ let handleWriteBatch = ( state.processChanges->Array.push(change->Utils.magic)->ignore } - // Store addresses into per-chain structure + // Store addresses into per-chain structure (first-write-wins to match ON CONFLICT DO NOTHING) addressesToWrite->Array.forEach(addr => { let chainIdStr = addr.chainId->Int.toString let chainDict = switch state.addressesByChain->Js.Dict.get(chainIdStr) { @@ -268,7 +276,10 @@ let handleWriteBatch = ( state.addressesByChain->Js.Dict.set(chainIdStr, dict) dict } - chainDict->Js.Dict.set(addr.id, addr) + switch chainDict->Js.Dict.get(addr.id) { + | Some(_) => () + | None => chainDict->Js.Dict.set(addr.id, addr) + } }) } diff --git a/scenarios/test_codegen/test/helpers/Mock.res b/scenarios/test_codegen/test/helpers/Mock.res index a4511dec4c..dd93e9ffe1 100644 --- a/scenarios/test_codegen/test/helpers/Mock.res +++ b/scenarios/test_codegen/test/helpers/Mock.res @@ -436,7 +436,27 @@ module Indexer = { ~tableName=InternalTable.EnvioAddresses.table.tableName, ), ) - ->(Utils.magic: promise> => promise>) + ->Promise.thenResolve(rows => { + let schema: S.t = S.object( + s => + ( + { + id: s.field("id", S.string), + chainId: s.field("chain_id", S.int), + registeringEventBlock: s.field("registering_event_block", S.int), + registeringEventLogIndex: s.field( + "registering_event_log_index", + S.null(S.int), + ), + contractName: s.field("contract_name", S.string), + checkpointId: s.field("envio_checkpoint_id", BigInt.schema), + }: Config.EnvioAddresses.t + ), + ) + rows + ->(Utils.magic: array => array) + ->Js.Array2.map(row => row->S.convertOrThrow(schema)) + }) }, queryCheckpoints: () => { sql diff --git a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res index b963489486..36d12e43ab 100644 --- a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res +++ b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res @@ -276,6 +276,8 @@ CREATE TABLE IF NOT EXISTS "test_schema"."EntityWith63LenghtName________________ CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_EntityWith63LenghtName__________________________6"("id" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, "envio_change" "test_schema".ENVIO_HISTORY_CHANGE NOT NULL, PRIMARY KEY("id", "envio_checkpoint_id")); CREATE TABLE IF NOT EXISTS "test_schema"."EntityWithAllTypes"("id" TEXT NOT NULL, "string" TEXT NOT NULL, "optString" TEXT, "arrayOfStrings" TEXT[] NOT NULL, "int_" INTEGER NOT NULL, "optInt" INTEGER, "arrayOfInts" INTEGER[] NOT NULL, "float_" DOUBLE PRECISION NOT NULL, "optFloat" DOUBLE PRECISION, "arrayOfFloats" DOUBLE PRECISION[] NOT NULL, "bool" BOOLEAN NOT NULL, "optBool" BOOLEAN, "bigInt" NUMERIC NOT NULL, "optBigInt" NUMERIC, "arrayOfBigInts" TEXT[] NOT NULL, "bigDecimal" NUMERIC NOT NULL, "optBigDecimal" NUMERIC, "bigDecimalWithConfig" NUMERIC(10, 8) NOT NULL, "arrayOfBigDecimals" TEXT[] NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "optTimestamp" TIMESTAMP WITH TIME ZONE NULL, "json" JSONB NOT NULL, "enumField" "test_schema".AccountType NOT NULL, "optEnumField" "test_schema".AccountType, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_EntityWithAllTypes"("id" TEXT NOT NULL, "string" TEXT, "optString" TEXT, "arrayOfStrings" TEXT[], "int_" INTEGER, "optInt" INTEGER, "arrayOfInts" INTEGER[], "float_" DOUBLE PRECISION, "optFloat" DOUBLE PRECISION, "arrayOfFloats" DOUBLE PRECISION[], "bool" BOOLEAN, "optBool" BOOLEAN, "bigInt" NUMERIC, "optBigInt" NUMERIC, "arrayOfBigInts" TEXT[], "bigDecimal" NUMERIC, "optBigDecimal" NUMERIC, "bigDecimalWithConfig" NUMERIC(10, 8), "arrayOfBigDecimals" TEXT[], "timestamp" TIMESTAMP WITH TIME ZONE NULL, "optTimestamp" TIMESTAMP WITH TIME ZONE NULL, "json" JSONB, "enumField" "test_schema".AccountType, "optEnumField" "test_schema".AccountType, "envio_checkpoint_id" BIGINT NOT NULL, "envio_change" "test_schema".ENVIO_HISTORY_CHANGE NOT NULL, PRIMARY KEY("id", "envio_checkpoint_id")); +CREATE INDEX IF NOT EXISTS "envio_addresses_chain_id" ON "test_schema"."envio_addresses"("chain_id"); +CREATE INDEX IF NOT EXISTS "envio_addresses_envio_checkpoint_id" ON "test_schema"."envio_addresses"("envio_checkpoint_id"); CREATE INDEX IF NOT EXISTS "A_b_id" ON "test_schema"."A"("b_id"); CREATE INDEX IF NOT EXISTS "A_b_id" ON "test_schema"."A"("b_id"); CREATE VIEW "test_schema"."_meta" AS @@ -344,6 +346,8 @@ CREATE TABLE IF NOT EXISTS "test_schema"."persisted_state"("id" SERIAL NOT NULL, CREATE TABLE IF NOT EXISTS "test_schema"."envio_checkpoints"("id" BIGINT NOT NULL, "chain_id" INTEGER NOT NULL, "block_number" INTEGER NOT NULL, "block_hash" TEXT, "events_processed" INTEGER NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."raw_events"("chain_id" INTEGER NOT NULL, "event_id" BIGINT NOT NULL, "event_name" TEXT NOT NULL, "contract_name" TEXT NOT NULL, "block_number" INTEGER NOT NULL, "log_index" INTEGER NOT NULL, "src_address" TEXT NOT NULL, "block_hash" TEXT NOT NULL, "block_timestamp" INTEGER NOT NULL, "block_fields" JSONB NOT NULL, "transaction_fields" JSONB NOT NULL, "params" JSONB NOT NULL, "serial" BIGSERIAL, PRIMARY KEY("serial")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id")); +CREATE INDEX IF NOT EXISTS "envio_addresses_chain_id" ON "test_schema"."envio_addresses"("chain_id"); +CREATE INDEX IF NOT EXISTS "envio_addresses_envio_checkpoint_id" ON "test_schema"."envio_addresses"("envio_checkpoint_id"); CREATE VIEW "test_schema"."_meta" AS SELECT "id" AS "chainId", @@ -426,6 +430,8 @@ CREATE TABLE IF NOT EXISTS "public"."raw_events"("chain_id" INTEGER NOT NULL, "e CREATE TABLE IF NOT EXISTS "public"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id")); CREATE TABLE IF NOT EXISTS "public"."A"("id" TEXT NOT NULL, "b_id" TEXT NOT NULL, "optionalStringToTestLinkedEntities" TEXT, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "public"."envio_history_A"("id" TEXT NOT NULL, "b_id" TEXT, "optionalStringToTestLinkedEntities" TEXT, "envio_checkpoint_id" BIGINT NOT NULL, "envio_change" "public".ENVIO_HISTORY_CHANGE NOT NULL, PRIMARY KEY("id", "envio_checkpoint_id")); +CREATE INDEX IF NOT EXISTS "envio_addresses_chain_id" ON "public"."envio_addresses"("chain_id"); +CREATE INDEX IF NOT EXISTS "envio_addresses_envio_checkpoint_id" ON "public"."envio_addresses"("envio_checkpoint_id"); CREATE INDEX IF NOT EXISTS "A_b_id" ON "public"."A"("b_id"); CREATE VIEW "public"."_meta" AS SELECT From 532ddc0bae717cafb3bf08f344db59c160b46836 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 14:56:14 +0000 Subject: [PATCH 05/11] Fix Utils.magic cast type and track within-batch address duplicates - Fix queryAddresses cast from (array => array) to (unknown => array) to match queryCheckpoints pattern - Track within-batch duplicate addresses via seenInBatch dict so emitted changes match ON CONFLICT DO NOTHING semantics https://claude.ai/code/session_01XQDfh8ZyX5TSyVDPKbMzx2 --- packages/envio/src/TestIndexer.res | 15 +++++++++++++-- scenarios/test_codegen/test/helpers/Mock.res | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/envio/src/TestIndexer.res b/packages/envio/src/TestIndexer.res index 7903343d6c..e68b2432d6 100644 --- a/packages/envio/src/TestIndexer.res +++ b/packages/envio/src/TestIndexer.res @@ -200,6 +200,9 @@ let handleWriteBatch = ( }) }) + // Track addresses accepted in this batch to avoid emitting duplicates + let seenInBatch: dict = Js.Dict.empty() + // Build combined checkpoint + entity changes objects for i in 0 to checkpointIds->Array.length - 1 { let checkpointId = checkpointIds->Array.getUnsafe(i) @@ -248,13 +251,21 @@ let handleWriteBatch = ( None } else { let chainIdStr = addr.chainId->Int.toString + let batchKey = chainIdStr ++ "-" ++ addr.id let alreadyExists = switch state.addressesByChain->Js.Dict.get(chainIdStr) { | Some(chainDict) => chainDict->Js.Dict.get(addr.id) !== None | None => false } - alreadyExists + let alreadyInBatch = seenInBatch->Js.Dict.get(batchKey) !== None + alreadyExists || alreadyInBatch ? None - : Some({"address": addr->Config.EnvioAddresses.getAddress, "contract": addr.contractName}) + : { + seenInBatch->Js.Dict.set(batchKey, true) + Some({ + "address": addr->Config.EnvioAddresses.getAddress, + "contract": addr.contractName, + }) + } } }) if addressSets->Array.length > 0 { diff --git a/scenarios/test_codegen/test/helpers/Mock.res b/scenarios/test_codegen/test/helpers/Mock.res index dd93e9ffe1..e2c44ff741 100644 --- a/scenarios/test_codegen/test/helpers/Mock.res +++ b/scenarios/test_codegen/test/helpers/Mock.res @@ -454,7 +454,7 @@ module Indexer = { ), ) rows - ->(Utils.magic: array => array) + ->(Utils.magic: unknown => array) ->Js.Array2.map(row => row->S.convertOrThrow(schema)) }) }, From fbcfda77348e3db503863c2fbbfe597e038e1a1f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 08:54:36 +0000 Subject: [PATCH 06/11] Add explicit type annotations to Utils.magic casts in TestIndexer Annotate all Utils.magic casts in handleWriteBatch with explicit input => output types per project guidelines. https://claude.ai/code/session_01XQDfh8ZyX5TSyVDPKbMzx2 --- packages/envio/src/TestIndexer.res | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/envio/src/TestIndexer.res b/packages/envio/src/TestIndexer.res index e68b2432d6..cdea2fddbf 100644 --- a/packages/envio/src/TestIndexer.res +++ b/packages/envio/src/TestIndexer.res @@ -235,12 +235,12 @@ let handleWriteBatch = ( ->Array.forEach(((entityName, {sets, deleted})) => { let entityObj: dict = Js.Dict.empty() if sets->Array.length > 0 { - entityObj->Js.Dict.set("sets", sets->Utils.magic) + entityObj->Js.Dict.set("sets", sets->(Utils.magic: array => unknown)) } if deleted->Array.length > 0 { - entityObj->Js.Dict.set("deleted", deleted->Utils.magic) + entityObj->Js.Dict.set("deleted", deleted->(Utils.magic: array => unknown)) } - change->Js.Dict.set(entityName, entityObj->Utils.magic) + change->Js.Dict.set(entityName, entityObj->(Utils.magic: dict => unknown)) }) | None => () } @@ -270,11 +270,16 @@ let handleWriteBatch = ( }) if addressSets->Array.length > 0 { let entityObj: dict = Js.Dict.empty() - entityObj->Js.Dict.set("sets", addressSets->Utils.magic) - change->Js.Dict.set("addresses", entityObj->Utils.magic) + entityObj->Js.Dict.set( + "sets", + addressSets->(Utils.magic: array<{"address": Address.t, "contract": string}> => unknown), + ) + change->Js.Dict.set("addresses", entityObj->(Utils.magic: dict => unknown)) } - state.processChanges->Array.push(change->Utils.magic)->ignore + state.processChanges + ->Array.push(change->(Utils.magic: dict => unknown)) + ->ignore } // Store addresses into per-chain structure (first-write-wins to match ON CONFLICT DO NOTHING) From 615007723204befa13042392a3f9df411ebbe6b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 09:51:16 +0000 Subject: [PATCH 07/11] Restore separate history table for envio_addresses Split envio_addresses storage into two tables: - envio_addresses: current state (no envio_checkpoint_id column) - envio_history_envio_addresses: append-only log with envio_checkpoint_id as part of the composite PK On write, both tables receive the new address rows. On rollback: 1. DELETE from main table any address whose only history is past the rollback target (i.e. was only registered after the target) 2. DELETE history rows past the target This mirrors the entity framework's history pattern while supporting composite (id, chain_id) primary keys. https://claude.ai/code/session_01XQDfh8ZyX5TSyVDPKbMzx2 --- packages/envio/src/Config.res | 27 +++++- packages/envio/src/PgStorage.res | 94 +++++++++++++------ scenarios/test_codegen/test/helpers/Mock.res | 16 +++- .../test/lib_tests/PgStorage_test.res | 15 +-- 4 files changed, 110 insertions(+), 42 deletions(-) diff --git a/packages/envio/src/Config.res b/packages/envio/src/Config.res index 8f524461d3..af9c6d38ef 100644 --- a/packages/envio/src/Config.res +++ b/packages/envio/src/Config.res @@ -91,6 +91,7 @@ type t = { module EnvioAddresses = { let name = "envio_addresses" + let historyTableName = "envio_history_envio_addresses" @genType type t = { @@ -104,6 +105,7 @@ module EnvioAddresses = { let getAddress = (entity: t): Address.t => entity.id->Address.unsafeFromString + // Main table: current state of registered addresses, no checkpoint tracking let table = Table.mkTable( name, ~fields=[ @@ -117,7 +119,30 @@ module EnvioAddresses = { ~fieldSchema=S.null(S.int), ), Table.mkField("contract_name", String, ~fieldSchema=S.string), - Table.mkField("envio_checkpoint_id", Table.UInt64, ~isIndex=true, ~fieldSchema=S.bigint), + ], + ) + + // History table: append-only log of address registrations, used for rollback + let historyTable = Table.mkTable( + historyTableName, + ~fields=[ + Table.mkField("id", String, ~isPrimaryKey=true, ~fieldSchema=S.string), + Table.mkField("chain_id", Int32, ~isPrimaryKey=true, ~fieldSchema=S.int), + Table.mkField("registering_event_block", Int32, ~fieldSchema=S.int), + Table.mkField( + "registering_event_log_index", + Int32, + ~isNullable=true, + ~fieldSchema=S.null(S.int), + ), + Table.mkField("contract_name", String, ~fieldSchema=S.string), + Table.mkField( + "envio_checkpoint_id", + Table.UInt64, + ~isPrimaryKey=true, + ~isIndex=true, + ~fieldSchema=S.bigint, + ), ], ) } diff --git a/packages/envio/src/PgStorage.res b/packages/envio/src/PgStorage.res index ff4b1bde13..e9044a2fa1 100644 --- a/packages/envio/src/PgStorage.res +++ b/packages/envio/src/PgStorage.res @@ -195,6 +195,7 @@ let makeInitializeTransaction = ( InternalTable.Checkpoints.table, InternalTable.RawEvents.table, InternalTable.EnvioAddresses.table, + InternalTable.EnvioAddresses.historyTable, ] let allTables = generalTables->Array.copy @@ -948,15 +949,38 @@ let rec writeBatch = async ( sql->InternalTable.Checkpoints.rollback(~pgSchema, ~rollbackTargetCheckpointId), ) ->ignore - // Rollback addresses: delete entries created after the rollback target + // Rollback addresses: + // 1. Remove from the current-state table addresses that were only ever + // registered after the rollback target (i.e. no prior history row). + // 2. Prune history rows past the target. + let rollbackParam = + [rollbackTargetCheckpointId->BigInt.toString]->(Utils.magic: array => unknown) promises ->Js.Array2.push( sql ->Postgres.preparedUnsafe( - `DELETE FROM "${pgSchema}"."${InternalTable.EnvioAddresses.table.tableName}" WHERE "envio_checkpoint_id" > $1;`, - [rollbackTargetCheckpointId->BigInt.toString]->( - Utils.magic: array => unknown - ), + `DELETE FROM "${pgSchema}"."${InternalTable.EnvioAddresses.table.tableName}" main +WHERE EXISTS ( + SELECT 1 FROM "${pgSchema}"."${InternalTable.EnvioAddresses.historyTable.tableName}" h + WHERE h."id" = main."id" AND h."chain_id" = main."chain_id" + AND h."envio_checkpoint_id" > $1 +) +AND NOT EXISTS ( + SELECT 1 FROM "${pgSchema}"."${InternalTable.EnvioAddresses.historyTable.tableName}" h + WHERE h."id" = main."id" AND h."chain_id" = main."chain_id" + AND h."envio_checkpoint_id" <= $1 +);`, + rollbackParam, + ) + ->Promise.ignoreValue, + ) + ->ignore + promises + ->Js.Array2.push( + sql + ->Postgres.preparedUnsafe( + `DELETE FROM "${pgSchema}"."${InternalTable.EnvioAddresses.historyTable.tableName}" WHERE "envio_checkpoint_id" > $1;`, + rollbackParam, ) ->Promise.ignoreValue, ) @@ -993,37 +1017,45 @@ let rec writeBatch = async ( setRawEvents, ]->Belt.Array.concat(setEntities) - // Insert new addresses + // Insert new addresses into both the current-state table and the history table if addressesToWrite->Array.length > 0 { + let ids = [] + let chainIds = [] + let blocks = [] + let logIndices = [] + let contractNames = [] + let checkpointIds = [] + addressesToWrite->Js.Array2.forEach(addr => { + ids->Js.Array2.push(addr.id)->ignore + chainIds->Js.Array2.push(addr.chainId)->ignore + blocks->Js.Array2.push(addr.registeringEventBlock)->ignore + logIndices + ->Js.Array2.push( + addr.registeringEventLogIndex->(Utils.magic: option => Js.Null.t), + ) + ->ignore + contractNames->Js.Array2.push(addr.contractName)->ignore + checkpointIds->Js.Array2.push(addr.checkpointId->BigInt.toString)->ignore + }) + setOperations->Belt.Array.push(sql => { sql ->Postgres.preparedUnsafe( - `INSERT INTO "${pgSchema}"."${InternalTable.EnvioAddresses.table.tableName}" ("id", "chain_id", "registering_event_block", "registering_event_log_index", "contract_name", "envio_checkpoint_id") -SELECT * FROM UNNEST($1::text[], $2::${(Postgres.Integer :> string)}[], $3::${(Postgres.Integer :> string)}[], $4::${(Postgres.Integer :> string)}[], $5::text[], $6::${(Postgres.BigInt :> string)}[]) + `INSERT INTO "${pgSchema}"."${InternalTable.EnvioAddresses.table.tableName}" ("id", "chain_id", "registering_event_block", "registering_event_log_index", "contract_name") +SELECT * FROM UNNEST($1::text[], $2::${(Postgres.Integer :> string)}[], $3::${(Postgres.Integer :> string)}[], $4::${(Postgres.Integer :> string)}[], $5::text[]) ON CONFLICT ("id", "chain_id") DO NOTHING;`, - { - let ids = [] - let chainIds = [] - let blocks = [] - let logIndices = [] - let contractNames = [] - let checkpointIds = [] - addressesToWrite->Js.Array2.forEach( - addr => { - ids->Js.Array2.push(addr.id)->ignore - chainIds->Js.Array2.push(addr.chainId)->ignore - blocks->Js.Array2.push(addr.registeringEventBlock)->ignore - logIndices - ->Js.Array2.push( - addr.registeringEventLogIndex->(Utils.magic: option => Js.Null.t), - ) - ->ignore - contractNames->Js.Array2.push(addr.contractName)->ignore - checkpointIds->Js.Array2.push(addr.checkpointId->BigInt.toString)->ignore - }, - ) - (ids, chainIds, blocks, logIndices, contractNames, checkpointIds)->Obj.magic - }, + (ids, chainIds, blocks, logIndices, contractNames)->Obj.magic, + ) + ->Promise.ignoreValue + }) + + setOperations->Belt.Array.push(sql => { + sql + ->Postgres.preparedUnsafe( + `INSERT INTO "${pgSchema}"."${InternalTable.EnvioAddresses.historyTable.tableName}" ("id", "chain_id", "registering_event_block", "registering_event_log_index", "contract_name", "envio_checkpoint_id") +SELECT * FROM UNNEST($1::text[], $2::${(Postgres.Integer :> string)}[], $3::${(Postgres.Integer :> string)}[], $4::${(Postgres.Integer :> string)}[], $5::text[], $6::${(Postgres.BigInt :> string)}[]) +ON CONFLICT ("id", "chain_id", "envio_checkpoint_id") DO NOTHING;`, + (ids, chainIds, blocks, logIndices, contractNames, checkpointIds)->Obj.magic, ) ->Promise.ignoreValue }) diff --git a/scenarios/test_codegen/test/helpers/Mock.res b/scenarios/test_codegen/test/helpers/Mock.res index e2c44ff741..02e3c4e23e 100644 --- a/scenarios/test_codegen/test/helpers/Mock.res +++ b/scenarios/test_codegen/test/helpers/Mock.res @@ -229,6 +229,15 @@ module Indexer = { value: string, labels: dict, } + // Row shape returned by queryAddresses — matches the main envio_addresses + // table (no envio_checkpoint_id, which only lives in the history table). + type addressRow = { + id: string, + @as("chain_id") chainId: int, + @as("registering_event_block") registeringEventBlock: int, + @as("registering_event_log_index") registeringEventLogIndex: option, + @as("contract_name") contractName: string, + } type graphqlResponse<'a> = {data?: {..} as 'a} type rec t = { getBatchWritePromise: unit => promise, @@ -236,7 +245,7 @@ module Indexer = { query: 'entity. Indexer.Entities.name<'entity> => promise>, queryHistory: 'entity. Indexer.Entities.name<'entity> => promise>>, queryRaw: 'entity. Internal.entityConfig => promise>, - queryAddresses: unit => promise>, + queryAddresses: unit => promise>, queryCheckpoints: unit => promise>, queryEffectCache: string => promise>, metric: string => promise>, @@ -437,7 +446,7 @@ module Indexer = { ), ) ->Promise.thenResolve(rows => { - let schema: S.t = S.object( + let schema: S.t = S.object( s => ( { @@ -449,8 +458,7 @@ module Indexer = { S.null(S.int), ), contractName: s.field("contract_name", S.string), - checkpointId: s.field("envio_checkpoint_id", BigInt.schema), - }: Config.EnvioAddresses.t + }: addressRow ), ) rows diff --git a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res index 36d12e43ab..d45b870efc 100644 --- a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res +++ b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res @@ -265,7 +265,8 @@ CREATE TABLE IF NOT EXISTS "test_schema"."envio_chains"("id" INTEGER NOT NULL, " CREATE TABLE IF NOT EXISTS "test_schema"."persisted_state"("id" SERIAL NOT NULL, "envio_version" TEXT NOT NULL, "config_hash" TEXT NOT NULL, "schema_hash" TEXT NOT NULL, "abi_files_hash" TEXT NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_checkpoints"("id" BIGINT NOT NULL, "chain_id" INTEGER NOT NULL, "block_number" INTEGER NOT NULL, "block_hash" TEXT, "events_processed" INTEGER NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."raw_events"("chain_id" INTEGER NOT NULL, "event_id" BIGINT NOT NULL, "event_name" TEXT NOT NULL, "contract_name" TEXT NOT NULL, "block_number" INTEGER NOT NULL, "log_index" INTEGER NOT NULL, "src_address" TEXT NOT NULL, "block_hash" TEXT NOT NULL, "block_timestamp" INTEGER NOT NULL, "block_fields" JSONB NOT NULL, "transaction_fields" JSONB NOT NULL, "params" JSONB NOT NULL, "serial" BIGSERIAL, PRIMARY KEY("serial")); -CREATE TABLE IF NOT EXISTS "test_schema"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id")); +CREATE TABLE IF NOT EXISTS "test_schema"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, PRIMARY KEY("id", "chain_id")); +CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id", "envio_checkpoint_id")); CREATE TABLE IF NOT EXISTS "test_schema"."A"("id" TEXT NOT NULL, "b_id" TEXT NOT NULL, "optionalStringToTestLinkedEntities" TEXT, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_A"("id" TEXT NOT NULL, "b_id" TEXT, "optionalStringToTestLinkedEntities" TEXT, "envio_checkpoint_id" BIGINT NOT NULL, "envio_change" "test_schema".ENVIO_HISTORY_CHANGE NOT NULL, PRIMARY KEY("id", "envio_checkpoint_id")); CREATE TABLE IF NOT EXISTS "test_schema"."B"("id" TEXT NOT NULL, "c_id" TEXT, PRIMARY KEY("id")); @@ -277,7 +278,7 @@ CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_EntityWith63LenghtName__ CREATE TABLE IF NOT EXISTS "test_schema"."EntityWithAllTypes"("id" TEXT NOT NULL, "string" TEXT NOT NULL, "optString" TEXT, "arrayOfStrings" TEXT[] NOT NULL, "int_" INTEGER NOT NULL, "optInt" INTEGER, "arrayOfInts" INTEGER[] NOT NULL, "float_" DOUBLE PRECISION NOT NULL, "optFloat" DOUBLE PRECISION, "arrayOfFloats" DOUBLE PRECISION[] NOT NULL, "bool" BOOLEAN NOT NULL, "optBool" BOOLEAN, "bigInt" NUMERIC NOT NULL, "optBigInt" NUMERIC, "arrayOfBigInts" TEXT[] NOT NULL, "bigDecimal" NUMERIC NOT NULL, "optBigDecimal" NUMERIC, "bigDecimalWithConfig" NUMERIC(10, 8) NOT NULL, "arrayOfBigDecimals" TEXT[] NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "optTimestamp" TIMESTAMP WITH TIME ZONE NULL, "json" JSONB NOT NULL, "enumField" "test_schema".AccountType NOT NULL, "optEnumField" "test_schema".AccountType, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_EntityWithAllTypes"("id" TEXT NOT NULL, "string" TEXT, "optString" TEXT, "arrayOfStrings" TEXT[], "int_" INTEGER, "optInt" INTEGER, "arrayOfInts" INTEGER[], "float_" DOUBLE PRECISION, "optFloat" DOUBLE PRECISION, "arrayOfFloats" DOUBLE PRECISION[], "bool" BOOLEAN, "optBool" BOOLEAN, "bigInt" NUMERIC, "optBigInt" NUMERIC, "arrayOfBigInts" TEXT[], "bigDecimal" NUMERIC, "optBigDecimal" NUMERIC, "bigDecimalWithConfig" NUMERIC(10, 8), "arrayOfBigDecimals" TEXT[], "timestamp" TIMESTAMP WITH TIME ZONE NULL, "optTimestamp" TIMESTAMP WITH TIME ZONE NULL, "json" JSONB, "enumField" "test_schema".AccountType, "optEnumField" "test_schema".AccountType, "envio_checkpoint_id" BIGINT NOT NULL, "envio_change" "test_schema".ENVIO_HISTORY_CHANGE NOT NULL, PRIMARY KEY("id", "envio_checkpoint_id")); CREATE INDEX IF NOT EXISTS "envio_addresses_chain_id" ON "test_schema"."envio_addresses"("chain_id"); -CREATE INDEX IF NOT EXISTS "envio_addresses_envio_checkpoint_id" ON "test_schema"."envio_addresses"("envio_checkpoint_id"); +CREATE INDEX IF NOT EXISTS "envio_history_envio_addresses_envio_checkpoint_id" ON "test_schema"."envio_history_envio_addresses"("envio_checkpoint_id"); CREATE INDEX IF NOT EXISTS "A_b_id" ON "test_schema"."A"("b_id"); CREATE INDEX IF NOT EXISTS "A_b_id" ON "test_schema"."A"("b_id"); CREATE VIEW "test_schema"."_meta" AS @@ -345,9 +346,10 @@ CREATE TABLE IF NOT EXISTS "test_schema"."envio_chains"("id" INTEGER NOT NULL, " CREATE TABLE IF NOT EXISTS "test_schema"."persisted_state"("id" SERIAL NOT NULL, "envio_version" TEXT NOT NULL, "config_hash" TEXT NOT NULL, "schema_hash" TEXT NOT NULL, "abi_files_hash" TEXT NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_checkpoints"("id" BIGINT NOT NULL, "chain_id" INTEGER NOT NULL, "block_number" INTEGER NOT NULL, "block_hash" TEXT, "events_processed" INTEGER NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."raw_events"("chain_id" INTEGER NOT NULL, "event_id" BIGINT NOT NULL, "event_name" TEXT NOT NULL, "contract_name" TEXT NOT NULL, "block_number" INTEGER NOT NULL, "log_index" INTEGER NOT NULL, "src_address" TEXT NOT NULL, "block_hash" TEXT NOT NULL, "block_timestamp" INTEGER NOT NULL, "block_fields" JSONB NOT NULL, "transaction_fields" JSONB NOT NULL, "params" JSONB NOT NULL, "serial" BIGSERIAL, PRIMARY KEY("serial")); -CREATE TABLE IF NOT EXISTS "test_schema"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id")); +CREATE TABLE IF NOT EXISTS "test_schema"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, PRIMARY KEY("id", "chain_id")); +CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id", "envio_checkpoint_id")); CREATE INDEX IF NOT EXISTS "envio_addresses_chain_id" ON "test_schema"."envio_addresses"("chain_id"); -CREATE INDEX IF NOT EXISTS "envio_addresses_envio_checkpoint_id" ON "test_schema"."envio_addresses"("envio_checkpoint_id"); +CREATE INDEX IF NOT EXISTS "envio_history_envio_addresses_envio_checkpoint_id" ON "test_schema"."envio_history_envio_addresses"("envio_checkpoint_id"); CREATE VIEW "test_schema"."_meta" AS SELECT "id" AS "chainId", @@ -427,11 +429,12 @@ CREATE TABLE IF NOT EXISTS "public"."envio_chains"("id" INTEGER NOT NULL, "start CREATE TABLE IF NOT EXISTS "public"."persisted_state"("id" SERIAL NOT NULL, "envio_version" TEXT NOT NULL, "config_hash" TEXT NOT NULL, "schema_hash" TEXT NOT NULL, "abi_files_hash" TEXT NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "public"."envio_checkpoints"("id" BIGINT NOT NULL, "chain_id" INTEGER NOT NULL, "block_number" INTEGER NOT NULL, "block_hash" TEXT, "events_processed" INTEGER NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "public"."raw_events"("chain_id" INTEGER NOT NULL, "event_id" BIGINT NOT NULL, "event_name" TEXT NOT NULL, "contract_name" TEXT NOT NULL, "block_number" INTEGER NOT NULL, "log_index" INTEGER NOT NULL, "src_address" TEXT NOT NULL, "block_hash" TEXT NOT NULL, "block_timestamp" INTEGER NOT NULL, "block_fields" JSONB NOT NULL, "transaction_fields" JSONB NOT NULL, "params" JSONB NOT NULL, "serial" BIGSERIAL, PRIMARY KEY("serial")); -CREATE TABLE IF NOT EXISTS "public"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id")); +CREATE TABLE IF NOT EXISTS "public"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, PRIMARY KEY("id", "chain_id")); +CREATE TABLE IF NOT EXISTS "public"."envio_history_envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id", "envio_checkpoint_id")); CREATE TABLE IF NOT EXISTS "public"."A"("id" TEXT NOT NULL, "b_id" TEXT NOT NULL, "optionalStringToTestLinkedEntities" TEXT, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "public"."envio_history_A"("id" TEXT NOT NULL, "b_id" TEXT, "optionalStringToTestLinkedEntities" TEXT, "envio_checkpoint_id" BIGINT NOT NULL, "envio_change" "public".ENVIO_HISTORY_CHANGE NOT NULL, PRIMARY KEY("id", "envio_checkpoint_id")); CREATE INDEX IF NOT EXISTS "envio_addresses_chain_id" ON "public"."envio_addresses"("chain_id"); -CREATE INDEX IF NOT EXISTS "envio_addresses_envio_checkpoint_id" ON "public"."envio_addresses"("envio_checkpoint_id"); +CREATE INDEX IF NOT EXISTS "envio_history_envio_addresses_envio_checkpoint_id" ON "public"."envio_history_envio_addresses"("envio_checkpoint_id"); CREATE INDEX IF NOT EXISTS "A_b_id" ON "public"."A"("b_id"); CREATE VIEW "public"."_meta" AS SELECT From 39bc026c31f8066e66f131afa645f0e197b462e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 10:19:49 +0000 Subject: [PATCH 08/11] Put EnvioAddresses back in the entity framework with composite id Undo the custom persistence path for envio_addresses. EnvioAddresses is once again a regular entity in Persistence.allEntities, which means: - auto-generated envio_history_envio_addresses via the entity framework - write/rollback go through the same InMemoryTable.Entity + EntityHistory paths as every other entity - the in-memory id is a composite {chainId}-{address} string to stay unique across chains; getAddress extracts the raw address No custom INSERT/DELETE in PgStorage, no addressesToWrite on InMemoryStore, no per-chain dict in TestIndexer. https://claude.ai/code/session_01XQDfh8ZyX5TSyVDPKbMzx2 --- packages/envio/src/Config.gen.ts | 3 +- packages/envio/src/Config.res | 63 +++---- packages/envio/src/InMemoryStore.res | 24 ++- packages/envio/src/Persistence.res | 4 +- packages/envio/src/PgStorage.res | 86 ---------- packages/envio/src/TestIndexer.res | 159 ++++++++---------- .../envio/src/TestIndexerProxyStorage.res | 3 - packages/envio/src/db/InternalTable.gen.ts | 3 +- packages/envio/src/db/InternalTable.res | 2 +- scenarios/test_codegen/test/helpers/Mock.res | 40 ----- .../test/lib_tests/Persistence_test.res | 4 +- .../test/lib_tests/PgStorage_test.res | 14 +- .../test/rollback/Rollback_test.res | 40 +++-- 13 files changed, 146 insertions(+), 299 deletions(-) diff --git a/packages/envio/src/Config.gen.ts b/packages/envio/src/Config.gen.ts index 64303e1e10..8c653977b6 100644 --- a/packages/envio/src/Config.gen.ts +++ b/packages/envio/src/Config.gen.ts @@ -8,6 +8,5 @@ export type EnvioAddresses_t = { readonly chain_id: number; readonly registering_event_block: number; readonly registering_event_log_index: (undefined | number); - readonly contract_name: string; - readonly envio_checkpoint_id: bigint + readonly contract_name: string }; diff --git a/packages/envio/src/Config.res b/packages/envio/src/Config.res index af9c6d38ef..d3dce68378 100644 --- a/packages/envio/src/Config.res +++ b/packages/envio/src/Config.res @@ -91,7 +91,11 @@ type t = { module EnvioAddresses = { let name = "envio_addresses" - let historyTableName = "envio_history_envio_addresses" + let index = -1 + + let makeId = (~chainId, ~address) => { + chainId->Belt.Int.toString ++ "-" ++ address->Address.toString + } @genType type t = { @@ -100,17 +104,32 @@ module EnvioAddresses = { @as("registering_event_block") registeringEventBlock: int, @as("registering_event_log_index") registeringEventLogIndex: option, @as("contract_name") contractName: string, - @as("envio_checkpoint_id") checkpointId: bigint, } - let getAddress = (entity: t): Address.t => entity.id->Address.unsafeFromString + // Extract the raw contract address from the composite id ({chainId}-{address}). + // Inverse of makeId. + let getAddress = (entity: t): Address.t => { + let chainIdStr = entity.chainId->Belt.Int.toString + entity.id + ->Js.String2.sliceToEnd(~from=Js.String2.length(chainIdStr) + 1) + ->Address.unsafeFromString + } + + let schema = S.schema(s => { + id: s.matches(S.string), + chainId: s.matches(S.int), + registeringEventBlock: s.matches(S.int), + registeringEventLogIndex: s.matches(S.null(S.int)), + contractName: s.matches(S.string), + }) + + let rowsSchema = S.array(schema) - // Main table: current state of registered addresses, no checkpoint tracking let table = Table.mkTable( name, ~fields=[ Table.mkField("id", String, ~isPrimaryKey=true, ~fieldSchema=S.string), - Table.mkField("chain_id", Int32, ~isPrimaryKey=true, ~isIndex=true, ~fieldSchema=S.int), + Table.mkField("chain_id", Int32, ~fieldSchema=S.int), Table.mkField("registering_event_block", Int32, ~fieldSchema=S.int), Table.mkField( "registering_event_log_index", @@ -122,29 +141,15 @@ module EnvioAddresses = { ], ) - // History table: append-only log of address registrations, used for rollback - let historyTable = Table.mkTable( - historyTableName, - ~fields=[ - Table.mkField("id", String, ~isPrimaryKey=true, ~fieldSchema=S.string), - Table.mkField("chain_id", Int32, ~isPrimaryKey=true, ~fieldSchema=S.int), - Table.mkField("registering_event_block", Int32, ~fieldSchema=S.int), - Table.mkField( - "registering_event_log_index", - Int32, - ~isNullable=true, - ~fieldSchema=S.null(S.int), - ), - Table.mkField("contract_name", String, ~fieldSchema=S.string), - Table.mkField( - "envio_checkpoint_id", - Table.UInt64, - ~isPrimaryKey=true, - ~isIndex=true, - ~fieldSchema=S.bigint, - ), - ], - ) + external castToInternal: t => Internal.entity = "%identity" + + let entityConfig = { + name, + index, + schema, + rowsSchema, + table, + }->Internal.fromGenericEntityConfig } // Types for parsing source config from internal.config.json @@ -739,7 +744,7 @@ let fromPublic = (publicConfigJson: Js.Json.t, ~maxAddrInPartition=5000) => { ->Option.getWithDefault([]) ->parseEntitiesFromJson(~enumConfigsByName) - let allEntities = userEntities + let allEntities = userEntities->Js.Array2.concat([EnvioAddresses.entityConfig]) let userEntitiesByName = userEntities diff --git a/packages/envio/src/InMemoryStore.res b/packages/envio/src/InMemoryStore.res index e7093e1976..8d5d0d68ae 100644 --- a/packages/envio/src/InMemoryStore.res +++ b/packages/envio/src/InMemoryStore.res @@ -52,8 +52,6 @@ type t = { entities: dict>, effects: dict, rollbackTargetCheckpointId: option, - // Addresses accumulated during this batch, to be written to DB - addressesToWrite: array, } let make = (~entities: array, ~rollbackTargetCheckpointId=?): t => { @@ -61,7 +59,6 @@ let make = (~entities: array, ~rollbackTargetCheckpointId entities: EntityTables.make(entities), effects: Js.Dict.empty(), rollbackTargetCheckpointId, - addressesToWrite: [], } let clone = (self: t) => { @@ -74,7 +71,6 @@ let clone = (self: t) => { effect: table.effect, }, self.effects), rollbackTargetCheckpointId: self.rollbackTargetCheckpointId, - addressesToWrite: self.addressesToWrite->Array.copy, } let getEffectInMemTable = (inMemoryStore: t, ~effect: Internal.effect) => { @@ -102,7 +98,10 @@ let getInMemTable = ( let isRollingBack = (inMemoryStore: t) => inMemoryStore.rollbackTargetCheckpointId !== None -let setBatchDcs = (inMemoryStore: t, ~batch: Batch.t, ~shouldSaveHistory as _) => { +let setBatchDcs = (inMemoryStore: t, ~batch: Batch.t, ~shouldSaveHistory) => { + let inMemTable = + inMemoryStore->getInMemTable(~entityConfig=InternalTable.EnvioAddresses.entityConfig) + let itemIdx = ref(0) for checkpoint in 0 to batch.checkpointIds->Array.length - 1 { @@ -120,15 +119,22 @@ let setBatchDcs = (inMemoryStore: t, ~batch: Batch.t, ~shouldSaveHistory as _) = let eventItem = item->Internal.castUnsafeEventItem for dcIdx in 0 to dcs->Array.length - 1 { let dc = dcs->Js.Array2.unsafe_get(dcIdx) - let entry: Config.EnvioAddresses.t = { - id: dc.address->Address.toString, + let entity: InternalTable.EnvioAddresses.t = { + id: InternalTable.EnvioAddresses.makeId(~chainId, ~address=dc.address), chainId, contractName: dc.contractName, registeringEventBlock: eventItem.blockNumber, registeringEventLogIndex: Some(eventItem.logIndex), - checkpointId, } - inMemoryStore.addressesToWrite->Js.Array2.push(entry)->ignore + + inMemTable->InMemoryTable.Entity.set( + Set({ + entityId: entity.id, + checkpointId, + entity: entity->InternalTable.EnvioAddresses.castToInternal, + }), + ~shouldSaveHistory, + ) } } } diff --git a/packages/envio/src/Persistence.res b/packages/envio/src/Persistence.res index d4fc6689e6..a5cc67a8ea 100644 --- a/packages/envio/src/Persistence.res +++ b/packages/envio/src/Persistence.res @@ -118,7 +118,6 @@ type storage = { ~allEntities: array, ~updatedEffectsCache: array, ~updatedEntities: array, - ~addressesToWrite: array, ) => promise, } @@ -143,7 +142,7 @@ let make = ( ~allEnums, ~storage, ) => { - let allEntities = userEntities + let allEntities = userEntities->Js.Array2.concat([InternalTable.EnvioAddresses.entityConfig]) let allEnums = allEnums->Js.Array2.concat([EntityHistory.RowAction.config->Table.fromGenericEnumConfig]) { @@ -258,7 +257,6 @@ let writeBatch = ( ~config, ~allEntities=persistence.allEntities, ~updatedEntities, - ~addressesToWrite=inMemoryStore.addressesToWrite, ~updatedEffectsCache={ inMemoryStore.effects ->Js.Dict.keys diff --git a/packages/envio/src/PgStorage.res b/packages/envio/src/PgStorage.res index e9044a2fa1..50ec27f58e 100644 --- a/packages/envio/src/PgStorage.res +++ b/packages/envio/src/PgStorage.res @@ -194,8 +194,6 @@ let makeInitializeTransaction = ( InternalTable.PersistedState.table, InternalTable.Checkpoints.table, InternalTable.RawEvents.table, - InternalTable.EnvioAddresses.table, - InternalTable.EnvioAddresses.historyTable, ] let allTables = generalTables->Array.copy @@ -739,7 +737,6 @@ let rec writeBatch = async ( ~setEffectCacheOrThrow, ~updatedEffectsCache, ~updatedEntities: array, - ~addressesToWrite: array, ~sinkPromise: option>>, ~escapeTables=?, ) => { @@ -949,42 +946,6 @@ let rec writeBatch = async ( sql->InternalTable.Checkpoints.rollback(~pgSchema, ~rollbackTargetCheckpointId), ) ->ignore - // Rollback addresses: - // 1. Remove from the current-state table addresses that were only ever - // registered after the rollback target (i.e. no prior history row). - // 2. Prune history rows past the target. - let rollbackParam = - [rollbackTargetCheckpointId->BigInt.toString]->(Utils.magic: array => unknown) - promises - ->Js.Array2.push( - sql - ->Postgres.preparedUnsafe( - `DELETE FROM "${pgSchema}"."${InternalTable.EnvioAddresses.table.tableName}" main -WHERE EXISTS ( - SELECT 1 FROM "${pgSchema}"."${InternalTable.EnvioAddresses.historyTable.tableName}" h - WHERE h."id" = main."id" AND h."chain_id" = main."chain_id" - AND h."envio_checkpoint_id" > $1 -) -AND NOT EXISTS ( - SELECT 1 FROM "${pgSchema}"."${InternalTable.EnvioAddresses.historyTable.tableName}" h - WHERE h."id" = main."id" AND h."chain_id" = main."chain_id" - AND h."envio_checkpoint_id" <= $1 -);`, - rollbackParam, - ) - ->Promise.ignoreValue, - ) - ->ignore - promises - ->Js.Array2.push( - sql - ->Postgres.preparedUnsafe( - `DELETE FROM "${pgSchema}"."${InternalTable.EnvioAddresses.historyTable.tableName}" WHERE "envio_checkpoint_id" > $1;`, - rollbackParam, - ) - ->Promise.ignoreValue, - ) - ->ignore Promise.all(promises) }, ) @@ -1017,50 +978,6 @@ AND NOT EXISTS ( setRawEvents, ]->Belt.Array.concat(setEntities) - // Insert new addresses into both the current-state table and the history table - if addressesToWrite->Array.length > 0 { - let ids = [] - let chainIds = [] - let blocks = [] - let logIndices = [] - let contractNames = [] - let checkpointIds = [] - addressesToWrite->Js.Array2.forEach(addr => { - ids->Js.Array2.push(addr.id)->ignore - chainIds->Js.Array2.push(addr.chainId)->ignore - blocks->Js.Array2.push(addr.registeringEventBlock)->ignore - logIndices - ->Js.Array2.push( - addr.registeringEventLogIndex->(Utils.magic: option => Js.Null.t), - ) - ->ignore - contractNames->Js.Array2.push(addr.contractName)->ignore - checkpointIds->Js.Array2.push(addr.checkpointId->BigInt.toString)->ignore - }) - - setOperations->Belt.Array.push(sql => { - sql - ->Postgres.preparedUnsafe( - `INSERT INTO "${pgSchema}"."${InternalTable.EnvioAddresses.table.tableName}" ("id", "chain_id", "registering_event_block", "registering_event_log_index", "contract_name") -SELECT * FROM UNNEST($1::text[], $2::${(Postgres.Integer :> string)}[], $3::${(Postgres.Integer :> string)}[], $4::${(Postgres.Integer :> string)}[], $5::text[]) -ON CONFLICT ("id", "chain_id") DO NOTHING;`, - (ids, chainIds, blocks, logIndices, contractNames)->Obj.magic, - ) - ->Promise.ignoreValue - }) - - setOperations->Belt.Array.push(sql => { - sql - ->Postgres.preparedUnsafe( - `INSERT INTO "${pgSchema}"."${InternalTable.EnvioAddresses.historyTable.tableName}" ("id", "chain_id", "registering_event_block", "registering_event_log_index", "contract_name", "envio_checkpoint_id") -SELECT * FROM UNNEST($1::text[], $2::${(Postgres.Integer :> string)}[], $3::${(Postgres.Integer :> string)}[], $4::${(Postgres.Integer :> string)}[], $5::text[], $6::${(Postgres.BigInt :> string)}[]) -ON CONFLICT ("id", "chain_id", "envio_checkpoint_id") DO NOTHING;`, - (ids, chainIds, blocks, logIndices, contractNames, checkpointIds)->Obj.magic, - ) - ->Promise.ignoreValue - }) - } - if shouldSaveHistory { setOperations->Belt.Array.push(sql => sql->InternalTable.Checkpoints.insert( @@ -1132,7 +1049,6 @@ ON CONFLICT ("id", "chain_id", "envio_checkpoint_id") DO NOTHING;`, ~updatedEffectsCache, ~allEntities, ~updatedEntities, - ~addressesToWrite, ~sinkPromise, ) } @@ -1684,7 +1600,6 @@ let make = ( ~allEntities, ~updatedEffectsCache, ~updatedEntities, - ~addressesToWrite, ) => { // Initialize sink if configured let sinkPromise = switch sink { @@ -1718,7 +1633,6 @@ let make = ( ~setEffectCacheOrThrow, ~updatedEffectsCache, ~updatedEntities, - ~addressesToWrite, ~sinkPromise, ) } diff --git a/packages/envio/src/TestIndexer.res b/packages/envio/src/TestIndexer.res index cdea2fddbf..b75c3c76f5 100644 --- a/packages/envio/src/TestIndexer.res +++ b/packages/envio/src/TestIndexer.res @@ -34,11 +34,13 @@ type testIndexerState = { entities: dict>, entityConfigs: dict, mutable processChanges: array, - // Addresses split by chain to avoid id collision (chainIdStr -> address -> entry) - addressesByChain: dict>, } -let toIndexingContract = (dc: Config.EnvioAddresses.t): Internal.indexingContract => { +// Cast Internal.entity back to EnvioAddresses.t +external castFromDcRegistry: Internal.entity => InternalTable.EnvioAddresses.t = "%identity" + +// Convert EnvioAddresses.t to Internal.indexingContract +let toIndexingContract = (dc: InternalTable.EnvioAddresses.t): Internal.indexingContract => { address: dc->Config.EnvioAddresses.getAddress, contractName: dc.contractName, startBlock: dc.registeringEventBlock, @@ -115,7 +117,6 @@ let handleLoadByField = ( let handleWriteBatch = ( state: testIndexerState, ~updatedEntities: array, - ~addressesToWrite: array, ~checkpointIds: array, ~checkpointChainIds: array, ~checkpointBlockNumbers: array, @@ -200,9 +201,6 @@ let handleWriteBatch = ( }) }) - // Track addresses accepted in this batch to avoid emitting duplicates - let seenInBatch: dict = Js.Dict.empty() - // Build combined checkpoint + entity changes objects for i in 0 to checkpointIds->Array.length - 1 { let checkpointId = checkpointIds->Array.getUnsafe(i) @@ -233,70 +231,42 @@ let handleWriteBatch = ( entityChanges ->Js.Dict.entries ->Array.forEach(((entityName, {sets, deleted})) => { - let entityObj: dict = Js.Dict.empty() - if sets->Array.length > 0 { - entityObj->Js.Dict.set("sets", sets->(Utils.magic: array => unknown)) - } - if deleted->Array.length > 0 { - entityObj->Js.Dict.set("deleted", deleted->(Utils.magic: array => unknown)) + // Transform envio_addresses to addresses with simplified structure + if entityName === InternalTable.EnvioAddresses.name { + let entityObj: dict = Js.Dict.empty() + if sets->Array.length > 0 { + // Transform sets to simplified {address, contract} objects + let simplifiedSets = sets->Array.map(entity => { + let dc = entity->Utils.magic->castFromDcRegistry + {"address": dc->Config.EnvioAddresses.getAddress, "contract": dc.contractName} + }) + entityObj->Js.Dict.set( + "sets", + simplifiedSets->( + Utils.magic: array<{"address": Address.t, "contract": string}> => unknown + ), + ) + } + // Note: deleted is not relevant for addresses since we use address string directly + change->Js.Dict.set("addresses", entityObj->(Utils.magic: dict => unknown)) + } else { + let entityObj: dict = Js.Dict.empty() + if sets->Array.length > 0 { + entityObj->Js.Dict.set("sets", sets->(Utils.magic: array => unknown)) + } + if deleted->Array.length > 0 { + entityObj->Js.Dict.set("deleted", deleted->(Utils.magic: array => unknown)) + } + change->Js.Dict.set(entityName, entityObj->(Utils.magic: dict => unknown)) } - change->Js.Dict.set(entityName, entityObj->(Utils.magic: dict => unknown)) }) | None => () } - // Add address changes for this checkpoint (skip already-stored addresses to match ON CONFLICT DO NOTHING) - let addressSets = addressesToWrite->Array.keepMap(addr => { - if addr.checkpointId !== checkpointId { - None - } else { - let chainIdStr = addr.chainId->Int.toString - let batchKey = chainIdStr ++ "-" ++ addr.id - let alreadyExists = switch state.addressesByChain->Js.Dict.get(chainIdStr) { - | Some(chainDict) => chainDict->Js.Dict.get(addr.id) !== None - | None => false - } - let alreadyInBatch = seenInBatch->Js.Dict.get(batchKey) !== None - alreadyExists || alreadyInBatch - ? None - : { - seenInBatch->Js.Dict.set(batchKey, true) - Some({ - "address": addr->Config.EnvioAddresses.getAddress, - "contract": addr.contractName, - }) - } - } - }) - if addressSets->Array.length > 0 { - let entityObj: dict = Js.Dict.empty() - entityObj->Js.Dict.set( - "sets", - addressSets->(Utils.magic: array<{"address": Address.t, "contract": string}> => unknown), - ) - change->Js.Dict.set("addresses", entityObj->(Utils.magic: dict => unknown)) - } - state.processChanges ->Array.push(change->(Utils.magic: dict => unknown)) ->ignore } - - // Store addresses into per-chain structure (first-write-wins to match ON CONFLICT DO NOTHING) - addressesToWrite->Array.forEach(addr => { - let chainIdStr = addr.chainId->Int.toString - let chainDict = switch state.addressesByChain->Js.Dict.get(chainIdStr) { - | Some(dict) => dict - | None => - let dict = Js.Dict.empty() - state.addressesByChain->Js.Dict.set(chainIdStr, dict) - dict - } - switch chainDict->Js.Dict.get(addr.id) { - | Some(_) => () - | None => chainDict->Js.Dict.set(addr.id, addr) - } - }) } let makeInitialState = ( @@ -561,21 +531,23 @@ let makeCreateTestIndexer = ( entities, entityConfigs, processChanges: [], - addressesByChain: Js.Dict.empty(), } // Build entity operations for each user entity let entityOpsDict: Js.Dict.t = Js.Dict.empty() allEntities->Array.forEach(entityConfig => { - entityOpsDict->Js.Dict.set( - entityConfig.name, - { - get: makeEntityGet(~state, ~entityConfig), - getAll: makeEntityGetAll(~state, ~entityConfig), - getOrThrow: makeEntityGetOrThrow(~state, ~entityConfig), - set: makeEntitySet(~state, ~entityConfig), - }, - ) + // Only create ops for user entities (not internal tables like envio_addresses) + if entityConfig.name !== InternalTable.EnvioAddresses.name { + entityOpsDict->Js.Dict.set( + entityConfig.name, + { + get: makeEntityGet(~state, ~entityConfig), + getAll: makeEntityGetAll(~state, ~entityConfig), + getOrThrow: makeEntityGetOrThrow(~state, ~entityConfig), + set: makeEntitySet(~state, ~entityConfig), + }, + ) + } }) // Build chain info from config (similar to Main.getGlobalIndexer but static) @@ -620,15 +592,15 @@ let makeCreateTestIndexer = ( } // Start with static config addresses let addresses = contract.addresses->Array.copy - // Add accumulated dynamic contract addresses from this chain - let chainIdStr = chainConfig.id->Int.toString - switch state.addressesByChain->Js.Dict.get(chainIdStr) { - | Some(chainDict) => - chainDict + // Add accumulated dynamic contract addresses + switch state.entities->Js.Dict.get(InternalTable.EnvioAddresses.name) { + | Some(dcDict) => + dcDict ->Js.Dict.values ->Array.forEach( - dc => { - if dc.contractName === contract.name { + entity => { + let dc = entity->castFromDcRegistry + if dc.contractName === contract.name && dc.chainId === chainConfig.id { addresses->Array.push(dc->Config.EnvioAddresses.getAddress)->ignore } }, @@ -738,21 +710,26 @@ let makeCreateTestIndexer = ( let chains: Js.Dict.t = Js.Dict.empty() chains->Js.Dict.set(chainIdStr, processChainConfig) - // Extract dynamic contracts from addressesByChain + // Extract dynamic contracts from state.entities for each chain let dynamicContractsByChain: dict> = Js.Dict.empty() - state.addressesByChain - ->Js.Dict.entries - ->Array.forEach(((dcChainIdStr, chainDict)) => { - let contracts = [] - chainDict + switch state.entities->Js.Dict.get(InternalTable.EnvioAddresses.name) { + | Some(dcDict) => + dcDict ->Js.Dict.values - ->Array.forEach(dc => { + ->Array.forEach(entity => { + let dc = entity->castFromDcRegistry + let dcChainIdStr = dc.chainId->Int.toString + let contracts = switch dynamicContractsByChain->Js.Dict.get(dcChainIdStr) { + | Some(arr) => arr + | None => + let arr = [] + dynamicContractsByChain->Js.Dict.set(dcChainIdStr, arr) + arr + } contracts->Array.push(dc->toIndexingContract)->ignore }) - if contracts->Array.length > 0 { - dynamicContractsByChain->Js.Dict.set(dcChainIdStr, contracts) - } - }) + | None => () + } let initialState = makeInitialState( ~config, @@ -803,7 +780,6 @@ let makeCreateTestIndexer = ( | WriteBatch({ updatedEntities, - addressesToWrite, checkpointIds, checkpointChainIds, checkpointBlockNumbers, @@ -812,7 +788,6 @@ let makeCreateTestIndexer = ( }) => state->handleWriteBatch( ~updatedEntities, - ~addressesToWrite, ~checkpointIds, ~checkpointChainIds, ~checkpointBlockNumbers, diff --git a/packages/envio/src/TestIndexerProxyStorage.res b/packages/envio/src/TestIndexerProxyStorage.res index 075bd9eb63..846c12e7f8 100644 --- a/packages/envio/src/TestIndexerProxyStorage.res +++ b/packages/envio/src/TestIndexerProxyStorage.res @@ -34,7 +34,6 @@ type workerPayload = | @as("writeBatch") WriteBatch({ updatedEntities: array, - addressesToWrite: array, checkpointIds: array, checkpointChainIds: array, checkpointBlockNumbers: array, @@ -146,7 +145,6 @@ let makeStorage = (proxy: t): Persistence.storage => { ~allEntities as _, ~updatedEffectsCache as _, ~updatedEntities, - ~addressesToWrite, ) => { // Encode entities to JSON for serialization across worker boundary let serializableEntities = updatedEntities->Array.map(( @@ -175,7 +173,6 @@ let makeStorage = (proxy: t): Persistence.storage => { let _ = await proxy->sendRequest( ~payload=WriteBatch({ updatedEntities: serializableEntities, - addressesToWrite, checkpointIds: batch.checkpointIds, checkpointChainIds: batch.checkpointChainIds, checkpointBlockNumbers: batch.checkpointBlockNumbers, diff --git a/packages/envio/src/db/InternalTable.gen.ts b/packages/envio/src/db/InternalTable.gen.ts index 755b14963c..9a74e6ca99 100644 --- a/packages/envio/src/db/InternalTable.gen.ts +++ b/packages/envio/src/db/InternalTable.gen.ts @@ -12,8 +12,7 @@ export type EnvioAddresses_t = { readonly chain_id: number; readonly registering_event_block: number; readonly registering_event_log_index: (undefined | number); - readonly contract_name: string; - readonly envio_checkpoint_id: bigint + readonly contract_name: string }; export type RawEvents_t = { diff --git a/packages/envio/src/db/InternalTable.res b/packages/envio/src/db/InternalTable.res index 7f81df5aec..bc0c4de717 100644 --- a/packages/envio/src/db/InternalTable.res +++ b/packages/envio/src/db/InternalTable.res @@ -188,7 +188,7 @@ WHERE "${(#id: field :> string)}" = $1;` "${(#source_block: field :> string)}" as "sourceBlockNumber", ( SELECT COALESCE(json_agg(json_build_object( - 'address', "id", + 'address', SUBSTRING("id" FROM POSITION('-' IN "id") + 1), 'contractName', "contract_name", 'startBlock', "registering_event_block", 'registrationBlock', "registering_event_block" diff --git a/scenarios/test_codegen/test/helpers/Mock.res b/scenarios/test_codegen/test/helpers/Mock.res index 02e3c4e23e..42eb81fff5 100644 --- a/scenarios/test_codegen/test/helpers/Mock.res +++ b/scenarios/test_codegen/test/helpers/Mock.res @@ -197,7 +197,6 @@ module Storage = { ~allEntities as _, ~updatedEffectsCache as _, ~updatedEntities as _, - ~addressesToWrite as _, ) => Js.Exn.raiseError("Not implemented"), }, } @@ -229,15 +228,6 @@ module Indexer = { value: string, labels: dict, } - // Row shape returned by queryAddresses — matches the main envio_addresses - // table (no envio_checkpoint_id, which only lives in the history table). - type addressRow = { - id: string, - @as("chain_id") chainId: int, - @as("registering_event_block") registeringEventBlock: int, - @as("registering_event_log_index") registeringEventLogIndex: option, - @as("contract_name") contractName: string, - } type graphqlResponse<'a> = {data?: {..} as 'a} type rec t = { getBatchWritePromise: unit => promise, @@ -245,7 +235,6 @@ module Indexer = { query: 'entity. Indexer.Entities.name<'entity> => promise>, queryHistory: 'entity. Indexer.Entities.name<'entity> => promise>>, queryRaw: 'entity. Internal.entityConfig => promise>, - queryAddresses: unit => promise>, queryCheckpoints: unit => promise>, queryEffectCache: string => promise>, metric: string => promise>, @@ -437,35 +426,6 @@ module Indexer = { }) ->(Utils.magic: promise> => promise>) }, - queryAddresses: () => { - sql - ->Postgres.unsafe( - PgStorage.makeLoadAllQuery( - ~pgSchema, - ~tableName=InternalTable.EnvioAddresses.table.tableName, - ), - ) - ->Promise.thenResolve(rows => { - let schema: S.t = S.object( - s => - ( - { - id: s.field("id", S.string), - chainId: s.field("chain_id", S.int), - registeringEventBlock: s.field("registering_event_block", S.int), - registeringEventLogIndex: s.field( - "registering_event_log_index", - S.null(S.int), - ), - contractName: s.field("contract_name", S.string), - }: addressRow - ), - ) - rows - ->(Utils.magic: unknown => array) - ->Js.Array2.map(row => row->S.convertOrThrow(schema)) - }) - }, queryCheckpoints: () => { sql ->Postgres.unsafe( diff --git a/scenarios/test_codegen/test/lib_tests/Persistence_test.res b/scenarios/test_codegen/test/lib_tests/Persistence_test.res index 70ec8cce93..7db9ac3624 100644 --- a/scenarios/test_codegen/test/lib_tests/Persistence_test.res +++ b/scenarios/test_codegen/test/lib_tests/Persistence_test.res @@ -8,8 +8,8 @@ describe("Test Persistence layer init", () => { t.expect( persistence.allEntities, - ~message=`All entities should be empty when no user entities are provided`, - ).toEqual([]) + ~message=`All entities should automatically include the indexer core ones`, + ).toEqual([InternalTable.EnvioAddresses.entityConfig]) t.expect( persistence.allEnums, ~message=`All enums should automatically include the indexer core ones`, diff --git a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res index d45b870efc..94c46f72d5 100644 --- a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res +++ b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res @@ -265,8 +265,6 @@ CREATE TABLE IF NOT EXISTS "test_schema"."envio_chains"("id" INTEGER NOT NULL, " CREATE TABLE IF NOT EXISTS "test_schema"."persisted_state"("id" SERIAL NOT NULL, "envio_version" TEXT NOT NULL, "config_hash" TEXT NOT NULL, "schema_hash" TEXT NOT NULL, "abi_files_hash" TEXT NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_checkpoints"("id" BIGINT NOT NULL, "chain_id" INTEGER NOT NULL, "block_number" INTEGER NOT NULL, "block_hash" TEXT, "events_processed" INTEGER NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."raw_events"("chain_id" INTEGER NOT NULL, "event_id" BIGINT NOT NULL, "event_name" TEXT NOT NULL, "contract_name" TEXT NOT NULL, "block_number" INTEGER NOT NULL, "log_index" INTEGER NOT NULL, "src_address" TEXT NOT NULL, "block_hash" TEXT NOT NULL, "block_timestamp" INTEGER NOT NULL, "block_fields" JSONB NOT NULL, "transaction_fields" JSONB NOT NULL, "params" JSONB NOT NULL, "serial" BIGSERIAL, PRIMARY KEY("serial")); -CREATE TABLE IF NOT EXISTS "test_schema"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, PRIMARY KEY("id", "chain_id")); -CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id", "envio_checkpoint_id")); CREATE TABLE IF NOT EXISTS "test_schema"."A"("id" TEXT NOT NULL, "b_id" TEXT NOT NULL, "optionalStringToTestLinkedEntities" TEXT, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_A"("id" TEXT NOT NULL, "b_id" TEXT, "optionalStringToTestLinkedEntities" TEXT, "envio_checkpoint_id" BIGINT NOT NULL, "envio_change" "test_schema".ENVIO_HISTORY_CHANGE NOT NULL, PRIMARY KEY("id", "envio_checkpoint_id")); CREATE TABLE IF NOT EXISTS "test_schema"."B"("id" TEXT NOT NULL, "c_id" TEXT, PRIMARY KEY("id")); @@ -277,8 +275,6 @@ CREATE TABLE IF NOT EXISTS "test_schema"."EntityWith63LenghtName________________ CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_EntityWith63LenghtName__________________________6"("id" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, "envio_change" "test_schema".ENVIO_HISTORY_CHANGE NOT NULL, PRIMARY KEY("id", "envio_checkpoint_id")); CREATE TABLE IF NOT EXISTS "test_schema"."EntityWithAllTypes"("id" TEXT NOT NULL, "string" TEXT NOT NULL, "optString" TEXT, "arrayOfStrings" TEXT[] NOT NULL, "int_" INTEGER NOT NULL, "optInt" INTEGER, "arrayOfInts" INTEGER[] NOT NULL, "float_" DOUBLE PRECISION NOT NULL, "optFloat" DOUBLE PRECISION, "arrayOfFloats" DOUBLE PRECISION[] NOT NULL, "bool" BOOLEAN NOT NULL, "optBool" BOOLEAN, "bigInt" NUMERIC NOT NULL, "optBigInt" NUMERIC, "arrayOfBigInts" TEXT[] NOT NULL, "bigDecimal" NUMERIC NOT NULL, "optBigDecimal" NUMERIC, "bigDecimalWithConfig" NUMERIC(10, 8) NOT NULL, "arrayOfBigDecimals" TEXT[] NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "optTimestamp" TIMESTAMP WITH TIME ZONE NULL, "json" JSONB NOT NULL, "enumField" "test_schema".AccountType NOT NULL, "optEnumField" "test_schema".AccountType, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_EntityWithAllTypes"("id" TEXT NOT NULL, "string" TEXT, "optString" TEXT, "arrayOfStrings" TEXT[], "int_" INTEGER, "optInt" INTEGER, "arrayOfInts" INTEGER[], "float_" DOUBLE PRECISION, "optFloat" DOUBLE PRECISION, "arrayOfFloats" DOUBLE PRECISION[], "bool" BOOLEAN, "optBool" BOOLEAN, "bigInt" NUMERIC, "optBigInt" NUMERIC, "arrayOfBigInts" TEXT[], "bigDecimal" NUMERIC, "optBigDecimal" NUMERIC, "bigDecimalWithConfig" NUMERIC(10, 8), "arrayOfBigDecimals" TEXT[], "timestamp" TIMESTAMP WITH TIME ZONE NULL, "optTimestamp" TIMESTAMP WITH TIME ZONE NULL, "json" JSONB, "enumField" "test_schema".AccountType, "optEnumField" "test_schema".AccountType, "envio_checkpoint_id" BIGINT NOT NULL, "envio_change" "test_schema".ENVIO_HISTORY_CHANGE NOT NULL, PRIMARY KEY("id", "envio_checkpoint_id")); -CREATE INDEX IF NOT EXISTS "envio_addresses_chain_id" ON "test_schema"."envio_addresses"("chain_id"); -CREATE INDEX IF NOT EXISTS "envio_history_envio_addresses_envio_checkpoint_id" ON "test_schema"."envio_history_envio_addresses"("envio_checkpoint_id"); CREATE INDEX IF NOT EXISTS "A_b_id" ON "test_schema"."A"("b_id"); CREATE INDEX IF NOT EXISTS "A_b_id" ON "test_schema"."A"("b_id"); CREATE VIEW "test_schema"."_meta" AS @@ -346,10 +342,6 @@ CREATE TABLE IF NOT EXISTS "test_schema"."envio_chains"("id" INTEGER NOT NULL, " CREATE TABLE IF NOT EXISTS "test_schema"."persisted_state"("id" SERIAL NOT NULL, "envio_version" TEXT NOT NULL, "config_hash" TEXT NOT NULL, "schema_hash" TEXT NOT NULL, "abi_files_hash" TEXT NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."envio_checkpoints"("id" BIGINT NOT NULL, "chain_id" INTEGER NOT NULL, "block_number" INTEGER NOT NULL, "block_hash" TEXT, "events_processed" INTEGER NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "test_schema"."raw_events"("chain_id" INTEGER NOT NULL, "event_id" BIGINT NOT NULL, "event_name" TEXT NOT NULL, "contract_name" TEXT NOT NULL, "block_number" INTEGER NOT NULL, "log_index" INTEGER NOT NULL, "src_address" TEXT NOT NULL, "block_hash" TEXT NOT NULL, "block_timestamp" INTEGER NOT NULL, "block_fields" JSONB NOT NULL, "transaction_fields" JSONB NOT NULL, "params" JSONB NOT NULL, "serial" BIGSERIAL, PRIMARY KEY("serial")); -CREATE TABLE IF NOT EXISTS "test_schema"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, PRIMARY KEY("id", "chain_id")); -CREATE TABLE IF NOT EXISTS "test_schema"."envio_history_envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id", "envio_checkpoint_id")); -CREATE INDEX IF NOT EXISTS "envio_addresses_chain_id" ON "test_schema"."envio_addresses"("chain_id"); -CREATE INDEX IF NOT EXISTS "envio_history_envio_addresses_envio_checkpoint_id" ON "test_schema"."envio_history_envio_addresses"("envio_checkpoint_id"); CREATE VIEW "test_schema"."_meta" AS SELECT "id" AS "chainId", @@ -429,12 +421,8 @@ CREATE TABLE IF NOT EXISTS "public"."envio_chains"("id" INTEGER NOT NULL, "start CREATE TABLE IF NOT EXISTS "public"."persisted_state"("id" SERIAL NOT NULL, "envio_version" TEXT NOT NULL, "config_hash" TEXT NOT NULL, "schema_hash" TEXT NOT NULL, "abi_files_hash" TEXT NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "public"."envio_checkpoints"("id" BIGINT NOT NULL, "chain_id" INTEGER NOT NULL, "block_number" INTEGER NOT NULL, "block_hash" TEXT, "events_processed" INTEGER NOT NULL, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "public"."raw_events"("chain_id" INTEGER NOT NULL, "event_id" BIGINT NOT NULL, "event_name" TEXT NOT NULL, "contract_name" TEXT NOT NULL, "block_number" INTEGER NOT NULL, "log_index" INTEGER NOT NULL, "src_address" TEXT NOT NULL, "block_hash" TEXT NOT NULL, "block_timestamp" INTEGER NOT NULL, "block_fields" JSONB NOT NULL, "transaction_fields" JSONB NOT NULL, "params" JSONB NOT NULL, "serial" BIGSERIAL, PRIMARY KEY("serial")); -CREATE TABLE IF NOT EXISTS "public"."envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, PRIMARY KEY("id", "chain_id")); -CREATE TABLE IF NOT EXISTS "public"."envio_history_envio_addresses"("id" TEXT NOT NULL, "chain_id" INTEGER NOT NULL, "registering_event_block" INTEGER NOT NULL, "registering_event_log_index" INTEGER, "contract_name" TEXT NOT NULL, "envio_checkpoint_id" BIGINT NOT NULL, PRIMARY KEY("id", "chain_id", "envio_checkpoint_id")); CREATE TABLE IF NOT EXISTS "public"."A"("id" TEXT NOT NULL, "b_id" TEXT NOT NULL, "optionalStringToTestLinkedEntities" TEXT, PRIMARY KEY("id")); CREATE TABLE IF NOT EXISTS "public"."envio_history_A"("id" TEXT NOT NULL, "b_id" TEXT, "optionalStringToTestLinkedEntities" TEXT, "envio_checkpoint_id" BIGINT NOT NULL, "envio_change" "public".ENVIO_HISTORY_CHANGE NOT NULL, PRIMARY KEY("id", "envio_checkpoint_id")); -CREATE INDEX IF NOT EXISTS "envio_addresses_chain_id" ON "public"."envio_addresses"("chain_id"); -CREATE INDEX IF NOT EXISTS "envio_history_envio_addresses_envio_checkpoint_id" ON "public"."envio_history_envio_addresses"("envio_checkpoint_id"); CREATE INDEX IF NOT EXISTS "A_b_id" ON "public"."A"("b_id"); CREATE VIEW "public"."_meta" AS SELECT @@ -830,7 +818,7 @@ VALUES (1, 100, 200, 5, 0, NULL, -1, -1, NULL, 0, false), "source_block" as "sourceBlockNumber", ( SELECT COALESCE(json_agg(json_build_object( - 'address', "id", + 'address', SUBSTRING("id" FROM POSITION('-' IN "id") + 1), 'contractName', "contract_name", 'startBlock', "registering_event_block", 'registrationBlock', "registering_event_block" diff --git a/scenarios/test_codegen/test/rollback/Rollback_test.res b/scenarios/test_codegen/test/rollback/Rollback_test.res index 52fe3ec328..b7ae993b4c 100644 --- a/scenarios/test_codegen/test/rollback/Rollback_test.res +++ b/scenarios/test_codegen/test/rollback/Rollback_test.res @@ -692,7 +692,7 @@ describe("E2E rollback tests", () => { ), ) t.expect( - await (indexerMock.queryAddresses()), + await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>), ~message="Shouldn't store dynamic contracts at this point", ).toEqual( [], @@ -733,23 +733,26 @@ describe("E2E rollback tests", () => { ), ) t.expect( - (await indexerMock.queryAddresses()) - ->Array.map(a => (a.id, a.chainId, a.contractName)), + await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise< + array, + >), ~message="Added the processed dynamic contract to the db", ).toEqual( [ - ( - Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0)->Address.toString, - 1337, - "SimpleNft", - ), + { + id: `1337-${Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0)->Address.toString}`, + chainId: 1337, + registeringEventBlock: 102, + registeringEventLogIndex: Some(2), + contractName: "SimpleNft", + }, ], ) sourceMock.resolveGetItemsOrThrow([], ~resolveAt=#last, ~latestFetchedBlockNumber=103) await indexerMock.getBatchWritePromise() t.expect( - (await (indexerMock.queryAddresses()))->Array.length, + (await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>))->Array.length, ~message="Should add the processed dynamic contracts to the db", ).toEqual( 2, @@ -812,7 +815,7 @@ describe("E2E rollback tests", () => { await Utils.delay(0) await Utils.delay(0) t.expect( - (await (indexerMock.queryAddresses()))->Array.length, + (await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise>))->Array.length, ~message=`Nothing won't be rollbacked at this point. Since we need to process an event for this (rollback db only on batch write). This might be wrong after we start exposing a block hash for progress block.`, ).toEqual( @@ -834,16 +837,19 @@ This might be wrong after we start exposing a block hash for progress block.`, await indexerMock.getBatchWritePromise() t.expect( - (await indexerMock.queryAddresses()) - ->Array.map(a => (a.id, a.chainId, a.contractName)), + await (indexerMock.queryRaw(InternalTable.EnvioAddresses.entityConfig): promise< + array, + >), ~message="Should have only one dynamic contract in the db. The second one rollbacked from db, the third one rollbacked from fetch state", ).toEqual( [ - ( - Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0)->Address.toString, - 1337, - "SimpleNft", - ), + { + id: `1337-${Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0)->Address.toString}`, + chainId: 1337, + registeringEventBlock: 102, + registeringEventLogIndex: Some(2), + contractName: "SimpleNft", + }, ], ) // After the db rollback, both partitions continue from block 105 (no chunk history yet) From d23dfcd79ca90a5cc285899dfeac5cc0c199d576 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 11:45:46 +0000 Subject: [PATCH 09/11] Polish: optimize getAddress, add sync comments, rename cast - getAddress now uses a single indexOf('-') lookup instead of formatting chainId to a string and measuring its length - Add 'keep in sync with makeId / getAddress' comments on getAddress and on the SUBSTRING SQL in makeGetInitialStateQuery so future changes to the composite-id format flag both sites - Rename castFromDcRegistry -> castToEnvioAddresses to match the new module name (the old name referenced the pre-rename DynamicContractRegistry) https://claude.ai/code/session_01XQDfh8ZyX5TSyVDPKbMzx2 --- packages/envio/src/Config.res | 9 ++++----- packages/envio/src/TestIndexer.res | 8 ++++---- packages/envio/src/db/InternalTable.res | 3 +++ scenarios/test_codegen/test/lib_tests/PgStorage_test.res | 3 +++ 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/envio/src/Config.res b/packages/envio/src/Config.res index d3dce68378..f59eb8db51 100644 --- a/packages/envio/src/Config.res +++ b/packages/envio/src/Config.res @@ -107,12 +107,11 @@ module EnvioAddresses = { } // Extract the raw contract address from the composite id ({chainId}-{address}). - // Inverse of makeId. + // Inverse of makeId. Keep in sync with makeId above and the SUBSTRING SQL in + // InternalTable.Chains.makeGetInitialStateQuery. let getAddress = (entity: t): Address.t => { - let chainIdStr = entity.chainId->Belt.Int.toString - entity.id - ->Js.String2.sliceToEnd(~from=Js.String2.length(chainIdStr) + 1) - ->Address.unsafeFromString + let sepIdx = entity.id->Js.String2.indexOf("-") + entity.id->Js.String2.sliceToEnd(~from=sepIdx + 1)->Address.unsafeFromString } let schema = S.schema(s => { diff --git a/packages/envio/src/TestIndexer.res b/packages/envio/src/TestIndexer.res index b75c3c76f5..322a8cabbf 100644 --- a/packages/envio/src/TestIndexer.res +++ b/packages/envio/src/TestIndexer.res @@ -37,7 +37,7 @@ type testIndexerState = { } // Cast Internal.entity back to EnvioAddresses.t -external castFromDcRegistry: Internal.entity => InternalTable.EnvioAddresses.t = "%identity" +external castToEnvioAddresses: Internal.entity => InternalTable.EnvioAddresses.t = "%identity" // Convert EnvioAddresses.t to Internal.indexingContract let toIndexingContract = (dc: InternalTable.EnvioAddresses.t): Internal.indexingContract => { @@ -237,7 +237,7 @@ let handleWriteBatch = ( if sets->Array.length > 0 { // Transform sets to simplified {address, contract} objects let simplifiedSets = sets->Array.map(entity => { - let dc = entity->Utils.magic->castFromDcRegistry + let dc = entity->Utils.magic->castToEnvioAddresses {"address": dc->Config.EnvioAddresses.getAddress, "contract": dc.contractName} }) entityObj->Js.Dict.set( @@ -599,7 +599,7 @@ let makeCreateTestIndexer = ( ->Js.Dict.values ->Array.forEach( entity => { - let dc = entity->castFromDcRegistry + let dc = entity->castToEnvioAddresses if dc.contractName === contract.name && dc.chainId === chainConfig.id { addresses->Array.push(dc->Config.EnvioAddresses.getAddress)->ignore } @@ -717,7 +717,7 @@ let makeCreateTestIndexer = ( dcDict ->Js.Dict.values ->Array.forEach(entity => { - let dc = entity->castFromDcRegistry + let dc = entity->castToEnvioAddresses let dcChainIdStr = dc.chainId->Int.toString let contracts = switch dynamicContractsByChain->Js.Dict.get(dcChainIdStr) { | Some(arr) => arr diff --git a/packages/envio/src/db/InternalTable.res b/packages/envio/src/db/InternalTable.res index bc0c4de717..88435dae16 100644 --- a/packages/envio/src/db/InternalTable.res +++ b/packages/envio/src/db/InternalTable.res @@ -187,6 +187,9 @@ WHERE "${(#id: field :> string)}" = $1;` "${(#progress_block: field :> string)}" as "progressBlockNumber", "${(#source_block: field :> string)}" as "sourceBlockNumber", ( + -- envio_addresses.id is a composite "{chainId}-{address}" string produced by + -- Config.EnvioAddresses.makeId; extract the address by taking everything + -- after the first '-'. Keep in sync with makeId / getAddress. SELECT COALESCE(json_agg(json_build_object( 'address', SUBSTRING("id" FROM POSITION('-' IN "id") + 1), 'contractName', "contract_name", diff --git a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res index 94c46f72d5..5f5c4e4ab3 100644 --- a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res +++ b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res @@ -817,6 +817,9 @@ VALUES (1, 100, 200, 5, 0, NULL, -1, -1, NULL, 0, false), "progress_block" as "progressBlockNumber", "source_block" as "sourceBlockNumber", ( + -- envio_addresses.id is a composite "{chainId}-{address}" string produced by + -- Config.EnvioAddresses.makeId; extract the address by taking everything + -- after the first '-'. Keep in sync with makeId / getAddress. SELECT COALESCE(json_agg(json_build_object( 'address', SUBSTRING("id" FROM POSITION('-' IN "id") + 1), 'contractName', "contract_name", From 105f96989d7264b2243733e65527134d7c1831f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 12:04:12 +0000 Subject: [PATCH 10/11] Drop @genType, rename registering_event_* fields, use -1 sentinel - Remove @genType from EnvioAddresses.t. The generated TS type wasn't imported anywhere; dropping it deletes Config.gen.ts and removes the unused EnvioAddresses_t export from InternalTable.gen.ts. - Rename registering_event_block -> registration_block and registering_event_log_index -> registration_log_index. Matches the existing Internal.indexingContract.registrationBlock name and reads more naturally without the awkward "registering_event" prefix. - registration_log_index goes from optional to a plain int with -1 as the sentinel for "registered from a block handler" (no log index). Log indices are always >= 0, so -1 is unambiguous. Mirrors the convention in envio_chains where uninitialized block numbers are -1. https://claude.ai/code/session_01XQDfh8ZyX5TSyVDPKbMzx2 --- packages/envio/src/Config.gen.ts | 12 ----------- packages/envio/src/Config.res | 20 ++++++++----------- packages/envio/src/InMemoryStore.res | 4 ++-- packages/envio/src/TestIndexer.res | 4 ++-- packages/envio/src/db/InternalTable.gen.ts | 8 -------- packages/envio/src/db/InternalTable.res | 4 ++-- .../test/lib_tests/PgStorage_test.res | 4 ++-- .../test/rollback/Rollback_test.res | 8 ++++---- 8 files changed, 20 insertions(+), 44 deletions(-) delete mode 100644 packages/envio/src/Config.gen.ts diff --git a/packages/envio/src/Config.gen.ts b/packages/envio/src/Config.gen.ts deleted file mode 100644 index 8c653977b6..0000000000 --- a/packages/envio/src/Config.gen.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* TypeScript file generated from Config.res by genType. */ - -/* eslint-disable */ -/* tslint:disable */ - -export type EnvioAddresses_t = { - readonly id: string; - readonly chain_id: number; - readonly registering_event_block: number; - readonly registering_event_log_index: (undefined | number); - readonly contract_name: string -}; diff --git a/packages/envio/src/Config.res b/packages/envio/src/Config.res index f59eb8db51..1b33cb8949 100644 --- a/packages/envio/src/Config.res +++ b/packages/envio/src/Config.res @@ -97,12 +97,12 @@ module EnvioAddresses = { chainId->Belt.Int.toString ++ "-" ++ address->Address.toString } - @genType type t = { id: string, @as("chain_id") chainId: int, - @as("registering_event_block") registeringEventBlock: int, - @as("registering_event_log_index") registeringEventLogIndex: option, + @as("registration_block") registrationBlock: int, + // -1 when the address was registered from a block handler (no log index) + @as("registration_log_index") registrationLogIndex: int, @as("contract_name") contractName: string, } @@ -117,8 +117,8 @@ module EnvioAddresses = { let schema = S.schema(s => { id: s.matches(S.string), chainId: s.matches(S.int), - registeringEventBlock: s.matches(S.int), - registeringEventLogIndex: s.matches(S.null(S.int)), + registrationBlock: s.matches(S.int), + registrationLogIndex: s.matches(S.int), contractName: s.matches(S.string), }) @@ -129,13 +129,9 @@ module EnvioAddresses = { ~fields=[ Table.mkField("id", String, ~isPrimaryKey=true, ~fieldSchema=S.string), Table.mkField("chain_id", Int32, ~fieldSchema=S.int), - Table.mkField("registering_event_block", Int32, ~fieldSchema=S.int), - Table.mkField( - "registering_event_log_index", - Int32, - ~isNullable=true, - ~fieldSchema=S.null(S.int), - ), + Table.mkField("registration_block", Int32, ~fieldSchema=S.int), + // -1 sentinel when registered from a block handler (no log index) + Table.mkField("registration_log_index", Int32, ~fieldSchema=S.int), Table.mkField("contract_name", String, ~fieldSchema=S.string), ], ) diff --git a/packages/envio/src/InMemoryStore.res b/packages/envio/src/InMemoryStore.res index 8d5d0d68ae..d2bf3c1d55 100644 --- a/packages/envio/src/InMemoryStore.res +++ b/packages/envio/src/InMemoryStore.res @@ -123,8 +123,8 @@ let setBatchDcs = (inMemoryStore: t, ~batch: Batch.t, ~shouldSaveHistory) => { id: InternalTable.EnvioAddresses.makeId(~chainId, ~address=dc.address), chainId, contractName: dc.contractName, - registeringEventBlock: eventItem.blockNumber, - registeringEventLogIndex: Some(eventItem.logIndex), + registrationBlock: eventItem.blockNumber, + registrationLogIndex: eventItem.logIndex, } inMemTable->InMemoryTable.Entity.set( diff --git a/packages/envio/src/TestIndexer.res b/packages/envio/src/TestIndexer.res index 322a8cabbf..0760731675 100644 --- a/packages/envio/src/TestIndexer.res +++ b/packages/envio/src/TestIndexer.res @@ -43,8 +43,8 @@ external castToEnvioAddresses: Internal.entity => InternalTable.EnvioAddresses.t let toIndexingContract = (dc: InternalTable.EnvioAddresses.t): Internal.indexingContract => { address: dc->Config.EnvioAddresses.getAddress, contractName: dc.contractName, - startBlock: dc.registeringEventBlock, - registrationBlock: Some(dc.registeringEventBlock), + startBlock: dc.registrationBlock, + registrationBlock: Some(dc.registrationBlock), } let handleLoadByIds = ( diff --git a/packages/envio/src/db/InternalTable.gen.ts b/packages/envio/src/db/InternalTable.gen.ts index 9a74e6ca99..c20615607b 100644 --- a/packages/envio/src/db/InternalTable.gen.ts +++ b/packages/envio/src/db/InternalTable.gen.ts @@ -7,14 +7,6 @@ import type {Json_t as Js_Json_t} from '../../src/Js.shim.js'; import type {t as Address_t} from '../../src/Address.gen.js'; -export type EnvioAddresses_t = { - readonly id: string; - readonly chain_id: number; - readonly registering_event_block: number; - readonly registering_event_log_index: (undefined | number); - readonly contract_name: string -}; - export type RawEvents_t = { readonly chain_id: number; readonly event_id: bigint; diff --git a/packages/envio/src/db/InternalTable.res b/packages/envio/src/db/InternalTable.res index 88435dae16..5041f8b538 100644 --- a/packages/envio/src/db/InternalTable.res +++ b/packages/envio/src/db/InternalTable.res @@ -193,8 +193,8 @@ WHERE "${(#id: field :> string)}" = $1;` SELECT COALESCE(json_agg(json_build_object( 'address', SUBSTRING("id" FROM POSITION('-' IN "id") + 1), 'contractName', "contract_name", - 'startBlock', "registering_event_block", - 'registrationBlock', "registering_event_block" + 'startBlock', "registration_block", + 'registrationBlock', "registration_block" )), '[]'::json) FROM "${pgSchema}"."${EnvioAddresses.table.tableName}" WHERE "chain_id" = chains."${(#id: field :> string)}" diff --git a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res index 5f5c4e4ab3..351e79084a 100644 --- a/scenarios/test_codegen/test/lib_tests/PgStorage_test.res +++ b/scenarios/test_codegen/test/lib_tests/PgStorage_test.res @@ -823,8 +823,8 @@ VALUES (1, 100, 200, 5, 0, NULL, -1, -1, NULL, 0, false), SELECT COALESCE(json_agg(json_build_object( 'address', SUBSTRING("id" FROM POSITION('-' IN "id") + 1), 'contractName', "contract_name", - 'startBlock', "registering_event_block", - 'registrationBlock', "registering_event_block" + 'startBlock', "registration_block", + 'registrationBlock', "registration_block" )), '[]'::json) FROM "test_schema"."envio_addresses" WHERE "chain_id" = chains."id" diff --git a/scenarios/test_codegen/test/rollback/Rollback_test.res b/scenarios/test_codegen/test/rollback/Rollback_test.res index b7ae993b4c..df0cbbf924 100644 --- a/scenarios/test_codegen/test/rollback/Rollback_test.res +++ b/scenarios/test_codegen/test/rollback/Rollback_test.res @@ -742,8 +742,8 @@ describe("E2E rollback tests", () => { { id: `1337-${Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0)->Address.toString}`, chainId: 1337, - registeringEventBlock: 102, - registeringEventLogIndex: Some(2), + registrationBlock: 102, + registrationLogIndex: 2, contractName: "SimpleNft", }, ], @@ -846,8 +846,8 @@ This might be wrong after we start exposing a block hash for progress block.`, { id: `1337-${Envio.TestHelpers.Addresses.mockAddresses->Array.getUnsafe(0)->Address.toString}`, chainId: 1337, - registeringEventBlock: 102, - registeringEventLogIndex: Some(2), + registrationBlock: 102, + registrationLogIndex: 2, contractName: "SimpleNft", }, ], From d9e4adcc16f65df5dc0689ed96573840b08be3c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 14:04:02 +0000 Subject: [PATCH 11/11] Regenerate gen.ts import paths Side effect of rebuilding with rescript 12.2.0: genType now emits relative imports for RescriptSchema.gen.js instead of the package-name import. No runtime change. https://claude.ai/code/session_01XQDfh8ZyX5TSyVDPKbMzx2 --- packages/envio/src/Envio.gen.ts | 2 +- packages/envio/src/Utils.gen.ts | 2 +- packages/envio/src/bindings/BigDecimal.gen.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/envio/src/Envio.gen.ts b/packages/envio/src/Envio.gen.ts index 7b43640066..19ba3a8601 100644 --- a/packages/envio/src/Envio.gen.ts +++ b/packages/envio/src/Envio.gen.ts @@ -9,7 +9,7 @@ import type {Effect as $$effect} from './Types.ts'; import type {Logger as $$logger} from './Types.ts'; -import type {S_t as RescriptSchema_S_t} from 'rescript-schema/RescriptSchema.gen.js'; +import type {S_t as RescriptSchema_S_t} from './RescriptSchema.gen.js'; export type blockEvent = { readonly number: number }; diff --git a/packages/envio/src/Utils.gen.ts b/packages/envio/src/Utils.gen.ts index a3d21e232d..653d36862d 100644 --- a/packages/envio/src/Utils.gen.ts +++ b/packages/envio/src/Utils.gen.ts @@ -5,6 +5,6 @@ import * as UtilsJS from './Utils.res.mjs'; -import type {S_t as RescriptSchema_S_t} from 'rescript-schema/RescriptSchema.gen.js'; +import type {S_t as RescriptSchema_S_t} from './RescriptSchema.gen.js'; export const bigIntSchema: RescriptSchema_S_t = UtilsJS.bigIntSchema as any; diff --git a/packages/envio/src/bindings/BigDecimal.gen.ts b/packages/envio/src/bindings/BigDecimal.gen.ts index 67607a0197..1ea39027af 100644 --- a/packages/envio/src/bindings/BigDecimal.gen.ts +++ b/packages/envio/src/bindings/BigDecimal.gen.ts @@ -5,7 +5,7 @@ import * as BigDecimalJS from './BigDecimal.res.mjs'; -import type {S_t as RescriptSchema_S_t} from 'rescript-schema/RescriptSchema.gen.js'; +import type {S_t as RescriptSchema_S_t} from './RescriptSchema.gen.js'; import type {default as $$t} from 'bignumber.js';