From 8d06292d37f1d661421e8851cbc94ab647c83697 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:54:37 -0300 Subject: [PATCH] feat: multichain support (#38) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🤖 Linear Closes GIT-177 GIT-178 GIT-179 ## Description - index remaining chains on Indexer app - enable multichain support on Processing app - fix InMemoryEventsRegistry to save events per chain - move out from Singleton pattern for logger as we want to log which Chain Orchestrator's makes a log ## Checklist before requesting a review - [x] I have conducted a self-review of my code. - [x] I have conducted a QA. - [x] If it is a core feature, I have included comprehensive tests. --- apps/indexer/config.yaml | 240 ++++++++-------- apps/indexer/package.json | 2 +- apps/indexer/pnpm-lock.yaml | 49 ++-- apps/processing/.env.example | 7 +- apps/processing/README.md | 5 +- apps/processing/src/config/env.ts | 17 +- .../src/services/processing.service.ts | 47 ++-- .../services/sharedDependencies.service.ts | 21 +- .../test/unit/processing.service.spec.ts | 151 ++++++++++ .../unit/sharedDependencies.service.spec.ts | 93 +++++++ apps/processing/tsconfig.json | 2 +- packages/data-flow/src/eventsRegistry.ts | 23 +- .../interfaces/eventsRegistry.interface.ts | 13 +- packages/data-flow/src/orchestrator.ts | 6 +- .../test/unit/eventsRegistry.spec.ts | 13 +- .../data-flow/test/unit/orchestrator.spec.ts | 20 +- packages/shared/src/logger/logger.ts | 23 +- pnpm-lock.yaml | 263 ++++++++++++++++-- 18 files changed, 758 insertions(+), 237 deletions(-) create mode 100644 apps/processing/test/unit/processing.service.spec.ts create mode 100644 apps/processing/test/unit/sharedDependencies.service.spec.ts diff --git a/apps/indexer/config.yaml b/apps/indexer/config.yaml index befb356..59b2dd4 100644 --- a/apps/indexer/config.yaml +++ b/apps/indexer/config.yaml @@ -122,16 +122,16 @@ networks: ####################### # MAINNET # ####################### - # - id: 1 # mainnet - # start_block: 18486688 - # contracts: - # - name: Allo - # address: - # - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 - # - name: Strategy - # - name: Registry - # address: - # - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 + - id: 1 # mainnet + start_block: 18486688 + contracts: + - name: Allo + address: + - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 + - name: Strategy + - name: Registry + address: + - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 - id: 10 # optimism start_block: 111678968 @@ -144,93 +144,82 @@ networks: address: - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 - # - id: 11155111 # sepolia - # start_block: 4617051 - # contracts: - # - name: Registry - # address: - # - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 - # - name: Strategy - # - name: Allo - # address: - # - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 - - # - id: 250 # fantom - # start_block: 77624278 - # contracts: - # - name: Registry - # address: - # - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 - # - name: Strategy - # - name: Allo - # address: - # - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 - - # - id: 42161 # arbitrum - # start_block: 146489425 - # contracts: - # - name: Registry - # address: - # - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 - # - name: Strategy - # - name: Allo - # address: - # - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 - - # - id: 137 # polygon - # start_block: 49466006 - # contracts: - # - name: Registry - # address: - # - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 - # - name: Strategy - # - name: Allo - # address: - # - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 - - # - id: 8453 # base - # start_block: 6083365 - # contracts: - # - name: Registry - # address: - # - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 - # - name: Strategy - # - name: Allo - # address: - # - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 - - # - id: 324 # zksync-era-mainnet - # start_block: 31154341 - # contracts: - # - name: Registry - # address: - # - 0xaa376Ef759c1f5A8b0B5a1e2FEC5C23f3bF30246 - # - name: Strategy - # - name: Allo - # address: - # - 0x9D1D1BF2835935C291C0f5228c86d5C4e235A249 - - # - id: 43114 # avalanche - # start_block: 34540051 - # contracts: - # - name: Registry - # address: - # - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 - # - name: Strategy - # - name: Allo - # address: - # - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 - - # - id: 534352 # scroll - # start_block: 2683205 - # contracts: - # - name: Registry - # address: - # - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 - # - name: Strategy - # - name: Allo - # address: - # - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 + - id: 250 # fantom + start_block: 77624278 + contracts: + - name: Registry + address: + - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 + - name: Strategy + - name: Allo + address: + - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 + + - id: 42161 # arbitrum + start_block: 146489425 + contracts: + - name: Registry + address: + - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 + - name: Strategy + - name: Allo + address: + - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 + + - id: 137 # polygon + start_block: 49466006 + contracts: + - name: Registry + address: + - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 + - name: Strategy + - name: Allo + address: + - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 + + - id: 8453 # base + start_block: 6083365 + contracts: + - name: Registry + address: + - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 + - name: Strategy + - name: Allo + address: + - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 + + - id: 324 # zksync-era-mainnet + start_block: 31154341 + contracts: + - name: Registry + address: + - 0xaa376Ef759c1f5A8b0B5a1e2FEC5C23f3bF30246 + - name: Strategy + - name: Allo + address: + - 0x9D1D1BF2835935C291C0f5228c86d5C4e235A249 + + - id: 43114 # avalanche + start_block: 34540051 + contracts: + - name: Registry + address: + - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 + - name: Strategy + - name: Allo + address: + - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 + + - id: 534352 # scroll + start_block: 2683205 + contracts: + - name: Registry + address: + - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 + - name: Strategy + - name: Allo + address: + - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 # - id: 1329 # sei-mainnet # rpc_config: @@ -245,39 +234,50 @@ networks: # address: # - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 - # - id: 42220 # celo-mainnet - # start_block: 22257475 - # contracts: - # - name: Registry - # address: - # - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 - # - name: Strategy - # - name: Allo - # address: - # - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 - - # - id: 42 # lukso-mainnet - # start_block: 2400000 - # contracts: - # - name: Registry - # address: - # - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 - # - name: Strategy - # - name: Allo - # address: - # - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 + - id: 42220 # celo-mainnet + start_block: 22257475 + contracts: + - name: Registry + address: + - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 + - name: Strategy + - name: Allo + address: + - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 + + - id: 42 # lukso-mainnet + start_block: 2400000 + contracts: + - name: Registry + address: + - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 + - name: Strategy + - name: Allo + address: + - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 ####################### # TESTNET # ####################### +# - id: 11155111 # sepolia +# start_block: 4617051 +# contracts: +# - name: Registry +# address: +# - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 +# - name: Strategy +# - name: Allo +# address: +# - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 + # - id: 80001 # polygon-mumbai # start_block: 41939383 # contracts: # - name: Registry # address: # - 0x4AAcca72145e1dF2aeC137E1f3C5E3D75DB8b5f3 -# - name: Strategy -# +# - name: Strategy + # - name: Allo # address: # - 0x1133eA7Af70876e64665ecD07C0A0476d09465a1 diff --git a/apps/indexer/package.json b/apps/indexer/package.json index bf053b3..da4642a 100644 --- a/apps/indexer/package.json +++ b/apps/indexer/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "chai": "4.3.10", - "envio": "2.7.3", + "envio": "2.8.2", "ethers": "6.8.0", "yaml": "2.5.1" }, diff --git a/apps/indexer/pnpm-lock.yaml b/apps/indexer/pnpm-lock.yaml index 29157c5..78f0f31 100644 --- a/apps/indexer/pnpm-lock.yaml +++ b/apps/indexer/pnpm-lock.yaml @@ -11,8 +11,8 @@ importers: specifier: 4.3.10 version: 4.3.10 envio: - specifier: 2.7.3 - version: 2.7.3 + specifier: 2.8.2 + version: 2.8.2(typescript@5.2.2) ethers: specifier: 6.8.0 version: 6.8.0 @@ -895,42 +895,42 @@ packages: integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==, } - envio-darwin-arm64@2.7.3: + envio-darwin-arm64@2.8.2: resolution: { - integrity: sha512-jFtqTbGRXlYBvwQJ2iEZwsumGy+19y7e4+OjPM5ZqqoJ1sHNQ70/G1AqnvZYFKD8RuTUN+5ArU5gYc0Tgqu8Ow==, + integrity: sha512-6FYf8/6qKRpdh8BuhylYtWsv/xW2Q1vkL9hYgy/VB0K1nMTBIfY83dDYDy2wHYSxg+R6gvawrI66L2bJ4rZn6g==, } cpu: [arm64] os: [darwin] - envio-darwin-x64@2.7.3: + envio-darwin-x64@2.8.2: resolution: { - integrity: sha512-yT7krrqRENpNYvCtyFMlPRGDsUSO0nvrTX0fl28f8xJyD/HAF0nKnU8f6FBiyWCzLzPh8Uby1qK3l+hUVhcktQ==, + integrity: sha512-t58SvMyyw3VOXW9CWrVkmxWDO7zACd56x5/sDZWFzcgGd0nOOYMkz2Gzag2VawsqqlZC0iyLQPE4HZiPPCcYZw==, } cpu: [x64] os: [darwin] - envio-linux-arm64@2.7.3: + envio-linux-arm64@2.8.2: resolution: { - integrity: sha512-QCJlypEOw3cc93UVkRvYuF9Z3x/n57usb0P85Mz0ADiykxBBd7QWyEFKgmMzQZlLZfgQ0NLHwcPh+92/bGoL/A==, + integrity: sha512-dMBRfCUB9S2fLAi88j3Fd8aftnwA4MGn5/VR1wK7VVtLFCCRKEjtNJqVyclt72UNYBw5wuhLei9U3KSiemAlDQ==, } cpu: [arm64] os: [linux] - envio-linux-x64@2.7.3: + envio-linux-x64@2.8.2: resolution: { - integrity: sha512-IRpfHZhlx7dTmtROhuVJONO7otPlwaUmFdb2HEycwgb4hN1+6Xadn5lPuLYmqJFSWgd2lyxn5EdAJWw8KV4qtA==, + integrity: sha512-u8tyNNbrEM8kbuqAy5YxNTOnpAg72DAY+5S0rxOv47RGAr9fXhmbo1EUBVwlV+Us6wz7gdcTtSId2e55Z/AzeQ==, } cpu: [x64] os: [linux] - envio@2.7.3: + envio@2.8.2: resolution: { - integrity: sha512-j/G4Gg+g0xUUf0RgH3PKo0REXwh2blB/t7kYEPFZy9EX3/lOw8fG076ZN7rJ82FzzOjVsQH6RSeW6OFGGWgnLw==, + integrity: sha512-Cg3O/1I78PPF1S3sV3ZRp1RAiaSFX2CKMFLcchbAgF7m1y/w9+OZKY4O0lhFCMNHLwmKyThnhfJI0JWxTEKs3g==, } hasBin: true @@ -2949,27 +2949,34 @@ snapshots: dependencies: once: 1.4.0 - envio-darwin-arm64@2.7.3: + envio-darwin-arm64@2.8.2: optional: true - envio-darwin-x64@2.7.3: + envio-darwin-x64@2.8.2: optional: true - envio-linux-arm64@2.7.3: + envio-linux-arm64@2.8.2: optional: true - envio-linux-x64@2.7.3: + envio-linux-x64@2.8.2: optional: true - envio@2.7.3: + envio@2.8.2(typescript@5.2.2): dependencies: + "@envio-dev/hypersync-client": 0.6.2 rescript: 11.1.3 rescript-schema: 8.2.0(rescript@11.1.3) + viem: 2.21.0(typescript@5.2.2) optionalDependencies: - envio-darwin-arm64: 2.7.3 - envio-darwin-x64: 2.7.3 - envio-linux-arm64: 2.7.3 - envio-linux-x64: 2.7.3 + envio-darwin-arm64: 2.8.2 + envio-darwin-x64: 2.8.2 + envio-linux-arm64: 2.8.2 + envio-linux-x64: 2.8.2 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod es-define-property@1.0.0: dependencies: diff --git a/apps/processing/.env.example b/apps/processing/.env.example index 79ded2e..591b483 100644 --- a/apps/processing/.env.example +++ b/apps/processing/.env.example @@ -1,9 +1,6 @@ -RPC_URLS=["https://optimism.llamarpc.com","https://rpc.ankr.com/optimism","https://optimism.gateway.tenderly.co","https://optimism.blockpi.network/v1/rpc/public","https://mainnet.optimism.io","https://opt-mainnet.g.alchemy.com/v2/demo"] -CHAIN_ID=10 -LOG_LEVEL=debug +CHAINS=[{"id":10,"name":"optimism","rpcUrls":["https://optimism.llamarpc.com","https://rpc.ankr.com/optimism","https://optimism.gateway.tenderly.co","https://optimism.blockpi.network/v1/rpc/public","https://mainnet.optimism.io","https://opt-mainnet.g.alchemy.com/v2/demo"]},{"id":1,"name":"mainnet","rpcUrls":["https://eth.llamarpc.com","https://rpc.flashbots.net/fast"],"fetchLimit":1000,"fetchDelayMs":30000}] -FETCH_LIMIT=10000 -FETCH_DELAY_MS=30000 +LOG_LEVEL=debug DATABASE_URL=postgresql://postgres:testing@localhost:5434/datalayer-postgres-db DATABASE_SCHEMA=chain_data_schema_1 diff --git a/apps/processing/README.md b/apps/processing/README.md index 7547863..96f7b4f 100644 --- a/apps/processing/README.md +++ b/apps/processing/README.md @@ -26,10 +26,7 @@ $ cp .env.example .env Available options: | Name | Description | Default | Required | Notes | |-----------------------------|--------------------------------------------------------------------------------------------------------------------------------|-----------|----------------------------------|-----------------------------------------------------------------| -| `RPC_URLS` | Array of RPC URLs | N/A | Yes | Multiple URLs for redundancy | -| `CHAIN_ID` | Chain ID | N/A | Yes | At the moment only Optimism is supported (10) | -| `FETCH_LIMIT` | Maximum number of items to fetch in one batch | 500 | No | | -| `FETCH_DELAY_MS` | Delay between fetch operations in milliseconds | 1000 | No | | +| `CHAINS` | JSON array of chain configurations | N/A | Yes | Each chain object requires: `id` (number), `name` (string), `rpcUrls` (string array). Optional: `fetchLimit` (default: 500), `fetchDelayMs` (default: 1000) | | `DATABASE_URL` | PostgreSQL Data Layer database connection URL | N/A | Yes | | | `DATABASE_SCHEMA` | PostgreSQL Data Layer database schema name | public | Yes | | | `INDEXER_GRAPHQL_URL` | GraphQL endpoint for the indexer | N/A | Yes | | diff --git a/apps/processing/src/config/env.ts b/apps/processing/src/config/env.ts index 638d1af..4a201e2 100644 --- a/apps/processing/src/config/env.ts +++ b/apps/processing/src/config/env.ts @@ -14,11 +14,20 @@ const stringToJSONSchema = z.string().transform((str, ctx): z.infer { + const ids = chains.map((chain) => chain.id); + const uniqueIds = new Set(ids); + return ids.length === uniqueIds.size; + }, "Chain IDs must be unique"), DATABASE_URL: z.string(), DATABASE_SCHEMA: z.string().default("public"), INDEXER_GRAPHQL_URL: z.string().url(), diff --git a/apps/processing/src/services/processing.service.ts b/apps/processing/src/services/processing.service.ts index 9451057..285cd8f 100644 --- a/apps/processing/src/services/processing.service.ts +++ b/apps/processing/src/services/processing.service.ts @@ -10,37 +10,43 @@ import { SharedDependencies, SharedDependenciesService } from "./index.js"; /** * Processor service application * - Initializes core dependencies (repositories, providers) via SharedDependenciesService + * For each chain: * - Sets up EVM provider with configured RPC endpoints * - Creates an Orchestrator instance to coordinate an specific chain: * - Fetching on-chain events via indexer client * - Processing events through registered handlers * - Storing processed data in PostgreSQL via repositories * - Manages graceful shutdown on termination signals - * - * TODO: support multichain */ export class ProcessingService { - private readonly logger = Logger.getInstance(); - private readonly orchestrator: Orchestrator; + private readonly orchestrators: Map = new Map(); + private readonly logger = new Logger({ className: "ProcessingService" }); private readonly kyselyDatabase: SharedDependencies["kyselyDatabase"]; constructor(private readonly env: Environment) { + const { CHAINS: chains } = env; const { core, registries, indexerClient, kyselyDatabase } = - SharedDependenciesService.initialize(env, this.logger); + SharedDependenciesService.initialize(env); this.kyselyDatabase = kyselyDatabase; - // Initialize EVM provider - const evmProvider = new EvmProvider(env.RPC_URLS, optimism, this.logger); + for (const chain of chains) { + const chainLogger = new Logger({ chainId: chain.id as ChainId }); + // Initialize EVM provider + const evmProvider = new EvmProvider(chain.rpcUrls, optimism, chainLogger); - this.orchestrator = new Orchestrator( - env.CHAIN_ID as ChainId, - { ...core, evmProvider }, - indexerClient, - registries, - env.FETCH_LIMIT, - env.FETCH_DELAY_MS, - this.logger, - ); + this.orchestrators.set( + chain.id as ChainId, + new Orchestrator( + chain.id as ChainId, + { ...core, evmProvider }, + indexerClient, + registries, + chain.fetchLimit, + chain.fetchDelayMs, + chainLogger, + ), + ); + } } /** @@ -53,6 +59,8 @@ export class ProcessingService { const abortController = new AbortController(); + const orchestratorProcesses: Promise[] = []; + // Handle graceful shutdown process.on("SIGINT", () => { this.logger.info("Received SIGINT signal. Shutting down..."); @@ -65,7 +73,12 @@ export class ProcessingService { }); try { - await this.orchestrator.run(abortController.signal); + for (const orchestrator of this.orchestrators.values()) { + this.logger.info(`Starting orchestrator for chain ${orchestrator.chainId}...`); + orchestratorProcesses.push(orchestrator.run(abortController.signal)); + } + + await Promise.allSettled(orchestratorProcesses); } catch (error) { this.logger.error(`Processor service failed: ${error}`); throw error; diff --git a/apps/processing/src/services/sharedDependencies.service.ts b/apps/processing/src/services/sharedDependencies.service.ts index 80e4bc4..6e1bc25 100644 --- a/apps/processing/src/services/sharedDependencies.service.ts +++ b/apps/processing/src/services/sharedDependencies.service.ts @@ -14,7 +14,7 @@ import { KyselyProjectRepository, KyselyRoundRepository, } from "@grants-stack-indexer/repository"; -import { ILogger } from "@grants-stack-indexer/shared"; +import { Logger } from "@grants-stack-indexer/shared"; import { Environment } from "../config/index.js"; @@ -35,7 +35,7 @@ export type SharedDependencies = { * - Initializes indexer client */ export class SharedDependenciesService { - static initialize(env: Environment, logger: ILogger): SharedDependencies { + static initialize(env: Environment): SharedDependencies { // Initialize repositories const kyselyDatabase = createKyselyDatabase({ connectionString: env.DATABASE_URL, @@ -55,13 +55,22 @@ export class SharedDependenciesService { kyselyDatabase, env.DATABASE_SCHEMA, ); - const pricingProvider = PricingProviderFactory.create(env, { logger }); + const pricingProvider = PricingProviderFactory.create(env, { + logger: new Logger({ className: "PricingProvider" }), + }); - const metadataProvider = new IpfsProvider(env.IPFS_GATEWAYS_URL, logger); + const metadataProvider = new IpfsProvider( + env.IPFS_GATEWAYS_URL, + new Logger({ className: "IpfsProvider" }), + ); // Initialize registries - const eventsRegistry = new InMemoryEventsRegistry(logger); - const strategyRegistry = new InMemoryStrategyRegistry(logger); + const eventsRegistry = new InMemoryEventsRegistry( + new Logger({ className: "InMemoryEventsRegistry" }), + ); + const strategyRegistry = new InMemoryStrategyRegistry( + new Logger({ className: "InMemoryStrategyRegistry" }), + ); // Initialize indexer client const indexerClient = new EnvioIndexerClient( diff --git a/apps/processing/test/unit/processing.service.spec.ts b/apps/processing/test/unit/processing.service.spec.ts new file mode 100644 index 0000000..802ed72 --- /dev/null +++ b/apps/processing/test/unit/processing.service.spec.ts @@ -0,0 +1,151 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { EvmProvider } from "@grants-stack-indexer/chain-providers"; +import { Orchestrator } from "@grants-stack-indexer/data-flow"; + +import type { Environment } from "../../src/config/env.js"; +import { ProcessingService } from "../../src/services/processing.service.js"; + +vi.mock("../../src/services/sharedDependencies.service.js", () => ({ + SharedDependenciesService: { + initialize: vi.fn(() => ({ + core: {}, + registries: {}, + indexerClient: {}, + kyselyDatabase: { + destroy: vi.fn(), + }, + })), + }, +})); + +vi.mock("@grants-stack-indexer/chain-providers", () => ({ + EvmProvider: vi.fn(), +})); + +vi.spyOn(Orchestrator.prototype, "run").mockImplementation(async function (signal: AbortSignal) { + while (!signal.aborted) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } +}); + +describe("ProcessingService", () => { + let processingService: ProcessingService; + const mockEnv: Pick = { + CHAINS: [ + { + id: 1, + rpcUrls: ["http://localhost:8545"], + name: "Chain 1", + fetchLimit: 100, + fetchDelayMs: 1000, + }, + { + id: 2, + rpcUrls: ["http://localhost:8546"], + name: "Chain 2", + fetchLimit: 200, + fetchDelayMs: 2000, + }, + ], + DATABASE_URL: "postgresql://localhost:5432/test", + DATABASE_SCHEMA: "public", + }; + + beforeEach(() => { + vi.clearAllMocks(); + processingService = new ProcessingService(mockEnv as Environment); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("initializes multiple orchestrators correctly", () => { + expect(EvmProvider).toHaveBeenCalledTimes(2); + // Verify orchestrators were created with correct parameters + expect(processingService["orchestrators"].size).toBe(2); + + // Verify first chain initialization + expect(EvmProvider).toHaveBeenNthCalledWith( + 1, + ["http://localhost:8545"], + expect.any(Object), + expect.any(Object), + ); + + // Verify second chain initialization + expect(EvmProvider).toHaveBeenNthCalledWith( + 2, + ["http://localhost:8546"], + expect.any(Object), + expect.any(Object), + ); + }); + + it("starts all orchestrators and handles shutdown signals", async () => { + const abortSpy = vi.spyOn(AbortController.prototype, "abort"); + const runSpy = vi.mocked(Orchestrator.prototype.run); + const logSpy = vi.spyOn(processingService["logger"], "info"); + + const startPromise = processingService.start(); + + // Wait for orchestrators to start + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Verify both orchestrators are running + // const orchestratorInstances = vi.mocked(Orchestrator).mock.results; + // Verify both orchestrators are running + expect(runSpy).toHaveBeenCalledTimes(2); + expect(runSpy.mock.calls.map((call) => call[0])).toEqual([ + expect.any(AbortSignal), + expect.any(AbortSignal), + ]); + expect(logSpy).toHaveBeenNthCalledWith(2, "Starting orchestrator for chain 1..."); + expect(logSpy).toHaveBeenNthCalledWith(3, "Starting orchestrator for chain 2..."); + + // Simulate SIGINT + process.emit("SIGINT"); + expect(abortSpy).toHaveBeenCalled(); + + // Wait for orchestrators to shut down + await startPromise; + + // Verify all orchestrators were properly shut down + expect(runSpy.mock.results.every((result) => result.value)).toBeTruthy(); + }); + + it("handles SIGTERM signal", async () => { + const abortSpy = vi.spyOn(AbortController.prototype, "abort"); + const startPromise = processingService.start(); + const runSpy = vi.mocked(Orchestrator.prototype.run); + + // Wait for orchestrators to start + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Simulate SIGTERM + process.emit("SIGTERM"); + expect(abortSpy).toHaveBeenCalled(); + + await startPromise; + + // Verify all orchestrators were properly shut down + expect(runSpy.mock.results.every((result) => result.value)).toBeTruthy(); + }); + + it("releases resources correctly", async () => { + await processingService.releaseResources(); + + expect(processingService["kyselyDatabase"].destroy).toHaveBeenCalled(); + }); + + it("logs error during resource release", async () => { + const mockError = new Error("Database error"); + const logSpy = vi.spyOn(processingService["logger"], "error"); + vi.mocked(processingService["kyselyDatabase"].destroy).mockRejectedValueOnce(mockError); + + await processingService.releaseResources(); + + expect(logSpy).toHaveBeenCalledWith(`Error releasing resources: ${mockError}`); + }); +}); diff --git a/apps/processing/test/unit/sharedDependencies.service.spec.ts b/apps/processing/test/unit/sharedDependencies.service.spec.ts new file mode 100644 index 0000000..fc633dd --- /dev/null +++ b/apps/processing/test/unit/sharedDependencies.service.spec.ts @@ -0,0 +1,93 @@ +import { describe, expect, it, vi } from "vitest"; + +import { EnvioIndexerClient } from "@grants-stack-indexer/indexer-client"; +import { IpfsProvider } from "@grants-stack-indexer/metadata"; +import { PricingProviderFactory } from "@grants-stack-indexer/pricing"; +import { createKyselyDatabase } from "@grants-stack-indexer/repository"; +import { Logger } from "@grants-stack-indexer/shared"; + +import type { Environment } from "../../src/config/env.js"; +import { SharedDependenciesService } from "../../src/services/sharedDependencies.service.js"; + +// Mock dependencies +vi.mock("@grants-stack-indexer/repository", () => ({ + createKyselyDatabase: vi.fn(), + KyselyProjectRepository: vi.fn(), + KyselyRoundRepository: vi.fn(), + KyselyApplicationRepository: vi.fn(), + KyselyDonationRepository: vi.fn(), + KyselyApplicationPayoutRepository: vi.fn(), +})); + +vi.mock("@grants-stack-indexer/pricing", () => ({ + PricingProviderFactory: { + create: vi.fn(), + }, +})); + +vi.mock("@grants-stack-indexer/metadata", () => ({ + IpfsProvider: vi.fn(), +})); + +vi.mock("@grants-stack-indexer/indexer-client", () => ({ + EnvioIndexerClient: vi.fn(), +})); + +describe("SharedDependenciesService", () => { + const mockEnv: Pick< + Environment, + | "DATABASE_URL" + | "DATABASE_SCHEMA" + | "IPFS_GATEWAYS_URL" + | "INDEXER_GRAPHQL_URL" + | "INDEXER_ADMIN_SECRET" + | "PRICING_SOURCE" + > = { + DATABASE_URL: "postgresql://localhost:5432/test", + DATABASE_SCHEMA: "public", + IPFS_GATEWAYS_URL: ["https://ipfs.io"], + INDEXER_GRAPHQL_URL: "http://localhost:8080", + INDEXER_ADMIN_SECRET: "secret", + PRICING_SOURCE: "dummy", + }; + + it("initializes all dependencies correctly", () => { + const dependencies = SharedDependenciesService.initialize(mockEnv as Environment); + + // Verify database initialization + expect(createKyselyDatabase).toHaveBeenCalledWith({ + connectionString: mockEnv.DATABASE_URL, + }); + + // Verify providers initialization + expect(PricingProviderFactory.create).toHaveBeenCalledWith(mockEnv, { + logger: expect.any(Logger) as Logger, + }); + expect(IpfsProvider).toHaveBeenCalledWith(mockEnv.IPFS_GATEWAYS_URL, expect.any(Logger)); + + // Verify indexer client initialization + expect(EnvioIndexerClient).toHaveBeenCalledWith( + mockEnv.INDEXER_GRAPHQL_URL, + mockEnv.INDEXER_ADMIN_SECRET, + ); + + // Verify structure of returned dependencies + expect(dependencies).toHaveProperty("core"); + expect(dependencies).toHaveProperty("registries"); + expect(dependencies).toHaveProperty("indexerClient"); + expect(dependencies).toHaveProperty("kyselyDatabase"); + + // Verify core dependencies + expect(dependencies.core).toHaveProperty("projectRepository"); + expect(dependencies.core).toHaveProperty("roundRepository"); + expect(dependencies.core).toHaveProperty("applicationRepository"); + expect(dependencies.core).toHaveProperty("pricingProvider"); + expect(dependencies.core).toHaveProperty("donationRepository"); + expect(dependencies.core).toHaveProperty("metadataProvider"); + expect(dependencies.core).toHaveProperty("applicationPayoutRepository"); + + // Verify registries + expect(dependencies.registries).toHaveProperty("eventsRegistry"); + expect(dependencies.registries).toHaveProperty("strategyRegistry"); + }); +}); diff --git a/apps/processing/tsconfig.json b/apps/processing/tsconfig.json index 66bb87a..21c1c5b 100644 --- a/apps/processing/tsconfig.json +++ b/apps/processing/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*"] + "include": ["src/**/*", "test/**/*"] } diff --git a/packages/data-flow/src/eventsRegistry.ts b/packages/data-flow/src/eventsRegistry.ts index b009215..42bed46 100644 --- a/packages/data-flow/src/eventsRegistry.ts +++ b/packages/data-flow/src/eventsRegistry.ts @@ -1,4 +1,10 @@ -import type { AnyEvent, ContractName, ILogger, ProcessorEvent } from "@grants-stack-indexer/shared"; +import type { + AnyEvent, + ChainId, + ContractName, + ILogger, + ProcessorEvent, +} from "@grants-stack-indexer/shared"; import { stringify } from "@grants-stack-indexer/shared"; import type { IEventsRegistry } from "./internal.js"; @@ -8,22 +14,27 @@ import type { IEventsRegistry } from "./internal.js"; */ //TODO: Implement storage version to persist the last processed event. we need to store it by chainId export class InMemoryEventsRegistry implements IEventsRegistry { - private lastProcessedEvent: ProcessorEvent | undefined; + private lastProcessedEvent: Map> = new Map(); constructor(private logger: ILogger) {} /** * @inheritdoc */ - async getLastProcessedEvent(): Promise | undefined> { - return this.lastProcessedEvent; + async getLastProcessedEvent( + chainId: ChainId, + ): Promise | undefined> { + return this.lastProcessedEvent.get(chainId); } /** * @inheritdoc */ - async saveLastProcessedEvent(event: ProcessorEvent): Promise { + async saveLastProcessedEvent( + chainId: ChainId, + event: ProcessorEvent, + ): Promise { this.logger.debug(`Saving last processed event: ${stringify(event, undefined, 4)}`); - this.lastProcessedEvent = event; + this.lastProcessedEvent.set(chainId, event); } } diff --git a/packages/data-flow/src/interfaces/eventsRegistry.interface.ts b/packages/data-flow/src/interfaces/eventsRegistry.interface.ts index 8f061f1..6c063e1 100644 --- a/packages/data-flow/src/interfaces/eventsRegistry.interface.ts +++ b/packages/data-flow/src/interfaces/eventsRegistry.interface.ts @@ -1,4 +1,4 @@ -import { AnyEvent, ContractName, ProcessorEvent } from "@grants-stack-indexer/shared"; +import { AnyEvent, ChainId, ContractName, ProcessorEvent } from "@grants-stack-indexer/shared"; /** * The events registry saves as a checkpoint to the last processed event by the system. @@ -7,12 +7,19 @@ import { AnyEvent, ContractName, ProcessorEvent } from "@grants-stack-indexer/sh export interface IEventsRegistry { /** * Get the last processed event by the system + * @param chainId - The chain id * @returns The last processed event or undefined if no event has been processed yet. */ - getLastProcessedEvent(): Promise | undefined>; + getLastProcessedEvent( + chainId: ChainId, + ): Promise | undefined>; /** * Save the last processed event by the system + * @param chainId - The chain id * @param event - The event to save. */ - saveLastProcessedEvent(event: ProcessorEvent): Promise; + saveLastProcessedEvent( + chainId: ChainId, + event: ProcessorEvent, + ): Promise; } diff --git a/packages/data-flow/src/orchestrator.ts b/packages/data-flow/src/orchestrator.ts index 52d58dc..ede93c6 100644 --- a/packages/data-flow/src/orchestrator.ts +++ b/packages/data-flow/src/orchestrator.ts @@ -67,7 +67,7 @@ export class Orchestrator { * @param fetchDelayInMs - The fetch delay in milliseconds */ constructor( - private chainId: ChainId, + public readonly chainId: ChainId, private dependencies: Readonly, private indexerClient: IIndexerClient, private registries: { @@ -110,7 +110,7 @@ export class Orchestrator { await delay(this.fetchDelayInMs); continue; } - await this.eventsRegistry.saveLastProcessedEvent(event); + await this.eventsRegistry.saveLastProcessedEvent(this.chainId, event); event = await this.enhanceStrategyId(event); if (event.contractName === "Strategy" && "strategyId" in event) { @@ -160,7 +160,7 @@ export class Orchestrator { * Enqueue new events from the events fetcher using the last processed event as a starting point */ private async enqueueEvents(): Promise { - const lastProcessedEvent = await this.eventsRegistry.getLastProcessedEvent(); + const lastProcessedEvent = await this.eventsRegistry.getLastProcessedEvent(this.chainId); const blockNumber = lastProcessedEvent?.blockNumber ?? 0; const logIndex = lastProcessedEvent?.logIndex ?? 0; diff --git a/packages/data-flow/test/unit/eventsRegistry.spec.ts b/packages/data-flow/test/unit/eventsRegistry.spec.ts index 59920e4..453245e 100644 --- a/packages/data-flow/test/unit/eventsRegistry.spec.ts +++ b/packages/data-flow/test/unit/eventsRegistry.spec.ts @@ -11,9 +11,10 @@ describe("InMemoryEventsRegistry", () => { info: vi.fn(), warn: vi.fn(), }; + const chainId = 1 as ChainId; it("return null when no event has been saved", async () => { const registry = new InMemoryEventsRegistry(logger); - const lastEvent = await registry.getLastProcessedEvent(); + const lastEvent = await registry.getLastProcessedEvent(chainId); expect(lastEvent).toBeUndefined(); }); @@ -42,8 +43,8 @@ describe("InMemoryEventsRegistry", () => { }, }; - await registry.saveLastProcessedEvent(mockEvent); - const retrievedEvent = await registry.getLastProcessedEvent(); + await registry.saveLastProcessedEvent(chainId, mockEvent); + const retrievedEvent = await registry.getLastProcessedEvent(chainId); expect(retrievedEvent).toEqual(mockEvent); }); @@ -94,10 +95,10 @@ describe("InMemoryEventsRegistry", () => { }, }; - await registry.saveLastProcessedEvent(firstEvent); - await registry.saveLastProcessedEvent(secondEvent); + await registry.saveLastProcessedEvent(chainId, firstEvent); + await registry.saveLastProcessedEvent(chainId, secondEvent); - const lastEvent = await registry.getLastProcessedEvent(); + const lastEvent = await registry.getLastProcessedEvent(chainId); expect(lastEvent).toEqual(secondEvent); expect(lastEvent).not.toEqual(firstEvent); }); diff --git a/packages/data-flow/test/unit/orchestrator.spec.ts b/packages/data-flow/test/unit/orchestrator.spec.ts index 0ad1bb3..4392da5 100644 --- a/packages/data-flow/test/unit/orchestrator.spec.ts +++ b/packages/data-flow/test/unit/orchestrator.spec.ts @@ -168,8 +168,14 @@ describe("Orchestrator", { sequential: true }, () => { expect(eventsProcessorSpy).toHaveBeenCalledWith(mockEvents[0]); expect(eventsProcessorSpy).toHaveBeenCalledWith(mockEvents[1]); - expect(mockEventsRegistry.saveLastProcessedEvent).toHaveBeenCalledWith(mockEvents[0]); - expect(mockEventsRegistry.saveLastProcessedEvent).toHaveBeenCalledWith(mockEvents[1]); + expect(mockEventsRegistry.saveLastProcessedEvent).toHaveBeenCalledWith( + chainId, + mockEvents[0], + ); + expect(mockEventsRegistry.saveLastProcessedEvent).toHaveBeenCalledWith( + chainId, + mockEvents[1], + ); }); it("wait and keep polling on empty queue", async () => { @@ -262,7 +268,10 @@ describe("Orchestrator", { sequential: true }, () => { }); expect(orchestrator["dataLoader"].applyChanges).toHaveBeenCalledTimes(1); expect(orchestrator["dataLoader"].applyChanges).toHaveBeenCalledWith(changesets); - expect(mockEventsRegistry.saveLastProcessedEvent).toHaveBeenCalledWith(mockEvent); + expect(mockEventsRegistry.saveLastProcessedEvent).toHaveBeenCalledWith( + chainId, + mockEvent, + ); }); const strategyEvents: Record = { @@ -339,7 +348,10 @@ describe("Orchestrator", { sequential: true }, () => { expect(mockStrategyRegistry.getStrategyId).toHaveBeenCalledWith(strategyAddress); expect(orchestrator["dataLoader"].applyChanges).toHaveBeenCalledTimes(1); expect(orchestrator["dataLoader"].applyChanges).toHaveBeenCalledWith(changesets); - expect(mockEventsRegistry.saveLastProcessedEvent).toHaveBeenCalledWith(mockEvent); + expect(mockEventsRegistry.saveLastProcessedEvent).toHaveBeenCalledWith( + chainId, + mockEvent, + ); }); } diff --git a/packages/shared/src/logger/logger.ts b/packages/shared/src/logger/logger.ts index 4ba6656..4527fe5 100644 --- a/packages/shared/src/logger/logger.ts +++ b/packages/shared/src/logger/logger.ts @@ -1,5 +1,6 @@ import { createLogger, format, transports, Logger as WinstonLogger } from "winston"; +import { ChainId } from "../internal.js"; import { ILogger } from "./logger.interface.js"; type LogLevel = "error" | "warn" | "info" | "debug"; @@ -8,34 +9,26 @@ const validLogLevels: LogLevel[] = ["error", "warn", "info", "debug"]; export class Logger implements ILogger { private logger: WinstonLogger; - private static instance: Logger | null; private level: LogLevel; - private constructor() { + + constructor(config: { chainId?: ChainId; className?: string } = {}) { + const { chainId, className } = config; this.level = this.isValidLogLevel(process.env.LOG_LEVEL) ? process.env.LOG_LEVEL : "info"; this.logger = createLogger({ level: this.level, + defaultMeta: { chainId, className }, format: format.combine( format.colorize(), format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), format.errors({ stack: true }), - format.printf(({ level, message, timestamp, stack }) => { - return `${timestamp} ${level}: ${stack ?? message ?? ""}`; + format.printf(({ level, message, timestamp, stack, chainId, className }) => { + return `${chainId ? `[Chain:${chainId}] ` : ""}${className ? `[${className}] ` : ""}${timestamp} ${level}: ${stack ?? message ?? ""}`; }), ), transports: [new transports.Console()], }); } - /** - * Returns the instance of the Logger class. - * @param level The log level to be used by the logger. - * @returns The instance of the Logger class. - */ - public static getInstance(): ILogger { - if (!Logger.instance) { - Logger.instance = new Logger(); - } - return Logger.instance; - } + isValidLogLevel(level?: string): level is LogLevel { return validLogLevels.includes(level as LogLevel); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1dd8608..1fdb47b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,8 +65,8 @@ importers: specifier: 4.3.10 version: 4.3.10 envio: - specifier: 2.7.3 - version: 2.7.3 + specifier: 2.8.2 + version: 2.8.2(typescript@5.2.2)(zod@3.23.8) ethers: specifier: 6.8.0 version: 6.8.0 @@ -561,6 +561,67 @@ packages: integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==, } + "@envio-dev/hypersync-client-darwin-arm64@0.6.2": + resolution: + { + integrity: sha512-dDIuQqEgARR1JYodbGkmck1i9qbYEidc4Kw4DOrRKQ0uZFwflI4o8wm3P+G/ofc1iXwp4pm7jqNUGzZDpK9pqA==, + } + engines: { node: ">= 10" } + cpu: [arm64] + os: [darwin] + + "@envio-dev/hypersync-client-darwin-x64@0.6.2": + resolution: + { + integrity: sha512-NGlgkmosAzlZ1EuQ5/djkg6sQI/dgVX+XGOLI6Zden0ca2zr/ByBRA7yCKxpwZHYynFN2FoMsMGSRY8jj6XQWw==, + } + engines: { node: ">= 10" } + cpu: [x64] + os: [darwin] + + "@envio-dev/hypersync-client-linux-arm64-gnu@0.6.2": + resolution: + { + integrity: sha512-p2LQSaOSgjVvvFHOGHnPHTnsDHCWNEG78J29guZyqWojmaxTaR4eGyFEknla7eO0w58EIUGEBTF0+EJ7duMimg==, + } + engines: { node: ">= 10" } + cpu: [arm64] + os: [linux] + + "@envio-dev/hypersync-client-linux-x64-gnu@0.6.2": + resolution: + { + integrity: sha512-/kIL9Q0e5hM0g6cvdCHL93LMu+OCnwQAGKQHCj2TKKUjVpWFNb0Ki5PMgJhQ8mimEBFsJAnErVK+HrugLTlaVQ==, + } + engines: { node: ">= 10" } + cpu: [x64] + os: [linux] + + "@envio-dev/hypersync-client-linux-x64-musl@0.6.2": + resolution: + { + integrity: sha512-4r5hA/zyxUjWBDYYeXLCl5pBfSI9njEIPuLMFXajjVcAvSXRtBhmgVwa3JrjjCxZ8aLfqk9vvPkb/KgYfrbXXw==, + } + engines: { node: ">= 10" } + cpu: [x64] + os: [linux] + + "@envio-dev/hypersync-client-win32-x64-msvc@0.6.2": + resolution: + { + integrity: sha512-VgsSwmDAgvpgo9QsIiwIp9IN+h3g0U/7zrCrvNAMj+lsMHJPZ7L0cfN58dQUr84wL8domJGJrnMKUtu/Yf49ow==, + } + engines: { node: ">= 10" } + cpu: [x64] + os: [win32] + + "@envio-dev/hypersync-client@0.6.2": + resolution: + { + integrity: sha512-wgp0UmblW8yn/q5NMkVYPFDvOmgHWPTibl/QJYyYK2KXqAMHssUjP07ayduiZCexQOZ94Agpv4SvmYxQNjGBIA==, + } + engines: { node: ">= 10" } + "@esbuild/aix-ppc64@0.21.5": resolution: { @@ -2374,42 +2435,42 @@ packages: } engines: { node: ">=6" } - envio-darwin-arm64@2.7.3: + envio-darwin-arm64@2.8.2: resolution: { - integrity: sha512-jFtqTbGRXlYBvwQJ2iEZwsumGy+19y7e4+OjPM5ZqqoJ1sHNQ70/G1AqnvZYFKD8RuTUN+5ArU5gYc0Tgqu8Ow==, + integrity: sha512-6FYf8/6qKRpdh8BuhylYtWsv/xW2Q1vkL9hYgy/VB0K1nMTBIfY83dDYDy2wHYSxg+R6gvawrI66L2bJ4rZn6g==, } cpu: [arm64] os: [darwin] - envio-darwin-x64@2.7.3: + envio-darwin-x64@2.8.2: resolution: { - integrity: sha512-yT7krrqRENpNYvCtyFMlPRGDsUSO0nvrTX0fl28f8xJyD/HAF0nKnU8f6FBiyWCzLzPh8Uby1qK3l+hUVhcktQ==, + integrity: sha512-t58SvMyyw3VOXW9CWrVkmxWDO7zACd56x5/sDZWFzcgGd0nOOYMkz2Gzag2VawsqqlZC0iyLQPE4HZiPPCcYZw==, } cpu: [x64] os: [darwin] - envio-linux-arm64@2.7.3: + envio-linux-arm64@2.8.2: resolution: { - integrity: sha512-QCJlypEOw3cc93UVkRvYuF9Z3x/n57usb0P85Mz0ADiykxBBd7QWyEFKgmMzQZlLZfgQ0NLHwcPh+92/bGoL/A==, + integrity: sha512-dMBRfCUB9S2fLAi88j3Fd8aftnwA4MGn5/VR1wK7VVtLFCCRKEjtNJqVyclt72UNYBw5wuhLei9U3KSiemAlDQ==, } cpu: [arm64] os: [linux] - envio-linux-x64@2.7.3: + envio-linux-x64@2.8.2: resolution: { - integrity: sha512-IRpfHZhlx7dTmtROhuVJONO7otPlwaUmFdb2HEycwgb4hN1+6Xadn5lPuLYmqJFSWgd2lyxn5EdAJWw8KV4qtA==, + integrity: sha512-u8tyNNbrEM8kbuqAy5YxNTOnpAg72DAY+5S0rxOv47RGAr9fXhmbo1EUBVwlV+Us6wz7gdcTtSId2e55Z/AzeQ==, } cpu: [x64] os: [linux] - envio@2.7.3: + envio@2.8.2: resolution: { - integrity: sha512-j/G4Gg+g0xUUf0RgH3PKo0REXwh2blB/t7kYEPFZy9EX3/lOw8fG076ZN7rJ82FzzOjVsQH6RSeW6OFGGWgnLw==, + integrity: sha512-Cg3O/1I78PPF1S3sV3ZRp1RAiaSFX2CKMFLcchbAgF7m1y/w9+OZKY4O0lhFCMNHLwmKyThnhfJI0JWxTEKs3g==, } hasBin: true @@ -3589,6 +3650,83 @@ packages: } engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + npm@10.9.1: + resolution: + { + integrity: sha512-yJUw03xLqjiv1D52oHeoS5qmOEC5hkJlhP1cWlSrCgshuxWVyFEEK3M3hLC0NwbTaklLTYrhoIanYsuNP5WUKg==, + } + engines: { node: ^18.17.0 || >=20.5.0 } + hasBin: true + bundledDependencies: + - "@isaacs/string-locale-compare" + - "@npmcli/arborist" + - "@npmcli/config" + - "@npmcli/fs" + - "@npmcli/map-workspaces" + - "@npmcli/package-json" + - "@npmcli/promise-spawn" + - "@npmcli/redact" + - "@npmcli/run-script" + - "@sigstore/tuf" + - abbrev + - archy + - cacache + - chalk + - ci-info + - cli-columns + - fastest-levenshtein + - fs-minipass + - glob + - graceful-fs + - hosted-git-info + - ini + - init-package-json + - is-cidr + - json-parse-even-better-errors + - libnpmaccess + - libnpmdiff + - libnpmexec + - libnpmfund + - libnpmhook + - libnpmorg + - libnpmpack + - libnpmpublish + - libnpmsearch + - libnpmteam + - libnpmversion + - make-fetch-happen + - minimatch + - minipass + - minipass-pipeline + - ms + - node-gyp + - nopt + - normalize-package-data + - npm-audit-report + - npm-install-checks + - npm-package-arg + - npm-pick-manifest + - npm-profile + - npm-registry-fetch + - npm-user-validate + - p-map + - pacote + - parse-conflict-json + - proc-log + - qrcode-terminal + - read + - semver + - spdx-expression-parse + - ssri + - supports-color + - tar + - text-table + - tiny-relative-date + - treeverse + - validate-npm-package-name + - which + - write-file-atomic + obuf@1.1.2: resolution: { @@ -4649,6 +4787,17 @@ packages: typescript: optional: true + viem@2.21.0: + resolution: + { + integrity: sha512-9g3Gw2nOU6t4bNuoDI5vwVExzIxseU0J7Jjx10gA2RNQVrytIrLxggW++tWEe3w4mnnm/pS1WgZFjQ/QKf/nHw==, + } + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + viem@2.21.19: resolution: { @@ -4920,6 +5069,14 @@ packages: } engines: { node: ">=12" } + yarn@1.22.22: + resolution: + { + integrity: sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==, + } + engines: { node: ">=4.0.0" } + hasBin: true + yn@2.0.0: resolution: { @@ -5186,6 +5343,36 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 + "@envio-dev/hypersync-client-darwin-arm64@0.6.2": + optional: true + + "@envio-dev/hypersync-client-darwin-x64@0.6.2": + optional: true + + "@envio-dev/hypersync-client-linux-arm64-gnu@0.6.2": + optional: true + + "@envio-dev/hypersync-client-linux-x64-gnu@0.6.2": + optional: true + + "@envio-dev/hypersync-client-linux-x64-musl@0.6.2": + optional: true + + "@envio-dev/hypersync-client-win32-x64-msvc@0.6.2": + optional: true + + "@envio-dev/hypersync-client@0.6.2": + dependencies: + npm: 10.9.1 + yarn: 1.22.22 + optionalDependencies: + "@envio-dev/hypersync-client-darwin-arm64": 0.6.2 + "@envio-dev/hypersync-client-darwin-x64": 0.6.2 + "@envio-dev/hypersync-client-linux-arm64-gnu": 0.6.2 + "@envio-dev/hypersync-client-linux-x64-gnu": 0.6.2 + "@envio-dev/hypersync-client-linux-x64-musl": 0.6.2 + "@envio-dev/hypersync-client-win32-x64-msvc": 0.6.2 + "@esbuild/aix-ppc64@0.21.5": optional: true @@ -5810,6 +5997,11 @@ snapshots: jsonparse: 1.3.1 through: 2.3.8 + abitype@1.0.5(typescript@5.2.2)(zod@3.23.8): + optionalDependencies: + typescript: 5.2.2 + zod: 3.23.8 + abitype@1.0.5(typescript@5.5.4)(zod@3.23.8): optionalDependencies: typescript: 5.5.4 @@ -6167,27 +6359,34 @@ snapshots: env-paths@2.2.1: {} - envio-darwin-arm64@2.7.3: + envio-darwin-arm64@2.8.2: optional: true - envio-darwin-x64@2.7.3: + envio-darwin-x64@2.8.2: optional: true - envio-linux-arm64@2.7.3: + envio-linux-arm64@2.8.2: optional: true - envio-linux-x64@2.7.3: + envio-linux-x64@2.8.2: optional: true - envio@2.7.3: + envio@2.8.2(typescript@5.2.2)(zod@3.23.8): dependencies: + "@envio-dev/hypersync-client": 0.6.2 rescript: 11.1.3 rescript-schema: 8.2.0(rescript@11.1.3) + viem: 2.21.0(typescript@5.2.2)(zod@3.23.8) optionalDependencies: - envio-darwin-arm64: 2.7.3 - envio-darwin-x64: 2.7.3 - envio-linux-arm64: 2.7.3 - envio-linux-x64: 2.7.3 + envio-darwin-arm64: 2.8.2 + envio-darwin-x64: 2.8.2 + envio-linux-arm64: 2.8.2 + envio-linux-x64: 2.8.2 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod environment@1.1.0: {} @@ -6885,6 +7084,8 @@ snapshots: dependencies: path-key: 4.0.0 + npm@10.9.1: {} + obuf@1.1.2: {} once@1.4.0: @@ -7444,6 +7645,24 @@ snapshots: - utf-8-validate - zod + viem@2.21.0(typescript@5.2.2)(zod@3.23.8): + dependencies: + "@adraffy/ens-normalize": 1.10.0 + "@noble/curves": 1.4.0 + "@noble/hashes": 1.4.0 + "@scure/bip32": 1.4.0 + "@scure/bip39": 1.3.0 + abitype: 1.0.5(typescript@5.2.2)(zod@3.23.8) + isows: 1.0.4(ws@8.17.1) + webauthn-p256: 0.0.5 + ws: 8.17.1 + optionalDependencies: + typescript: 5.2.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + viem@2.21.19(typescript@5.5.4)(zod@3.23.8): dependencies: "@adraffy/ens-normalize": 1.11.0 @@ -7630,6 +7849,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yarn@1.22.22: {} + yn@2.0.0: {} yn@3.1.1: {}