diff --git a/.env.example b/.env.example index 61ebc6a..4823dba 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,31 @@ +###################################################### +############### DATALAYER POSTGRES ################### +###################################################### +DATALAYER_POSTGRES_PASSWORD=testing +DATALAYER_PG_USER=postgres +DATALAYER_PG_DATABASE=datalayer-postgres +DATALAYER_POSTGRES_EXPOSED_PORT=5434 + + +############################################################ +############### ENVIO POSTGRES & INDEXER ################### +############################################################ +ENVIO_POSTGRES_PASSWORD=testing +ENVIO_PG_USER=postgres +ENVIO_PG_DATABASE=envio-dev + ############################################ ############### POSTGRES ################### ############################################ -POSTGRES_EXPOSED_PORT=5433 -POSTGRES_PASSWORD=testing -POSTGRES_USER=postgres -POSTGRES_DB=envio-dev +ENVIO_POSTGRES_EXPOSED_PORT=5433 + +############################################ +############### INDEXER #################### +############################################ +ENVIO_PG_HOST=envio-postgres +ENVIO_PG_PORT=5432 +HASURA_GRAPHQL_ENDPOINT=http://graphql-engine:8080/v1/metadata +TUI_OFF=true ############################################ ############### HASURA ##################### @@ -17,15 +38,4 @@ HASURA_GRAPHQL_NO_OF_RETRIES=10 HASURA_GRAPHQL_ADMIN_SECRET=testing HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES="true" PORT=8080 -HASURA_GRAPHQL_UNAUTHORIZED_ROLE=public - -############################################ -############### INDEXER #################### -############################################ -ENVIO_PG_HOST=envio-postgres -ENVIO_PG_PORT=5432 -ENVIO_POSTGRES_PASSWORD=testing -ENVIO_PG_USER=postgres -ENVIO_PG_DATABASE=envio-dev -HASURA_GRAPHQL_ENDPOINT=http://graphql-engine:8080/v1/metadata -TUI_OFF=true \ No newline at end of file +HASURA_GRAPHQL_UNAUTHORIZED_ROLE=public \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 5ec4734..892e3fa 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,13 +1,32 @@ services: + datalayer-postgres: + image: postgres:16 + restart: always + ports: + - "${DATALAYER_POSTGRES_EXPOSED_PORT:-5432}:5432" + volumes: + - db_data:/var/lib/datalayer-postgresql/data + env_file: + - .env + environment: + POSTGRES_DB: ${DATALAYER_PG_DATABASE} + POSTGRES_USER: ${DATALAYER_PG_USER} + POSTGRES_PASSWORD: ${DATALAYER_POSTGRES_PASSWORD} + networks: + - datalayer envio-postgres: image: postgres:16 restart: always ports: - - "${POSTGRES_PORT:-5433}:5432" + - "${ENVIO_POSTGRES_EXPOSED_PORT:-5433}:5432" volumes: - - db_data:/var/lib/postgresql/data + - db_data:/var/lib/envio-postgresql/data env_file: - .env + environment: + POSTGRES_DB: ${ENVIO_PG_DATABASE} + POSTGRES_USER: ${ENVIO_PG_USER} + POSTGRES_PASSWORD: ${ENVIO_POSTGRES_PASSWORD} networks: - indexer-service graphql-engine: @@ -46,3 +65,5 @@ volumes: networks: indexer-service: name: indexer_test_network + datalayer: + name: datalayer_test_network diff --git a/packages/data-flow/README.md b/packages/data-flow/README.md new file mode 100644 index 0000000..65ce7fa --- /dev/null +++ b/packages/data-flow/README.md @@ -0,0 +1,45 @@ +# @grants-stack-indexer/data-flow + +Is a library that provides the core components of the processing pipeline for gitcoin grants-stack-indexer. + +## Available Scripts + +Available scripts that can be run using `pnpm`: + +| Script | Description | +| ------------- | ------------------------------------------------------- | +| `build` | Build library using tsc | +| `check-types` | Check types issues using tsc | +| `clean` | Remove `dist` folder | +| `lint` | Run ESLint to check for coding standards | +| `lint:fix` | Run linter and automatically fix code formatting issues | +| `format` | Check code formatting and style using Prettier | +| `format:fix` | Run formatter and automatically fix issues | +| `test` | Run tests using vitest | +| `test:cov` | Run tests with coverage report | + +## Usage + +### Importing the Package + +You can import the package in your TypeScript or JavaScript files as follows: + +```typescript +import { EventsFetcher } from "@grants-stack-indexer/data-flow"; +``` + +### Example + +```typescript +const eventsFetcher = new EventsFetcher(indexerClient); + +const chainId = 1; +const blockNumber = 1000; +const logIndex = 0; + +const result = await eventsFetcher.fetcEventsByBlockNumberAndLogIndex( + chainId, + blockNumber, + logIndex, +); +``` diff --git a/packages/data-flow/package.json b/packages/data-flow/package.json new file mode 100644 index 0000000..9d7d84f --- /dev/null +++ b/packages/data-flow/package.json @@ -0,0 +1,35 @@ +{ + "name": "@grants-stack-indexer/data-flow", + "version": "0.0.1", + "private": true, + "description": "", + "license": "MIT", + "author": "Wonderland", + "type": "module", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "directories": { + "src": "src" + }, + "files": [ + "dist/*", + "package.json", + "!**/*.tsbuildinfo" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "check-types": "tsc --noEmit -p ./tsconfig.json", + "clean": "rm -rf dist/", + "format": "prettier --check \"{src,test}/**/*.{js,ts,json}\"", + "format:fix": "prettier --write \"{src,test}/**/*.{js,ts,json}\"", + "lint": "eslint \"{src,test}/**/*.{js,ts,json}\"", + "lint:fix": "pnpm lint --fix", + "test": "vitest run --config vitest.config.ts --passWithNoTests", + "test:cov": "vitest run --config vitest.config.ts --coverage" + }, + "dependencies": { + "@grants-stack-indexer/indexer-client": "workspace:*", + "@grants-stack-indexer/shared": "workspace:*", + "viem": "2.21.19" + } +} diff --git a/packages/data-flow/src/eventsFetcher.ts b/packages/data-flow/src/eventsFetcher.ts new file mode 100644 index 0000000..cf83383 --- /dev/null +++ b/packages/data-flow/src/eventsFetcher.ts @@ -0,0 +1,22 @@ +import { IIndexerClient } from "@grants-stack-indexer/indexer-client"; +import { AnyProtocolEvent } from "@grants-stack-indexer/shared"; + +import { IEventsFetcher } from "./interfaces/index.js"; + +export class EventsFetcher implements IEventsFetcher { + constructor(private indexerClient: IIndexerClient) {} + /* @inheritdoc */ + async fetchEventsByBlockNumberAndLogIndex( + chainId: bigint, + blockNumber: bigint, + logIndex: number, + limit: number = 100, + ): Promise { + return await this.indexerClient.getEventsAfterBlockNumberAndLogIndex( + chainId, + blockNumber, + logIndex, + limit, + ); + } +} diff --git a/packages/data-flow/src/exceptions/index.ts b/packages/data-flow/src/exceptions/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/data-flow/src/external.ts b/packages/data-flow/src/external.ts new file mode 100644 index 0000000..f35e641 --- /dev/null +++ b/packages/data-flow/src/external.ts @@ -0,0 +1 @@ +export { EventsFetcher } from "./internal.js"; diff --git a/packages/data-flow/src/index.ts b/packages/data-flow/src/index.ts new file mode 100644 index 0000000..a5a2748 --- /dev/null +++ b/packages/data-flow/src/index.ts @@ -0,0 +1 @@ +export * from "./external.js"; diff --git a/packages/data-flow/src/interfaces/index.ts b/packages/data-flow/src/interfaces/index.ts new file mode 100644 index 0000000..a02c04e --- /dev/null +++ b/packages/data-flow/src/interfaces/index.ts @@ -0,0 +1,20 @@ +import { AnyProtocolEvent } from "@grants-stack-indexer/shared"; + +/** + * Interface for the events fetcher + */ +export interface IEventsFetcher { + /** + * Fetch the events by block number and log index for a chain + * @param chainId id of the chain + * @param blockNumber block number to fetch events from + * @param logIndex log index in the block to fetch events from + * @param limit limit of events to fetch + */ + fetchEventsByBlockNumberAndLogIndex( + chainId: bigint, + blockNumber: bigint, + logIndex: number, + limit?: number, + ): Promise; +} diff --git a/packages/data-flow/src/internal.ts b/packages/data-flow/src/internal.ts new file mode 100644 index 0000000..f1c91ab --- /dev/null +++ b/packages/data-flow/src/internal.ts @@ -0,0 +1 @@ +export * from "./eventsFetcher.js"; diff --git a/packages/data-flow/test/unit/eventsFetcher.spec.ts b/packages/data-flow/test/unit/eventsFetcher.spec.ts new file mode 100644 index 0000000..a547b5e --- /dev/null +++ b/packages/data-flow/test/unit/eventsFetcher.spec.ts @@ -0,0 +1,78 @@ +import { beforeEach, describe, expect, it, Mocked, vi } from "vitest"; + +import { IIndexerClient } from "@grants-stack-indexer/indexer-client"; +import { AnyProtocolEvent } from "@grants-stack-indexer/shared"; + +import { EventsFetcher } from "../../src/eventsFetcher.js"; + +describe("EventsFetcher", () => { + let indexerClientMock: Mocked; + let eventsFetcher: EventsFetcher; + + beforeEach(() => { + indexerClientMock = { + getEventsAfterBlockNumberAndLogIndex: vi.fn(), + }; + + eventsFetcher = new EventsFetcher(indexerClientMock); + }); + + it("should fetch events by block number and log index", async () => { + const mockEvents: AnyProtocolEvent[] = [ + { + chain_id: 1, + block_number: 12345, + block_timestamp: 123123123, + contract_name: "Allo", + event_name: "PoolCreated", + src_address: "0x1234567890123456789012345678901234567890", + log_index: 0, + params: { contractAddress: "0x1234" }, + }, + { + chain_id: 1, + block_number: 12345, + block_timestamp: 123123123, + contract_name: "Allo", + event_name: "PoolCreated", + src_address: "0x1234567890123456789012345678901234567890", + log_index: 0, + params: { contractAddress: "0x1234" }, + }, + ]; + const chainId = 1n; + const blockNumber = 1000n; + const logIndex = 0; + const limit = 100; + + indexerClientMock.getEventsAfterBlockNumberAndLogIndex.mockResolvedValue(mockEvents); + + const result = await eventsFetcher.fetchEventsByBlockNumberAndLogIndex( + chainId, + blockNumber, + logIndex, + ); + + expect(indexerClientMock.getEventsAfterBlockNumberAndLogIndex).toHaveBeenCalledWith( + chainId, + blockNumber, + logIndex, + limit, + ); + expect(result).toEqual(mockEvents); + }); + + it("should handle errors thrown by indexer client", async () => { + const chainId = 1n; + const blockNumber = 1000n; + const logIndex = 0; + + indexerClientMock.getEventsAfterBlockNumberAndLogIndex.mockRejectedValue( + new Error("Network error"), + ); + + await expect( + eventsFetcher.fetchEventsByBlockNumberAndLogIndex(chainId, blockNumber, logIndex), + ).rejects.toThrow("Network error"); + }); +}); diff --git a/packages/data-flow/tsconfig.build.json b/packages/data-flow/tsconfig.build.json new file mode 100644 index 0000000..56d3112 --- /dev/null +++ b/packages/data-flow/tsconfig.build.json @@ -0,0 +1,13 @@ +/* Based on total-typescript no-dom library config */ +/* https://github.com/total-typescript/tsconfig */ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "composite": true, + "declarationMap": true, + "declaration": true, + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/packages/data-flow/tsconfig.json b/packages/data-flow/tsconfig.json new file mode 100644 index 0000000..66bb87a --- /dev/null +++ b/packages/data-flow/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*"] +} diff --git a/packages/data-flow/vitest.config.ts b/packages/data-flow/vitest.config.ts new file mode 100644 index 0000000..8e1bbf4 --- /dev/null +++ b/packages/data-flow/vitest.config.ts @@ -0,0 +1,22 @@ +import path from "path"; +import { configDefaults, defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, // Use Vitest's global API without importing it in each file + environment: "node", // Use the Node.js environment + include: ["test/**/*.spec.ts"], // Include test files + exclude: ["node_modules", "dist"], // Exclude certain directories + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], // Coverage reporters + exclude: ["node_modules", "dist", "src/index.ts", ...configDefaults.exclude], // Files to exclude from coverage + }, + }, + resolve: { + alias: { + // Setup path alias based on tsconfig paths + "@": path.resolve(__dirname, "src"), + }, + }, +}); diff --git a/packages/indexer-client/README.md b/packages/indexer-client/README.md new file mode 100644 index 0000000..aa666be --- /dev/null +++ b/packages/indexer-client/README.md @@ -0,0 +1,36 @@ +# @grants-stack-indexer/indexer-client + +Is library for interacting with blockchain event indexing services. + +## Available Scripts + +Available scripts that can be run using `pnpm`: + +| Script | Description | +| ------------- | ------------------------------------------------------- | +| `build` | Build library using tsc | +| `check-types` | Check types issues using tsc | +| `clean` | Remove `dist` folder | +| `lint` | Run ESLint to check for coding standards | +| `lint:fix` | Run linter and automatically fix code formatting issues | +| `format` | Check code formatting and style using Prettier | +| `format:fix` | Run formatter and automatically fix issues | +| `test` | Run tests using vitest | +| `test:cov` | Run tests with coverage report | + +## Usage + +### Importing the Package + +You can import the package in your TypeScript or JavaScript files as follows: + +```typescript +import { EnvioIndexerClient } from "@grants-stack-indexer/indexer-client"; +``` + +### Example + +```typescript +const envioIndexerClient = new EnvioIndexerClient("http://example.com/graphql", "secret"); +await envioIndexerClient.getEventsByBlockNumberAndLogIndex(1, 12345, 0); +``` diff --git a/packages/indexer-client/package.json b/packages/indexer-client/package.json new file mode 100644 index 0000000..71b42b6 --- /dev/null +++ b/packages/indexer-client/package.json @@ -0,0 +1,34 @@ +{ + "name": "@grants-stack-indexer/indexer-client", + "version": "0.0.1", + "private": true, + "description": "", + "license": "MIT", + "author": "Wonderland", + "type": "module", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "directories": { + "src": "src" + }, + "files": [ + "dist/*", + "package.json", + "!**/*.tsbuildinfo" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "check-types": "tsc --noEmit -p ./tsconfig.json", + "clean": "rm -rf dist/", + "format": "prettier --check \"{src,test}/**/*.{js,ts,json}\"", + "format:fix": "prettier --write \"{src,test}/**/*.{js,ts,json}\"", + "lint": "eslint \"{src,test}/**/*.{js,ts,json}\"", + "lint:fix": "pnpm lint --fix", + "test": "vitest run --config vitest.config.ts --passWithNoTests", + "test:cov": "vitest run --config vitest.config.ts --coverage" + }, + "dependencies": { + "@grants-stack-indexer/shared": "workspace:*", + "graphql-request": "7.1.0" + } +} diff --git a/packages/indexer-client/src/exceptions/index.ts b/packages/indexer-client/src/exceptions/index.ts new file mode 100644 index 0000000..40a80c4 --- /dev/null +++ b/packages/indexer-client/src/exceptions/index.ts @@ -0,0 +1,2 @@ +export * from "./invalidIndexerResponse.exception.js"; +export * from "./indexerClientError.exception.js"; diff --git a/packages/indexer-client/src/exceptions/indexerClientError.exception.ts b/packages/indexer-client/src/exceptions/indexerClientError.exception.ts new file mode 100644 index 0000000..9a9a6a9 --- /dev/null +++ b/packages/indexer-client/src/exceptions/indexerClientError.exception.ts @@ -0,0 +1,6 @@ +export class IndexerClientError extends Error { + constructor(message: string) { + super(`Indexer client error - ${message}`); + this.name = "IndexerClientError"; + } +} diff --git a/packages/indexer-client/src/exceptions/invalidIndexerResponse.exception.ts b/packages/indexer-client/src/exceptions/invalidIndexerResponse.exception.ts new file mode 100644 index 0000000..01b38ee --- /dev/null +++ b/packages/indexer-client/src/exceptions/invalidIndexerResponse.exception.ts @@ -0,0 +1,6 @@ +export class InvalidIndexerResponse extends Error { + constructor(response: string) { + super(`Indexer response is invalid - ${response}`); + this.name = "InvalidIndexerResponse"; + } +} diff --git a/packages/indexer-client/src/external.ts b/packages/indexer-client/src/external.ts new file mode 100644 index 0000000..ba0a97f --- /dev/null +++ b/packages/indexer-client/src/external.ts @@ -0,0 +1,3 @@ +export type { IIndexerClient } from "./internal.js"; + +export { EnvioIndexerClient } from "./internal.js"; diff --git a/packages/indexer-client/src/index.ts b/packages/indexer-client/src/index.ts new file mode 100644 index 0000000..a5a2748 --- /dev/null +++ b/packages/indexer-client/src/index.ts @@ -0,0 +1 @@ +export * from "./external.js"; diff --git a/packages/indexer-client/src/interfaces/index.ts b/packages/indexer-client/src/interfaces/index.ts new file mode 100644 index 0000000..d109a80 --- /dev/null +++ b/packages/indexer-client/src/interfaces/index.ts @@ -0,0 +1 @@ +export * from "./indexerClient.js"; diff --git a/packages/indexer-client/src/interfaces/indexerClient.ts b/packages/indexer-client/src/interfaces/indexerClient.ts new file mode 100644 index 0000000..f1fca99 --- /dev/null +++ b/packages/indexer-client/src/interfaces/indexerClient.ts @@ -0,0 +1,20 @@ +import { AnyProtocolEvent } from "@grants-stack-indexer/shared"; + +/** + * Interface for the indexer client + */ +export interface IIndexerClient { + /** + * Get the events by block number and log index from the indexer service + * @param chainId Id of the chain + * @param fromBlock Block number to start fetching events from + * @param logIndex Log index in the block + * @param limit Limit of events to fetch + */ + getEventsAfterBlockNumberAndLogIndex( + chainId: bigint, + fromBlock: bigint, + logIndex: number, + limit?: number, + ): Promise; +} diff --git a/packages/indexer-client/src/internal.ts b/packages/indexer-client/src/internal.ts new file mode 100644 index 0000000..092eb9a --- /dev/null +++ b/packages/indexer-client/src/internal.ts @@ -0,0 +1,3 @@ +export * from "./exceptions/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 new file mode 100644 index 0000000..8c210f4 --- /dev/null +++ b/packages/indexer-client/src/providers/envioIndexerClient.ts @@ -0,0 +1,67 @@ +import { gql, GraphQLClient } from "graphql-request"; + +import { AnyProtocolEvent } from "@grants-stack-indexer/shared"; + +import { IndexerClientError, InvalidIndexerResponse } from "../exceptions/index.js"; +import { IIndexerClient } from "../internal.js"; + +/** + * Indexer client for the Envio indexer service + */ +export class EnvioIndexerClient implements IIndexerClient { + private client: GraphQLClient; + + constructor(url: string, secret: string) { + this.client = new GraphQLClient(url); + this.client.setHeader("x-hasura-admin-secret", secret); + } + /* @inheritdoc */ + public async getEventsAfterBlockNumberAndLogIndex( + chainId: bigint, + blockNumber: bigint, + logIndex: number, + limit: number = 100, + ): Promise { + try { + const response = (await this.client.request( + gql` + query getEventsAfterBlockNumberAndLogIndex( + $chainId: Int! + $blockNumber: Int! + $logIndex: Int! + $limit: Int! + ) { + raw_events( + where: { + chain_id: { _eq: $chainId } + block_number: { _gte: $blockNumber } + log_index: { _gt: $logIndex } + } + limit: $limit + ) { + block_number: blockNumber + block_timestamp: blockTimestamp + chain_id: chainId + contract_name: contractName + event_name: eventName + log_index: logIndex + params + src_address: srcAddress + } + } + `, + { chainId, blockNumber, logIndex, limit }, + )) as { data: { raw_events: AnyProtocolEvent[] } }; + if (response?.data?.raw_events) { + return response.data.raw_events; + } else { + throw new InvalidIndexerResponse(JSON.stringify(response)); + } + } catch (error) { + if (error instanceof InvalidIndexerResponse) { + throw error; + } + throw new IndexerClientError(JSON.stringify(error)); + } + } +} diff --git a/packages/indexer-client/src/providers/index.ts b/packages/indexer-client/src/providers/index.ts new file mode 100644 index 0000000..740ac8d --- /dev/null +++ b/packages/indexer-client/src/providers/index.ts @@ -0,0 +1 @@ +export * from "./envioIndexerClient.js"; diff --git a/packages/indexer-client/test/unit/envioIndexerClient.spec.ts b/packages/indexer-client/test/unit/envioIndexerClient.spec.ts new file mode 100644 index 0000000..1a9abb8 --- /dev/null +++ b/packages/indexer-client/test/unit/envioIndexerClient.spec.ts @@ -0,0 +1,148 @@ +import { GraphQLClient } from "graphql-request"; +import { afterEach, beforeEach, describe, expect, it, Mocked, vi } from "vitest"; + +import { AnyProtocolEvent } from "@grants-stack-indexer/shared"; + +import { IndexerClientError, InvalidIndexerResponse } from "../../src/exceptions/index.js"; +import { EnvioIndexerClient } from "../../src/providers/envioIndexerClient.js"; + +// Mock GraphQLClient +vi.mock("graphql-request", async (importOriginal) => { + const mod: object = await importOriginal(); + return { + ...mod, + GraphQLClient: vi.fn().mockImplementation(() => ({ + setHeader: vi.fn(), + request: vi.fn(), + })), + }; +}); + +describe("EnvioIndexerClient", () => { + let envioIndexerClient: EnvioIndexerClient; + let graphqlClient: Mocked; + + beforeEach(() => { + envioIndexerClient = new EnvioIndexerClient("http://example.com/graphql", "secret"); + graphqlClient = envioIndexerClient["client"] as unknown as Mocked; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("constructor", () => { + it("creates a GraphQLClient with the provided URL", () => { + expect(GraphQLClient).toHaveBeenCalledWith("http://example.com/graphql"); + }); + + it("sets the x-hasura-admin-secret header", () => { + expect(graphqlClient.setHeader).toHaveBeenCalledWith("x-hasura-admin-secret", "secret"); + }); + }); + + describe("getEventsAfterBlockNumberAndLogIndex", () => { + const mockEvents: AnyProtocolEvent[] = [ + { + chainId: 1, + blockNumber: 12345, + blockTimestamp: 123123123, + contractName: "Allo", + eventName: "PoolCreated", + srcAddress: "0x1234567890123456789012345678901234567890", + logIndex: 0, + params: { contractAddress: "0x1234" }, + }, + ]; + + it("returns events when the query is successful", async () => { + const mockedResponse = { + status: 200, + headers: {}, + data: { + raw_events: mockEvents, + }, + }; + graphqlClient.request.mockResolvedValue(mockedResponse); + + const result = await envioIndexerClient.getEventsAfterBlockNumberAndLogIndex( + 1n, + 12345n, + 0, + 100, + ); + expect(result).toEqual(mockEvents); + }); + + it("throws InvalidIndexerResponse when response structure is incorrect", async () => { + const mockedResponse = { + status: 200, + headers: {}, + data: { + raw_events: undefined, + }, + }; + graphqlClient.request.mockResolvedValue(mockedResponse); + + await expect( + envioIndexerClient.getEventsAfterBlockNumberAndLogIndex(1n, 12345n, 0), + ).rejects.toThrow(InvalidIndexerResponse); + }); + + it("throws IndexerClientError when GraphQL request fails", async () => { + const error = new Error("GraphQL request failed"); + graphqlClient.request.mockRejectedValue(error); + + await expect( + envioIndexerClient.getEventsAfterBlockNumberAndLogIndex(1n, 12345n, 0), + ).rejects.toThrow(IndexerClientError); + }); + + it("uses the default limit value when limit is not provided", async () => { + const mockedResponse = { + status: 200, + headers: {}, + data: { + raw_events: mockEvents, + }, + }; + graphqlClient.request.mockResolvedValue(mockedResponse); + + // Call the method without the limit argument + const result = await envioIndexerClient.getEventsAfterBlockNumberAndLogIndex( + 1n, + 12345n, + 0, + ); + + expect(result).toEqual(mockEvents); + expect(graphqlClient.request).toHaveBeenCalledWith( + expect.any(String), // We can check the query string later if necessary + { + chainId: 1n, + blockNumber: 12345n, + logIndex: 0, + limit: 100, // Ensure the default limit is used + }, + ); + }); + + it("returns an empty array when no events are found", async () => { + const mockedResponse = { + status: 200, + headers: {}, + data: { + raw_events: [], + }, + }; + graphqlClient.request.mockResolvedValue(mockedResponse); + + const result = await envioIndexerClient.getEventsAfterBlockNumberAndLogIndex( + 1n, + 12345n, + 0, + ); + expect(result).toEqual([]); + }); + }); +}); diff --git a/packages/indexer-client/tsconfig.build.json b/packages/indexer-client/tsconfig.build.json new file mode 100644 index 0000000..56d3112 --- /dev/null +++ b/packages/indexer-client/tsconfig.build.json @@ -0,0 +1,13 @@ +/* Based on total-typescript no-dom library config */ +/* https://github.com/total-typescript/tsconfig */ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "composite": true, + "declarationMap": true, + "declaration": true, + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/packages/indexer-client/tsconfig.json b/packages/indexer-client/tsconfig.json new file mode 100644 index 0000000..66bb87a --- /dev/null +++ b/packages/indexer-client/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*"] +} diff --git a/packages/indexer-client/vitest.config.ts b/packages/indexer-client/vitest.config.ts new file mode 100644 index 0000000..8e1bbf4 --- /dev/null +++ b/packages/indexer-client/vitest.config.ts @@ -0,0 +1,22 @@ +import path from "path"; +import { configDefaults, defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, // Use Vitest's global API without importing it in each file + environment: "node", // Use the Node.js environment + include: ["test/**/*.spec.ts"], // Include test files + exclude: ["node_modules", "dist"], // Exclude certain directories + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], // Coverage reporters + exclude: ["node_modules", "dist", "src/index.ts", ...configDefaults.exclude], // Files to exclude from coverage + }, + }, + resolve: { + alias: { + // Setup path alias based on tsconfig paths + "@": path.resolve(__dirname, "src"), + }, + }, +}); diff --git a/packages/shared/src/types/events/common.ts b/packages/shared/src/types/events/common.ts index e64fb7e..3b4921b 100644 --- a/packages/shared/src/types/events/common.ts +++ b/packages/shared/src/types/events/common.ts @@ -30,15 +30,15 @@ export type EventParams * This type is used to represent a protocol event. */ export type ProtocolEvent> = { - block_number: number; - block_timestamp: number; - chain_id: number; - contract_name: T; - event_id: string; - event_name: E; - log_index: number; + //TODO: make blocknumber and chainId bigints, implies implementing adapter patterns in the EventsFetcher or IndexerClient + blockNumber: number; + blockTimestamp: number; + chainId: number; + contractName: T; + eventName: E; + logIndex: number; params: EventParams; - src_address: Address; + srcAddress: Address; }; export type AnyProtocolEvent = ProtocolEvent>; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6eee7cb..6790b90 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,27 @@ importers: specifier: 5.2.2 version: 5.2.2 + packages/data-flow: + dependencies: + "@grants-stack-indexer/indexer-client": + specifier: workspace:* + version: link:../indexer-client + "@grants-stack-indexer/shared": + specifier: workspace:* + version: link:../shared + viem: + specifier: 2.21.19 + version: 2.21.19(typescript@5.5.4)(zod@3.23.8) + + packages/indexer-client: + dependencies: + "@grants-stack-indexer/shared": + specifier: workspace:* + version: link:../shared + graphql-request: + specifier: 7.1.0 + version: 7.1.0(graphql@16.9.0) + packages/metadata: dependencies: "@grants-stack-indexer/shared": @@ -659,6 +680,14 @@ packages: } engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + "@graphql-typed-document-node/core@3.2.0": + resolution: + { + integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==, + } + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + "@humanwhocodes/config-array@0.11.14": resolution: { @@ -746,6 +775,18 @@ packages: integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==, } + "@molt/command@0.9.0": + resolution: + { + integrity: sha512-1JI8dAlpqlZoXyKWVQggX7geFNPxBpocHIXQCsnxDjKy+3WX4SGyZVJXuLlqRRrX7FmQCuuMAfx642ovXmPA9g==, + } + + "@molt/types@0.2.0": + resolution: + { + integrity: sha512-p6ChnEZDGjg9PYPec9BK6Yp5/DdSrYQvXTBAtgrnqX6N36cZy37ql1c8Tc5LclfIYBNG7EZp8NBcRTYJwyi84g==, + } + "@noble/curves@1.2.0": resolution: { @@ -1226,6 +1267,12 @@ packages: integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==, } + alge@0.8.1: + resolution: + { + integrity: sha512-kiV9nTt+XIauAXsowVygDxMZLplZxDWt0W8plE/nB32/V2ziM/P/TxDbSVK7FYIUt2Xo16h3/htDh199LNPCKQ==, + } + ansi-colors@4.1.1: resolution: { @@ -2247,6 +2294,32 @@ packages: integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==, } + graphql-request@7.1.0: + resolution: + { + integrity: sha512-Ouu/lYVFhARS1aXeZoVJWnGT6grFJXTLwXJuK4mUGGRo0EUk1JkyYp43mdGmRgUVezpRm6V5Sq3t8jBDQcajng==, + } + hasBin: true + peerDependencies: + "@dprint/formatter": ^0.3.0 + "@dprint/typescript": ^0.91.1 + dprint: ^0.46.2 + graphql: 14 - 16 + peerDependenciesMeta: + "@dprint/formatter": + optional: true + "@dprint/typescript": + optional: true + dprint: + optional: true + + graphql@16.9.0: + resolution: + { + integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==, + } + engines: { node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0 } + has-flag@3.0.0: resolution: { @@ -2643,6 +2716,12 @@ packages: integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==, } + lodash.ismatch@4.4.0: + resolution: + { + integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==, + } + lodash.isplainobject@4.0.6: resolution: { @@ -3276,6 +3355,19 @@ packages: } engines: { node: ">=8.10.0" } + readline-sync@1.4.10: + resolution: + { + integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==, + } + engines: { node: ">= 0.8.0" } + + remeda@1.61.0: + resolution: + { + integrity: sha512-caKfSz9rDeSKBQQnlJnVW3mbVdFgxgGWQKq1XlFokqjf+hQD5gxutLGTTY2A/x24UxVyJe9gH5fAkFI63ULw4A==, + } + require-directory@2.1.1: resolution: { @@ -3487,6 +3579,13 @@ packages: } engines: { node: ">=0.6.19" } + string-length@6.0.0: + resolution: + { + integrity: sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==, + } + engines: { node: ">=16" } + string-width@4.2.3: resolution: { @@ -3682,6 +3781,12 @@ packages: engines: { node: ">=4.2.0" } hasBin: true + ts-toolbelt@9.6.0: + resolution: + { + integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==, + } + tsconfig-paths@3.15.0: resolution: { @@ -3776,6 +3881,13 @@ packages: } engines: { node: ">=10" } + type-fest@4.26.1: + resolution: + { + integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==, + } + engines: { node: ">=16" } + typescript@5.2.2: resolution: { @@ -4427,6 +4539,10 @@ snapshots: "@eslint/js@8.56.0": {} + "@graphql-typed-document-node/core@3.2.0(graphql@16.9.0)": + dependencies: + graphql: 16.9.0 + "@humanwhocodes/config-array@0.11.14": dependencies: "@humanwhocodes/object-schema": 2.0.3 @@ -4484,6 +4600,24 @@ snapshots: "@jridgewell/resolve-uri": 3.1.2 "@jridgewell/sourcemap-codec": 1.5.0 + "@molt/command@0.9.0": + dependencies: + "@molt/types": 0.2.0 + alge: 0.8.1 + chalk: 5.3.0 + lodash.camelcase: 4.3.0 + lodash.snakecase: 4.1.1 + readline-sync: 1.4.10 + string-length: 6.0.0 + strip-ansi: 7.1.0 + ts-toolbelt: 9.6.0 + type-fest: 4.26.1 + zod: 3.23.8 + + "@molt/types@0.2.0": + dependencies: + ts-toolbelt: 9.6.0 + "@noble/curves@1.2.0": dependencies: "@noble/hashes": 1.3.2 @@ -4779,6 +4913,13 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + alge@0.8.1: + dependencies: + lodash.ismatch: 4.4.0 + remeda: 1.61.0 + ts-toolbelt: 9.6.0 + zod: 3.23.8 + ansi-colors@4.1.1: {} ansi-escapes@7.0.0: @@ -5396,6 +5537,15 @@ snapshots: graphemer@1.4.0: {} + graphql-request@7.1.0(graphql@16.9.0): + dependencies: + "@graphql-typed-document-node/core": 3.2.0(graphql@16.9.0) + "@molt/command": 0.9.0 + graphql: 16.9.0 + zod: 3.23.8 + + graphql@16.9.0: {} + has-flag@3.0.0: {} has-flag@4.0.0: {} @@ -5579,6 +5729,8 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.ismatch@4.4.0: {} + lodash.isplainobject@4.0.6: {} lodash.kebabcase@4.1.1: {} @@ -5899,6 +6051,10 @@ snapshots: dependencies: picomatch: 2.3.1 + readline-sync@1.4.10: {} + + remeda@1.61.0: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -6010,6 +6166,10 @@ snapshots: string-argv@0.3.2: {} + string-length@6.0.0: + dependencies: + strip-ansi: 7.1.0 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -6126,6 +6286,8 @@ snapshots: source-map-support: 0.5.21 yn: 2.0.0 + ts-toolbelt@9.6.0: {} + tsconfig-paths@3.15.0: dependencies: "@types/json5": 0.0.29 @@ -6173,6 +6335,8 @@ snapshots: type-fest@0.20.2: {} + type-fest@4.26.1: {} + typescript@5.2.2: {} typescript@5.5.4: {}