From aa49ef6744a205bc2d63af3f32ad6ecbd2396229 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:15:13 -0300 Subject: [PATCH 1/3] feat: fetch events by src addresses --- packages/data-flow/src/eventsFetcher.ts | 19 +- .../src/interfaces/eventsFetcher.interface.ts | 24 ++- .../data-flow/test/unit/eventsFetcher.spec.ts | 1 + .../src/interfaces/indexerClient.ts | 24 ++- .../src/providers/envioIndexerClient.ts | 93 +++++++++- .../test/unit/envioIndexerClient.spec.ts | 163 ++++++++++++++++++ .../kysely/strategyRegistry.repository.ts | 10 +- 7 files changed, 325 insertions(+), 9 deletions(-) diff --git a/packages/data-flow/src/eventsFetcher.ts b/packages/data-flow/src/eventsFetcher.ts index 3d87a16..a043469 100644 --- a/packages/data-flow/src/eventsFetcher.ts +++ b/packages/data-flow/src/eventsFetcher.ts @@ -1,5 +1,5 @@ import { IIndexerClient } from "@grants-stack-indexer/indexer-client"; -import { AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared"; +import { Address, AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared"; import { IEventsFetcher } from "./interfaces/index.js"; @@ -19,4 +19,21 @@ export class EventsFetcher implements IEventsFetcher { limit, ); } + + /** @inheritdoc */ + async fetchEventsBySrcAddress(params: { + chainId: ChainId; + srcAddresses: Address[]; + from?: { + blockNumber?: number; + logIndex?: number; + }; + to: { + blockNumber: number; + logIndex: number; + }; + limit?: number; + }): Promise { + return this.indexerClient.getEventsBySrcAddress(params); + } } diff --git a/packages/data-flow/src/interfaces/eventsFetcher.interface.ts b/packages/data-flow/src/interfaces/eventsFetcher.interface.ts index e16ab86..c3586a3 100644 --- a/packages/data-flow/src/interfaces/eventsFetcher.interface.ts +++ b/packages/data-flow/src/interfaces/eventsFetcher.interface.ts @@ -1,4 +1,4 @@ -import { AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared"; +import { Address, AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared"; /** * Interface for the events fetcher @@ -17,4 +17,26 @@ export interface IEventsFetcher { logIndex: number, limit?: number, ): Promise; + + /** + * Fetch the events by src address, block number and log index for a chain + * @param chainId id of the chain + * @param srcAddresses src addresses to fetch events from + * @param toBlock block number to fetch events from + * @param logIndex log index in the block to fetch events from + * @param limit limit of events to fetch + */ + fetchEventsBySrcAddress(params: { + chainId: ChainId; + srcAddresses: Address[]; + from?: { + blockNumber?: number; + logIndex?: number; + }; + to: { + blockNumber: number; + logIndex: number; + }; + limit?: number; + }): Promise; } diff --git a/packages/data-flow/test/unit/eventsFetcher.spec.ts b/packages/data-flow/test/unit/eventsFetcher.spec.ts index f13fa5b..3a8e157 100644 --- a/packages/data-flow/test/unit/eventsFetcher.spec.ts +++ b/packages/data-flow/test/unit/eventsFetcher.spec.ts @@ -12,6 +12,7 @@ describe("EventsFetcher", () => { beforeEach(() => { indexerClientMock = { getEventsAfterBlockNumberAndLogIndex: vi.fn(), + getEventsBySrcAddress: vi.fn(), }; eventsFetcher = new EventsFetcher(indexerClientMock); diff --git a/packages/indexer-client/src/interfaces/indexerClient.ts b/packages/indexer-client/src/interfaces/indexerClient.ts index 77e1c97..0eaaf90 100644 --- a/packages/indexer-client/src/interfaces/indexerClient.ts +++ b/packages/indexer-client/src/interfaces/indexerClient.ts @@ -1,4 +1,4 @@ -import { AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared"; +import { Address, AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared"; /** * Interface for the indexer client @@ -17,4 +17,26 @@ export interface IIndexerClient { logIndex: number, limit?: number, ): Promise; + + /** + * Get the events by src address from the indexer service + * @param chainId Id of the chain + * @param srcAddresses Src addresses to fetch events from + * @param from Block number to start fetching events from + * @param logIndex Log index in the block + * @param limit Limit of events to fetch + */ + getEventsBySrcAddress(params: { + chainId: ChainId; + srcAddresses: Address[]; + from?: { + blockNumber?: number; + logIndex?: number; + }; + to: { + blockNumber: number; + logIndex: number; + }; + limit?: number; + }): Promise; } diff --git a/packages/indexer-client/src/providers/envioIndexerClient.ts b/packages/indexer-client/src/providers/envioIndexerClient.ts index ef2a615..cd40a8d 100644 --- a/packages/indexer-client/src/providers/envioIndexerClient.ts +++ b/packages/indexer-client/src/providers/envioIndexerClient.ts @@ -1,6 +1,6 @@ import { gql, GraphQLClient } from "graphql-request"; -import { AnyIndexerFetchedEvent, ChainId, stringify } from "@grants-stack-indexer/shared"; +import { Address, AnyIndexerFetchedEvent, ChainId, stringify } from "@grants-stack-indexer/shared"; import { IndexerClientError, InvalidIndexerResponse } from "../exceptions/index.js"; import { IIndexerClient } from "../internal.js"; @@ -73,4 +73,95 @@ export class EnvioIndexerClient implements IIndexerClient { throw new IndexerClientError(stringify(error, Object.getOwnPropertyNames(error))); } } + + /** @inheritdoc */ + async getEventsBySrcAddress(params: { + chainId: ChainId; + srcAddresses: Address[]; + from?: { + blockNumber?: number; + logIndex?: number; + }; + to: { + blockNumber: number; + logIndex: number; + }; + limit?: number; + }): Promise { + try { + const { chainId, srcAddresses, from, to, limit = 100 } = params; + const { blockNumber: toBlock, logIndex: toLogIndex } = to; + const { blockNumber: fromBlock, logIndex: fromLogIndex } = from ?? { + blockNumber: 0, + logIndex: 0, + }; + const response = (await this.client.request( + gql` + query getEventsBySrcAddress( + $chainId: Int! + $srcAddresses: [String!]! + $fromBlock: Int! + $fromLogIndex: Int! + $toBlock: Int! + $toLogIndex: Int! + $limit: Int! + ) { + raw_events( + order_by: [{ block_number: asc }, { log_index: asc }] + where: { + chain_id: { _eq: $chainId } + src_address: { _in: $srcAddresses } + _and: [ + { + _or: [ + { block_number: { _gt: $fromBlock } } + { + _and: [ + { block_number: { _eq: $fromBlock } } + { log_index: { _gt: $fromLogIndex } } + ] + } + ] + } + { + _or: [ + { block_number: { _lt: $toBlock } } + { + _and: [ + { block_number: { _eq: $toBlock } } + { log_index: { _lte: $toLogIndex } } + ] + } + ] + } + ] + } + limit: $limit + ) { + blockNumber: block_number + blockTimestamp: block_timestamp + chainId: chain_id + contractName: contract_name + eventName: event_name + logIndex: log_index + params + srcAddress: src_address + transactionFields: transaction_fields + } + } + `, + { chainId, srcAddresses, fromBlock, fromLogIndex, toBlock, toLogIndex, limit }, + )) as { raw_events: AnyIndexerFetchedEvent[] }; + if (response?.raw_events) { + return response.raw_events; + } else { + throw new InvalidIndexerResponse(stringify(response)); + } + } catch (error) { + if (error instanceof InvalidIndexerResponse) { + throw error; + } + throw new IndexerClientError(stringify(error, Object.getOwnPropertyNames(error))); + } + } } diff --git a/packages/indexer-client/test/unit/envioIndexerClient.spec.ts b/packages/indexer-client/test/unit/envioIndexerClient.spec.ts index b8ccadb..fde325b 100644 --- a/packages/indexer-client/test/unit/envioIndexerClient.spec.ts +++ b/packages/indexer-client/test/unit/envioIndexerClient.spec.ts @@ -254,4 +254,167 @@ describe("EnvioIndexerClient", () => { expect(result).toEqual([]); }); }); + + describe("getEventsBySrcAddress", () => { + beforeEach(() => { + // Update the mock implementation for getEventsBySrcAddress queries + graphqlClient.request.mockImplementation( + async ( + _document: RequestDocument | RequestOptions, + ...args: object[] + ) => { + const variables = args[0] as { + chainId: ChainId; + srcAddresses: string[]; + fromBlock: number; + fromLogIndex: number; + toBlock: number; + toLogIndex: number; + limit: number; + }; + const { + chainId, + srcAddresses, + fromBlock, + fromLogIndex, + toBlock, + toLogIndex, + limit, + } = variables; + + const filteredEvents = testEvents + .filter((event) => { + // Match chainId and srcAddress + if (event.chainId !== chainId) return false; + if (!srcAddresses.includes(event.srcAddress)) return false; + + // Check if event is after fromBlock/fromLogIndex + const isAfterFrom = + event.blockNumber > fromBlock || + (event.blockNumber === fromBlock && event.logIndex > fromLogIndex); + + // Check if event is before or at toBlock/toLogIndex + const isBeforeTo = + event.blockNumber < toBlock || + (event.blockNumber === toBlock && event.logIndex <= toLogIndex); + + return isAfterFrom && isBeforeTo; + }) + .slice(0, limit); + + return { raw_events: filteredEvents }; + }, + ); + }); + + it("returns events within the specified block range and matching srcAddresses", async () => { + const result = await envioIndexerClient.getEventsBySrcAddress({ + chainId: 1 as ChainId, + srcAddresses: ["0x1234"], + from: { blockNumber: 100, logIndex: 0 }, + to: { blockNumber: 101, logIndex: 2 }, + }); + + expect(result).toHaveLength(3); + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ blockNumber: 100, logIndex: 1 }), + expect.objectContaining({ blockNumber: 100, logIndex: 3 }), + expect.objectContaining({ blockNumber: 101, logIndex: 1 }), + ]), + ); + }); + + it("uses default from values when not provided", async () => { + const result = await envioIndexerClient.getEventsBySrcAddress({ + chainId: 1 as ChainId, + srcAddresses: ["0x1234"], + to: { blockNumber: 101, logIndex: 2 }, + }); + + expect(result).toHaveLength(3); + // Should include all events up to block 101, logIndex 2 + expect(result[0]?.blockNumber).toBe(100); + expect(result[2]?.blockNumber).toBe(101); + }); + + it("respects the limit parameter", async () => { + const result = await envioIndexerClient.getEventsBySrcAddress({ + chainId: 1 as ChainId, + srcAddresses: ["0x1234"], + to: { blockNumber: 101, logIndex: 2 }, + limit: 2, + }); + + expect(result).toHaveLength(2); + }); + + it("uses default limit when not provided", async () => { + await envioIndexerClient.getEventsBySrcAddress({ + chainId: 1 as ChainId, + srcAddresses: ["0x1234"], + to: { blockNumber: 101, logIndex: 2 }, + }); + + expect(graphqlClient.request).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ limit: 100 }), + ); + }); + + it("returns empty array when no events match srcAddresses", async () => { + const result = await envioIndexerClient.getEventsBySrcAddress({ + chainId: 1 as ChainId, + srcAddresses: ["0x9999"], + to: { blockNumber: 101, logIndex: 2 }, + }); + + expect(result).toHaveLength(0); + }); + + it("throws InvalidIndexerResponse when response structure is incorrect", async () => { + graphqlClient.request.mockResolvedValue({ status: 200, headers: {}, data: {} }); + + await expect( + envioIndexerClient.getEventsBySrcAddress({ + chainId: 1 as ChainId, + srcAddresses: ["0x1234"], + to: { blockNumber: 101, logIndex: 2 }, + }), + ).rejects.toThrow(InvalidIndexerResponse); + }); + + it("throws IndexerClientError when GraphQL request fails", async () => { + graphqlClient.request.mockRejectedValue(new Error("GraphQL request failed")); + + await expect( + envioIndexerClient.getEventsBySrcAddress({ + chainId: 1 as ChainId, + srcAddresses: ["0x1234"], + to: { blockNumber: 101, logIndex: 2 }, + }), + ).rejects.toThrow(IndexerClientError); + }); + + it("filters events by multiple srcAddresses", async () => { + // Add a test event with a different srcAddress + const extraTestEvent = { + ...testEvents[0], + srcAddress: "0x5678", + blockNumber: 100, + logIndex: 2, + } as AnyIndexerFetchedEvent; + testEvents.push(extraTestEvent); + + const result = await envioIndexerClient.getEventsBySrcAddress({ + chainId: 1 as ChainId, + srcAddresses: ["0x1234", "0x5678"], + from: { blockNumber: 100, logIndex: 0 }, + to: { blockNumber: 101, logIndex: 2 }, + }); + + expect(result).toContainEqual(expect.objectContaining({ srcAddress: "0x5678" })); + expect(result).toContainEqual(expect.objectContaining({ srcAddress: "0x1234" })); + }); + }); }); diff --git a/packages/repository/src/repositories/kysely/strategyRegistry.repository.ts b/packages/repository/src/repositories/kysely/strategyRegistry.repository.ts index a251f42..2b1b011 100644 --- a/packages/repository/src/repositories/kysely/strategyRegistry.repository.ts +++ b/packages/repository/src/repositories/kysely/strategyRegistry.repository.ts @@ -37,16 +37,16 @@ export class KyselyStrategyRegistryRepository implements IStrategyRegistryReposi /** @inheritdoc */ async getStrategies(filters?: { handled?: boolean; chainId?: ChainId }): Promise { - const query = this.db.withSchema(this.schemaName).selectFrom("strategies"); + let query = this.db.withSchema(this.schemaName).selectFrom("strategies").selectAll(); if (filters?.chainId) { - query.where("chainId", "=", filters.chainId); + query = query.where("chainId", "=", filters.chainId); } - if (filters?.handled) { - query.where("handled", "=", filters.handled); + if (filters?.handled !== undefined && filters?.handled !== null) { + query = query.where("handled", "=", filters.handled); } - return query.selectAll().execute(); + return query.execute(); } } From 1386695dc6b1dd37c0ece0f9568bd2b0afc0c842 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Wed, 18 Dec 2024 19:11:41 -0300 Subject: [PATCH 2/3] fix: write a generic method and fix errors --- packages/data-flow/src/eventsFetcher.ts | 20 +- .../src/interfaces/eventsFetcher.interface.ts | 17 +- .../data-flow/test/unit/eventsFetcher.spec.ts | 2 +- packages/indexer-client/src/external.ts | 2 + .../src/interfaces/indexerClient.ts | 25 +- packages/indexer-client/src/internal.ts | 1 + .../src/providers/envioIndexerClient.ts | 119 +++--- packages/indexer-client/src/types/index.ts | 1 + .../src/types/indexerClient.types.ts | 36 ++ .../test/unit/envioIndexerClient.spec.ts | 404 +++++++++++++----- 10 files changed, 413 insertions(+), 214 deletions(-) create mode 100644 packages/indexer-client/src/types/index.ts create mode 100644 packages/indexer-client/src/types/indexerClient.types.ts diff --git a/packages/data-flow/src/eventsFetcher.ts b/packages/data-flow/src/eventsFetcher.ts index a043469..9848215 100644 --- a/packages/data-flow/src/eventsFetcher.ts +++ b/packages/data-flow/src/eventsFetcher.ts @@ -1,5 +1,5 @@ -import { IIndexerClient } from "@grants-stack-indexer/indexer-client"; -import { Address, AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared"; +import { GetEventsFilters, IIndexerClient } from "@grants-stack-indexer/indexer-client"; +import { AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared"; import { IEventsFetcher } from "./interfaces/index.js"; @@ -21,19 +21,7 @@ export class EventsFetcher implements IEventsFetcher { } /** @inheritdoc */ - async fetchEventsBySrcAddress(params: { - chainId: ChainId; - srcAddresses: Address[]; - from?: { - blockNumber?: number; - logIndex?: number; - }; - to: { - blockNumber: number; - logIndex: number; - }; - limit?: number; - }): Promise { - return this.indexerClient.getEventsBySrcAddress(params); + async fetchEvents(params: GetEventsFilters): Promise { + return this.indexerClient.getEvents(params); } } diff --git a/packages/data-flow/src/interfaces/eventsFetcher.interface.ts b/packages/data-flow/src/interfaces/eventsFetcher.interface.ts index c3586a3..27969fa 100644 --- a/packages/data-flow/src/interfaces/eventsFetcher.interface.ts +++ b/packages/data-flow/src/interfaces/eventsFetcher.interface.ts @@ -1,4 +1,5 @@ -import { Address, AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared"; +import { GetEventsFilters } from "@grants-stack-indexer/indexer-client"; +import { AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared"; /** * Interface for the events fetcher @@ -26,17 +27,5 @@ export interface IEventsFetcher { * @param logIndex log index in the block to fetch events from * @param limit limit of events to fetch */ - fetchEventsBySrcAddress(params: { - chainId: ChainId; - srcAddresses: Address[]; - from?: { - blockNumber?: number; - logIndex?: number; - }; - to: { - blockNumber: number; - logIndex: number; - }; - limit?: number; - }): Promise; + fetchEvents(params: GetEventsFilters): Promise; } diff --git a/packages/data-flow/test/unit/eventsFetcher.spec.ts b/packages/data-flow/test/unit/eventsFetcher.spec.ts index 3a8e157..586eedb 100644 --- a/packages/data-flow/test/unit/eventsFetcher.spec.ts +++ b/packages/data-flow/test/unit/eventsFetcher.spec.ts @@ -12,7 +12,7 @@ describe("EventsFetcher", () => { beforeEach(() => { indexerClientMock = { getEventsAfterBlockNumberAndLogIndex: vi.fn(), - getEventsBySrcAddress: vi.fn(), + getEvents: vi.fn(), }; eventsFetcher = new EventsFetcher(indexerClientMock); diff --git a/packages/indexer-client/src/external.ts b/packages/indexer-client/src/external.ts index ba0a97f..b9a9716 100644 --- a/packages/indexer-client/src/external.ts +++ b/packages/indexer-client/src/external.ts @@ -1,3 +1,5 @@ export type { IIndexerClient } from "./internal.js"; export { EnvioIndexerClient } from "./internal.js"; + +export type { GetEventsFilters } from "./internal.js"; diff --git a/packages/indexer-client/src/interfaces/indexerClient.ts b/packages/indexer-client/src/interfaces/indexerClient.ts index 0eaaf90..efce730 100644 --- a/packages/indexer-client/src/interfaces/indexerClient.ts +++ b/packages/indexer-client/src/interfaces/indexerClient.ts @@ -1,4 +1,6 @@ -import { Address, AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared"; +import { AnyIndexerFetchedEvent, ChainId } from "@grants-stack-indexer/shared"; + +import { GetEventsFilters } from "../internal.js"; /** * Interface for the indexer client @@ -20,23 +22,8 @@ export interface IIndexerClient { /** * Get the events by src address from the indexer service - * @param chainId Id of the chain - * @param srcAddresses Src addresses to fetch events from - * @param from Block number to start fetching events from - * @param logIndex Log index in the block - * @param limit Limit of events to fetch + * @param params Filters to fetch events + * @returns Events fetched from the indexer service */ - getEventsBySrcAddress(params: { - chainId: ChainId; - srcAddresses: Address[]; - from?: { - blockNumber?: number; - logIndex?: number; - }; - to: { - blockNumber: number; - logIndex: number; - }; - limit?: number; - }): Promise; + getEvents(params: GetEventsFilters): Promise; } diff --git a/packages/indexer-client/src/internal.ts b/packages/indexer-client/src/internal.ts index 092eb9a..318b9d7 100644 --- a/packages/indexer-client/src/internal.ts +++ b/packages/indexer-client/src/internal.ts @@ -1,3 +1,4 @@ export * from "./exceptions/index.js"; +export * from "./types/index.js"; export * from "./interfaces/index.js"; export * from "./providers/index.js"; diff --git a/packages/indexer-client/src/providers/envioIndexerClient.ts b/packages/indexer-client/src/providers/envioIndexerClient.ts index cd40a8d..cff4cd2 100644 --- a/packages/indexer-client/src/providers/envioIndexerClient.ts +++ b/packages/indexer-client/src/providers/envioIndexerClient.ts @@ -1,9 +1,9 @@ import { gql, GraphQLClient } from "graphql-request"; -import { Address, AnyIndexerFetchedEvent, ChainId, stringify } from "@grants-stack-indexer/shared"; +import { AnyIndexerFetchedEvent, ChainId, stringify } from "@grants-stack-indexer/shared"; import { IndexerClientError, InvalidIndexerResponse } from "../exceptions/index.js"; -import { IIndexerClient } from "../internal.js"; +import { GetEventsFilters, IIndexerClient } from "../internal.js"; /** * Indexer client for the Envio indexer service @@ -75,67 +75,72 @@ export class EnvioIndexerClient implements IIndexerClient { } /** @inheritdoc */ - async getEventsBySrcAddress(params: { - chainId: ChainId; - srcAddresses: Address[]; - from?: { - blockNumber?: number; - logIndex?: number; - }; - to: { - blockNumber: number; - logIndex: number; - }; - limit?: number; - }): Promise { + async getEvents(params: GetEventsFilters): Promise { try { const { chainId, srcAddresses, from, to, limit = 100 } = params; - const { blockNumber: toBlock, logIndex: toLogIndex } = to; - const { blockNumber: fromBlock, logIndex: fromLogIndex } = from ?? { - blockNumber: 0, - logIndex: 0, - }; + + // Build the _and conditions array + const andConditions = []; + andConditions.push(`chain_id: { _eq: $chainId }`); + const vars: Record = { chainId }; + + // Add srcAddresses filter if provided + if (srcAddresses && srcAddresses.length > 0) { + andConditions.push(`src_address: { _in: $srcAddresses }`); + vars["srcAddresses"] = srcAddresses; + } + + if (from != undefined && from != null) { + andConditions.push(` + _or: [ + { block_number: { _gt: $fromBlock } }, + { + _and: [ + { block_number: { _eq: $fromBlock } }, + { log_index: { _gt: $fromLogIndex } } + ] + } + ] + `); + vars["fromBlock"] = from.blockNumber; + vars["fromLogIndex"] = from.logIndex; + } + + if (to != undefined && to != null) { + andConditions.push(` + _or: [ + { block_number: { _lt: $toBlock } }, + { + _and: [ + { block_number: { _eq: $toBlock } }, + { log_index: { _lte: $toLogIndex } } + ] + } + ] + `); + vars["toBlock"] = to.blockNumber; + vars["toLogIndex"] = to.logIndex; + } + + const whereClause = + andConditions.length > 1 + ? `_and: [{ ${andConditions.join(" }, { ")} }]` + : andConditions[0]; + const response = (await this.client.request( gql` - query getEventsBySrcAddress( + query getEvents( $chainId: Int! - $srcAddresses: [String!]! - $fromBlock: Int! - $fromLogIndex: Int! - $toBlock: Int! - $toLogIndex: Int! + $srcAddresses: [String!] + $fromBlock: Int + $fromLogIndex: Int + $toBlock: Int + $toLogIndex: Int $limit: Int! ) { raw_events( order_by: [{ block_number: asc }, { log_index: asc }] - where: { - chain_id: { _eq: $chainId } - src_address: { _in: $srcAddresses } - _and: [ - { - _or: [ - { block_number: { _gt: $fromBlock } } - { - _and: [ - { block_number: { _eq: $fromBlock } } - { log_index: { _gt: $fromLogIndex } } - ] - } - ] - } - { - _or: [ - { block_number: { _lt: $toBlock } } - { - _and: [ - { block_number: { _eq: $toBlock } } - { log_index: { _lte: $toLogIndex } } - ] - } - ] - } - ] - } + where: { ${whereClause} } limit: $limit ) { blockNumber: block_number @@ -150,8 +155,12 @@ export class EnvioIndexerClient implements IIndexerClient { } } `, - { chainId, srcAddresses, fromBlock, fromLogIndex, toBlock, toLogIndex, limit }, + { + ...vars, + limit, + }, )) as { raw_events: AnyIndexerFetchedEvent[] }; + if (response?.raw_events) { return response.raw_events; } else { diff --git a/packages/indexer-client/src/types/index.ts b/packages/indexer-client/src/types/index.ts new file mode 100644 index 0000000..c1f2ca3 --- /dev/null +++ b/packages/indexer-client/src/types/index.ts @@ -0,0 +1 @@ +export * from "./indexerClient.types.js"; diff --git a/packages/indexer-client/src/types/indexerClient.types.ts b/packages/indexer-client/src/types/indexerClient.types.ts new file mode 100644 index 0000000..79e9385 --- /dev/null +++ b/packages/indexer-client/src/types/indexerClient.types.ts @@ -0,0 +1,36 @@ +import { Address, ChainId } from "@grants-stack-indexer/shared"; + +export type GetEventsFilters = { + /** + * Id of the chain to fetch events from + */ + chainId: ChainId; + /** + * Src addresses to filter events by + */ + srcAddresses?: Address[]; + from?: { + /** + * Block number to start fetching events from + */ + blockNumber: number; + /** + * Log index in the block + */ + logIndex: number; + }; + to?: { + /** + * Block number to end fetching events at + */ + blockNumber: number; + /** + * Log index in the block + */ + logIndex: number; + }; + /** + * Limit of events to fetch + */ + limit?: number; +}; diff --git a/packages/indexer-client/test/unit/envioIndexerClient.spec.ts b/packages/indexer-client/test/unit/envioIndexerClient.spec.ts index fde325b..10cd34a 100644 --- a/packages/indexer-client/test/unit/envioIndexerClient.spec.ts +++ b/packages/indexer-client/test/unit/envioIndexerClient.spec.ts @@ -1,7 +1,12 @@ import { GraphQLClient, RequestDocument, RequestOptions } from "graphql-request"; import { afterEach, beforeEach, describe, expect, it, Mocked, vi } from "vitest"; -import { AnyIndexerFetchedEvent, ChainId, PoolCreatedParams } from "@grants-stack-indexer/shared"; +import { + Address, + AnyIndexerFetchedEvent, + ChainId, + PoolCreatedParams, +} from "@grants-stack-indexer/shared"; import { IndexerClientError, InvalidIndexerResponse } from "../../src/exceptions/index.js"; import { EnvioIndexerClient } from "../../src/providers/envioIndexerClient.js"; @@ -60,7 +65,7 @@ describe("EnvioIndexerClient", () => { blockTimestamp: 123123124, contractName: "Allo", eventName: "PoolCreated", - srcAddress: "0x1234", + srcAddress: "0x3456", logIndex: 1, params: { contractAddress: "0x1234", @@ -89,37 +94,6 @@ describe("EnvioIndexerClient", () => { beforeEach(() => { envioIndexerClient = new EnvioIndexerClient("http://example.com/graphql", "secret"); graphqlClient = envioIndexerClient["client"] as unknown as Mocked; - - // Mock the request implementation to simulate database querying - graphqlClient.request.mockImplementation( - async ( - _document: RequestDocument | RequestOptions, - ...args: object[] - ) => { - const variables = args[0] as { - chainId: ChainId; - blockNumber: number; - logIndex: number; - limit: number; - }; - const { chainId, blockNumber, logIndex, limit } = variables; - - const filteredEvents = testEvents - .filter((event) => { - // Match chainId - if (event.chainId !== chainId) return false; - - // Implement the _or condition from the GraphQL query - return ( - event.blockNumber > blockNumber || - (event.blockNumber === blockNumber && event.logIndex > logIndex) - ); - }) - .slice(0, limit); // Apply limit - - return { raw_events: filteredEvents }; - }, - ); }); afterEach(() => { @@ -137,6 +111,42 @@ describe("EnvioIndexerClient", () => { }); describe("getEventsAfterBlockNumberAndLogIndex", () => { + beforeEach(() => { + envioIndexerClient = new EnvioIndexerClient("http://example.com/graphql", "secret"); + graphqlClient = envioIndexerClient["client"] as unknown as Mocked; + + // Mock the request implementation to simulate database querying + graphqlClient.request.mockImplementation( + async ( + _document: RequestDocument | RequestOptions, + ...args: object[] + ) => { + const variables = args[0] as { + chainId: ChainId; + blockNumber: number; + logIndex: number; + limit: number; + }; + const { chainId, blockNumber, logIndex, limit } = variables; + + const filteredEvents = testEvents + .filter((event) => { + // Match chainId + if (event.chainId !== chainId) return false; + + // Implement the _or condition from the GraphQL query + return ( + event.blockNumber > blockNumber || + (event.blockNumber === blockNumber && event.logIndex > logIndex) + ); + }) + .slice(0, limit); // Apply limit + + return { raw_events: filteredEvents }; + }, + ); + }); + it("returns events after the specified block number", async () => { const result = await envioIndexerClient.getEventsAfterBlockNumberAndLogIndex( 1 as ChainId, @@ -255,64 +265,65 @@ describe("EnvioIndexerClient", () => { }); }); - describe("getEventsBySrcAddress", () => { + describe("getEvents (old test cases)", () => { beforeEach(() => { - // Update the mock implementation for getEventsBySrcAddress queries + envioIndexerClient = new EnvioIndexerClient("http://example.com/graphql", "secret"); + graphqlClient = envioIndexerClient["client"] as unknown as Mocked; + + // Mock the request implementation to simulate database querying graphqlClient.request.mockImplementation( async ( _document: RequestDocument | RequestOptions, ...args: object[] ) => { - const variables = args[0] as { + const filters = args[0] as { chainId: ChainId; - srcAddresses: string[]; - fromBlock: number; - fromLogIndex: number; - toBlock: number; - toLogIndex: number; limit: number; + srcAddresses?: Address[]; + fromBlock?: number; + fromLogIndex?: number; + toBlock?: number; + toLogIndex?: number; }; - const { - chainId, - srcAddresses, - fromBlock, - fromLogIndex, - toBlock, - toLogIndex, - limit, - } = variables; + const { chainId, limit = 100, srcAddresses } = filters; + const { fromBlock, fromLogIndex, toBlock, toLogIndex } = filters; const filteredEvents = testEvents .filter((event) => { - // Match chainId and srcAddress if (event.chainId !== chainId) return false; - if (!srcAddresses.includes(event.srcAddress)) return false; - // Check if event is after fromBlock/fromLogIndex + if (srcAddresses && !srcAddresses.includes(event.srcAddress)) + return false; + const isAfterFrom = - event.blockNumber > fromBlock || - (event.blockNumber === fromBlock && event.logIndex > fromLogIndex); + fromBlock !== undefined && fromLogIndex !== undefined + ? event.blockNumber > fromBlock || + (event.blockNumber === fromBlock && + event.logIndex > fromLogIndex) + : true; - // Check if event is before or at toBlock/toLogIndex const isBeforeTo = - event.blockNumber < toBlock || - (event.blockNumber === toBlock && event.logIndex <= toLogIndex); + toBlock !== undefined && toLogIndex !== undefined + ? event.blockNumber < toBlock || + (event.blockNumber === toBlock && + event.logIndex <= toLogIndex) + : true; + // Implement the _or condition from the GraphQL query return isAfterFrom && isBeforeTo; }) - .slice(0, limit); + .slice(0, limit); // Apply limit return { raw_events: filteredEvents }; }, ); }); - it("returns events within the specified block range and matching srcAddresses", async () => { - const result = await envioIndexerClient.getEventsBySrcAddress({ + it("returns events after the specified block number", async () => { + const result = await envioIndexerClient.getEvents({ chainId: 1 as ChainId, - srcAddresses: ["0x1234"], from: { blockNumber: 100, logIndex: 0 }, - to: { blockNumber: 101, logIndex: 2 }, + limit: 100, }); expect(result).toHaveLength(3); @@ -325,96 +336,271 @@ describe("EnvioIndexerClient", () => { ); }); - it("uses default from values when not provided", async () => { - const result = await envioIndexerClient.getEventsBySrcAddress({ + it("returns only events after the specified log index within the same block", async () => { + const result = await envioIndexerClient.getEvents({ chainId: 1 as ChainId, - srcAddresses: ["0x1234"], - to: { blockNumber: 101, logIndex: 2 }, + from: { blockNumber: 100, logIndex: 2 }, + limit: 100, }); - expect(result).toHaveLength(3); - // Should include all events up to block 101, logIndex 2 - expect(result[0]?.blockNumber).toBe(100); - expect(result[2]?.blockNumber).toBe(101); + expect(result).toHaveLength(2); + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ blockNumber: 100, logIndex: 3 }), + expect.objectContaining({ blockNumber: 101, logIndex: 1 }), + ]), + ); }); - it("respects the limit parameter", async () => { - const result = await envioIndexerClient.getEventsBySrcAddress({ + it("returns events within the specified block range and matching srcAddresses", async () => { + const result = await envioIndexerClient.getEvents({ chainId: 1 as ChainId, srcAddresses: ["0x1234"], - to: { blockNumber: 101, logIndex: 2 }, - limit: 2, + from: { blockNumber: 100, logIndex: 0 }, }); expect(result).toHaveLength(2); + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ blockNumber: 100, logIndex: 1 }), + expect.objectContaining({ blockNumber: 100, logIndex: 3 }), + ]), + ); }); - it("uses default limit when not provided", async () => { - await envioIndexerClient.getEventsBySrcAddress({ + it("returns events within the specified block range", async () => { + const result = await envioIndexerClient.getEvents({ chainId: 1 as ChainId, - srcAddresses: ["0x1234"], - to: { blockNumber: 101, logIndex: 2 }, + from: { blockNumber: 100, logIndex: 5 }, + to: { blockNumber: 101, logIndex: 1 }, }); - expect(graphqlClient.request).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ limit: 100 }), + expect(result).toHaveLength(1); + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ blockNumber: 101, logIndex: 1 }), + ]), ); }); - it("returns empty array when no events match srcAddresses", async () => { - const result = await envioIndexerClient.getEventsBySrcAddress({ + it("respects the limit parameter", async () => { + const result = await envioIndexerClient.getEvents({ + chainId: 1 as ChainId, + from: { blockNumber: 100, logIndex: 0 }, + limit: 2, + }); + + expect(result).toHaveLength(2); + }); + + it("returns empty array when no matching events found", async () => { + const result = await envioIndexerClient.getEvents({ chainId: 1 as ChainId, - srcAddresses: ["0x9999"], - to: { blockNumber: 101, logIndex: 2 }, + from: { blockNumber: 102, logIndex: 0 }, + limit: 100, }); expect(result).toHaveLength(0); }); it("throws InvalidIndexerResponse when response structure is incorrect", async () => { - graphqlClient.request.mockResolvedValue({ status: 200, headers: {}, data: {} }); + const mockedResponse = { + status: 200, + headers: {}, + data: { + raw_events: undefined, + }, + }; + graphqlClient.request.mockResolvedValue(mockedResponse); await expect( - envioIndexerClient.getEventsBySrcAddress({ + envioIndexerClient.getEvents({ chainId: 1 as ChainId, - srcAddresses: ["0x1234"], - to: { blockNumber: 101, logIndex: 2 }, + from: { blockNumber: 12345, logIndex: 0 }, + limit: 100, }), ).rejects.toThrow(InvalidIndexerResponse); }); it("throws IndexerClientError when GraphQL request fails", async () => { - graphqlClient.request.mockRejectedValue(new Error("GraphQL request failed")); + const error = new Error("GraphQL request failed"); + graphqlClient.request.mockRejectedValue(error); await expect( - envioIndexerClient.getEventsBySrcAddress({ + envioIndexerClient.getEvents({ chainId: 1 as ChainId, - srcAddresses: ["0x1234"], - to: { blockNumber: 101, logIndex: 2 }, + from: { blockNumber: 12345, logIndex: 0 }, + limit: 100, }), ).rejects.toThrow(IndexerClientError); }); - it("filters events by multiple srcAddresses", async () => { - // Add a test event with a different srcAddress - const extraTestEvent = { - ...testEvents[0], - srcAddress: "0x5678", - blockNumber: 100, - logIndex: 2, - } as AnyIndexerFetchedEvent; - testEvents.push(extraTestEvent); - - const result = await envioIndexerClient.getEventsBySrcAddress({ + it("uses the default limit value when limit is not provided", async () => { + const mockedResponse = { + status: 200, + headers: {}, + raw_events: testEvents, + }; + graphqlClient.request.mockResolvedValue(mockedResponse); + + // Call the method without the limit argument + const result = await envioIndexerClient.getEvents({ + chainId: 1 as ChainId, + from: { blockNumber: 12345, logIndex: 0 }, + }); + + expect(result).toEqual(testEvents); + expect(graphqlClient.request).toHaveBeenCalledWith( + expect.any(String), // We can check the query string later if necessary + { + chainId: 1, + srcAddresses: undefined, + fromBlock: 12345, + fromLogIndex: 0, + toBlock: undefined, + toLogIndex: undefined, + limit: 100, // Ensure the default limit is used + }, + ); + }); + }); + + describe("where clause construction", () => { + let queryString: string; + let queryVars: Record; + + beforeEach(() => { + // Capture the query and variables for inspection + graphqlClient.request.mockImplementation( + async ( + _document: RequestDocument | RequestOptions, + ...args: object[] + ) => { + queryString = _document.toString(); + queryVars = args[0] as Record; + return { raw_events: [] }; + }, + ); + }); + + it("constructs basic where clause with only chainId", async () => { + await envioIndexerClient.getEvents({ + chainId: 1 as ChainId, + limit: 100, + }); + + expect(queryString).toContain("where: { chain_id: { _eq: $chainId } }"); + expect(queryVars).toEqual({ + chainId: 1, + limit: 100, + }); + }); + + it("constructs where clause with srcAddresses", async () => { + await envioIndexerClient.getEvents({ chainId: 1 as ChainId, srcAddresses: ["0x1234", "0x5678"], - from: { blockNumber: 100, logIndex: 0 }, - to: { blockNumber: 101, logIndex: 2 }, }); - expect(result).toContainEqual(expect.objectContaining({ srcAddress: "0x5678" })); - expect(result).toContainEqual(expect.objectContaining({ srcAddress: "0x1234" })); + expect(queryString).toContain( + "where: { _and: [{ chain_id: { _eq: $chainId } }, { src_address: { _in: $srcAddresses } }] }", + ); + expect(queryVars).toEqual({ + chainId: 1, + srcAddresses: ["0x1234", "0x5678"], + limit: 100, + }); + }); + + it("constructs where clause with from conditions", async () => { + await envioIndexerClient.getEvents({ + chainId: 1 as ChainId, + from: { blockNumber: 100, logIndex: 5 }, + }); + + expect(queryString).toContain("_or: ["); + expect(queryString).toContain(`{ block_number: { _gt: $fromBlock } },`); + expect(queryString).toContain(`{ _and: [`); + expect(queryString).toContain(`{ block_number: { _eq: $fromBlock } },`); + expect(queryString).toContain(`{ log_index: { _gt: $fromLogIndex } }`); + expect(queryVars).toEqual({ + chainId: 1, + fromBlock: 100, + fromLogIndex: 5, + limit: 100, + }); + }); + + it("constructs where clause with to conditions", async () => { + await envioIndexerClient.getEvents({ + chainId: 1 as ChainId, + to: { blockNumber: 200, logIndex: 10 }, + }); + + expect(queryString).toContain("block_number: { _lt: $toBlock }"); + expect(queryString).toContain("block_number: { _eq: $toBlock }"); + expect(queryString).toContain("log_index: { _lte: $toLogIndex }"); + expect(queryVars).toEqual({ + chainId: 1, + toBlock: 200, + toLogIndex: 10, + limit: 100, + }); + }); + + it("constructs complete where clause with all conditions", async () => { + await envioIndexerClient.getEvents({ + chainId: 1 as ChainId, + srcAddresses: ["0x1234"], + from: { blockNumber: 100, logIndex: 5 }, + to: { blockNumber: 200, logIndex: 10 }, + limit: 50, + }); + + // Check that all conditions are present + expect(queryString).toContain("chain_id: { _eq: $chainId }"); + expect(queryString).toContain("src_address: { _in: $srcAddresses }"); + expect(queryString).toContain("block_number: { _gt: $fromBlock }"); + expect(queryString).toContain("block_number: { _lt: $toBlock }"); + + // Check variables + expect(queryVars).toEqual({ + chainId: 1, + srcAddresses: ["0x1234"], + fromBlock: 100, + fromLogIndex: 5, + toBlock: 200, + toLogIndex: 10, + limit: 50, + }); + }); + + it("properly formats the query with whitespace and newlines", async () => { + await envioIndexerClient.getEvents({ + chainId: 1 as ChainId, + from: { blockNumber: 100, logIndex: 5 }, + }); + + // Remove all whitespace to make comparison easier + const normalizedQuery = queryString.replace(/\s+/g, " ").trim(); + + // Check that the query is properly formatted + expect(normalizedQuery).toContain( + "where: { _and: [{ chain_id: { _eq: $chainId } }, { _or: [", + ); + expect(normalizedQuery).not.toContain(",,"); // No double commas + expect(normalizedQuery).not.toContain("{{"); // No double braces + expect(normalizedQuery).not.toContain("}}"); // No double braces + }); + + it("handles empty optional parameters", async () => { + await envioIndexerClient.getEvents({ + chainId: 1 as ChainId, + srcAddresses: [], // Empty array + }); + + expect(queryString).toContain("where: { chain_id: { _eq: $chainId } }"); + expect(queryVars).not.toHaveProperty("srcAddresses"); }); }); }); From dd8e25e13295bee7ad1029fc05b3d7a7580fba29 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Wed, 18 Dec 2024 19:14:46 -0300 Subject: [PATCH 3/3] docs: update natspec --- packages/indexer-client/src/interfaces/indexerClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/indexer-client/src/interfaces/indexerClient.ts b/packages/indexer-client/src/interfaces/indexerClient.ts index efce730..7051923 100644 --- a/packages/indexer-client/src/interfaces/indexerClient.ts +++ b/packages/indexer-client/src/interfaces/indexerClient.ts @@ -21,7 +21,7 @@ export interface IIndexerClient { ): Promise; /** - * Get the events by src address from the indexer service + * Get the events by filters from the indexer service * @param params Filters to fetch events * @returns Events fetched from the indexer service */