From fabe7e3bac0704bfcc43ad339418236c2b1ad0f8 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:16:49 -0300 Subject: [PATCH 1/7] feat: base strategy processor & dvmd strategy factory --- .../test/unit/envioIndexerClient.spec.ts | 2 +- packages/processors/src/exceptions/index.ts | 1 + .../unsupportedStrategy.exception.ts | 7 ++ packages/processors/src/interfaces/index.ts | 1 + .../interfaces/strategyHandler.interface.ts | 15 ++++ .../dvmdDirectTransfer.handler.ts | 45 +++++++++++ .../handlers/distributed.handler.ts | 19 +++++ .../handlers/index.ts | 2 + .../handlers/registered.handler.ts | 22 ++++++ .../src/strategy/strategy.processor.ts | 21 ++++-- .../src/strategy/strategyHandler.factory.ts | 30 ++++++++ .../dvmdDirectTransfer.handler.spec.ts | 5 ++ .../handlers/distributed.handler.spec.ts | 6 ++ .../handlers/registered.handler.spec.ts | 6 ++ .../strategy/strategyHandler.factory.spec.ts | 74 +++++++++++++++++++ packages/shared/src/types/events/strategy.ts | 31 ++++++-- 16 files changed, 274 insertions(+), 13 deletions(-) create mode 100644 packages/processors/src/exceptions/unsupportedStrategy.exception.ts create mode 100644 packages/processors/src/interfaces/strategyHandler.interface.ts create mode 100644 packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts create mode 100644 packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.ts create mode 100644 packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.ts create mode 100644 packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts create mode 100644 packages/processors/src/strategy/strategyHandler.factory.ts create mode 100644 packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts create mode 100644 packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.spec.ts create mode 100644 packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts create mode 100644 packages/processors/test/strategy/strategyHandler.factory.spec.ts diff --git a/packages/indexer-client/test/unit/envioIndexerClient.spec.ts b/packages/indexer-client/test/unit/envioIndexerClient.spec.ts index c15dd90..cffa08f 100644 --- a/packages/indexer-client/test/unit/envioIndexerClient.spec.ts +++ b/packages/indexer-client/test/unit/envioIndexerClient.spec.ts @@ -51,7 +51,7 @@ describe("EnvioIndexerClient", () => { eventName: "PoolCreated", srcAddress: "0x1234567890123456789012345678901234567890", logIndex: 0, - params: { contractAddress: "0x1234" }, + params: { contractAddress: "0x1234", tokenAddress: "0x1234", amount: 1000 }, transactionFields: { hash: "0x123", transactionIndex: 1, diff --git a/packages/processors/src/exceptions/index.ts b/packages/processors/src/exceptions/index.ts index 746c664..8d2208a 100644 --- a/packages/processors/src/exceptions/index.ts +++ b/packages/processors/src/exceptions/index.ts @@ -1,3 +1,4 @@ export * from "./tokenPriceNotFound.exception.js"; export * from "./unsupportedEvent.exception.js"; export * from "./invalidArgument.exception.js"; +export * from "./unsupportedStrategy.exception.js"; diff --git a/packages/processors/src/exceptions/unsupportedStrategy.exception.ts b/packages/processors/src/exceptions/unsupportedStrategy.exception.ts new file mode 100644 index 0000000..c1ddbb1 --- /dev/null +++ b/packages/processors/src/exceptions/unsupportedStrategy.exception.ts @@ -0,0 +1,7 @@ +import { Hex } from "viem"; + +export class UnsupportedStrategy extends Error { + constructor(strategyId: Hex) { + super(`Strategy ${strategyId} unsupported`); + } +} diff --git a/packages/processors/src/interfaces/index.ts b/packages/processors/src/interfaces/index.ts index c8e38e6..f76718c 100644 --- a/packages/processors/src/interfaces/index.ts +++ b/packages/processors/src/interfaces/index.ts @@ -1,3 +1,4 @@ export * from "./processor.interface.js"; export * from "./factory.interface.js"; export * from "./eventHandler.interface.js"; +export * from "./strategyHandler.interface.js"; diff --git a/packages/processors/src/interfaces/strategyHandler.interface.ts b/packages/processors/src/interfaces/strategyHandler.interface.ts new file mode 100644 index 0000000..d5c2ec4 --- /dev/null +++ b/packages/processors/src/interfaces/strategyHandler.interface.ts @@ -0,0 +1,15 @@ +import { Changeset } from "@grants-stack-indexer/repository"; +import { ContractToEventName, ProtocolEvent } from "@grants-stack-indexer/shared"; + +/** + * Interface for an event handler. + * @template C - The contract name. + * @template E - The event name. + */ +export interface IStrategyHandler> { + /** + * Handles the event. + * @returns A promise that resolves to an array of changesets. + */ + handle(event: ProtocolEvent<"Strategy", E>): Promise; +} diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts new file mode 100644 index 0000000..58a792d --- /dev/null +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts @@ -0,0 +1,45 @@ +import { Changeset } from "@grants-stack-indexer/repository"; +import { ChainId, ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared"; + +import type { IStrategyHandler, ProcessorDependencies } from "../../internal.js"; +import { UnsupportedEventException } from "../../internal.js"; +import { DVMDDistributedHandler, DVMDRegisteredHandler } from "./handlers/index.js"; + +type Dependencies = Pick< + ProcessorDependencies, + "projectRepository" | "roundRepository" | "metadataProvider" +>; + +/** + * This handler is responsible for processing events related to the + * Donation Voting Merkle Distribution Direct Transfer strategy. + * + * The following events are currently handled by this strategy: + * - Registered + * - Distributed + */ + +export class DVMDDirectTransferHandler implements IStrategyHandler { + constructor( + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + async handle(event: ProtocolEvent<"Strategy", StrategyEvent>): Promise { + switch (event.eventName) { + case "Registered": + return new DVMDRegisteredHandler( + event as ProtocolEvent<"Strategy", "Registered">, + this.chainId, + this.dependencies, + ).handle(); + case "Distributed": + return new DVMDDistributedHandler( + event as ProtocolEvent<"Strategy", "Distributed">, + this.chainId, + this.dependencies, + ).handle(); + default: + throw new UnsupportedEventException("Strategy", event.eventName); + } + } +} diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.ts new file mode 100644 index 0000000..58316b1 --- /dev/null +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.ts @@ -0,0 +1,19 @@ +import { Changeset } from "@grants-stack-indexer/repository"; +import { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; + +import { IEventHandler, ProcessorDependencies } from "../../../internal.js"; + +type Dependencies = Pick; + +export class DVMDDistributedHandler implements IEventHandler<"Strategy", "Distributed"> { + constructor( + readonly event: ProtocolEvent<"Strategy", "Distributed">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + async handle(): Promise { + //TODO: Implement + throw new Error("Method not implemented."); + } +} diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.ts new file mode 100644 index 0000000..ccd560a --- /dev/null +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.ts @@ -0,0 +1,2 @@ +export * from "./distributed.handler.js"; +export * from "./registered.handler.js"; diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts new file mode 100644 index 0000000..42e4f8a --- /dev/null +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts @@ -0,0 +1,22 @@ +import { Changeset } from "@grants-stack-indexer/repository"; +import { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; + +import { IEventHandler, ProcessorDependencies } from "../../../internal.js"; + +type Dependencies = Pick< + ProcessorDependencies, + "roundRepository" | "projectRepository" | "metadataProvider" +>; + +export class DVMDRegisteredHandler implements IEventHandler<"Strategy", "Registered"> { + constructor( + readonly event: ProtocolEvent<"Strategy", "Registered">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + async handle(): Promise { + //TODO: Implement + throw new Error("Not implemented"); + } +} diff --git a/packages/processors/src/strategy/strategy.processor.ts b/packages/processors/src/strategy/strategy.processor.ts index 9c2c4ae..c082a96 100644 --- a/packages/processors/src/strategy/strategy.processor.ts +++ b/packages/processors/src/strategy/strategy.processor.ts @@ -1,11 +1,22 @@ import { Changeset } from "@grants-stack-indexer/repository"; -import { ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared"; +import { ChainId, ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared"; -import type { IProcessor } from "../internal.js"; +import type { IProcessor, ProcessorDependencies } from "../internal.js"; +import { StrategyHandlerFactory } from "./strategyHandler.factory.js"; export class StrategyProcessor implements IProcessor<"Strategy", StrategyEvent> { - process(_event: ProtocolEvent<"Strategy", StrategyEvent>): Promise { - //TODO: Implement - throw new Error("Method not implemented."); + constructor( + private readonly chainId: ChainId, + private readonly dependencies: ProcessorDependencies, + ) {} + + async process(event: ProtocolEvent<"Strategy", StrategyEvent>): Promise { + const strategyId = event.strategyId; + + return StrategyHandlerFactory.createHandler( + this.chainId, + this.dependencies, + strategyId, + ).handle(event); } } diff --git a/packages/processors/src/strategy/strategyHandler.factory.ts b/packages/processors/src/strategy/strategyHandler.factory.ts new file mode 100644 index 0000000..a084397 --- /dev/null +++ b/packages/processors/src/strategy/strategyHandler.factory.ts @@ -0,0 +1,30 @@ +import { Hex } from "viem"; + +import { ChainId, StrategyEvent } from "@grants-stack-indexer/shared"; + +import { IStrategyHandler, ProcessorDependencies, UnsupportedStrategy } from "../internal.js"; +import { DVMDDirectTransferHandler } from "./donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; + +export class StrategyHandlerFactory { + static createHandler( + chainId: ChainId, + dependencies: ProcessorDependencies, + strategyId: Hex, + ): IStrategyHandler { + const _strategyId = strategyId.toLowerCase(); + + switch (_strategyId) { + case "0x6f9291df02b2664139cec5703c124e4ebce32879c74b6297faa1468aa5ff9ebf": + // DonationVotingMerkleDistributionDirectTransferStrategyv1.1 + case "0x2f46bf157821dc41daa51479e94783bb0c8699eac63bf75ec450508ab03867ce": + // DonationVotingMerkleDistributionDirectTransferStrategyv2.0 + case "0x2f0250d534b2d59b8b5cfa5eb0d0848a59ccbf5de2eaf72d2ba4bfe73dce7c6b": + // DonationVotingMerkleDistributionDirectTransferStrategyv2.1 + case "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0": + return new DVMDDirectTransferHandler(chainId, dependencies); + + default: + throw new UnsupportedStrategy(strategyId); + } + } +} diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts new file mode 100644 index 0000000..daa6a88 --- /dev/null +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts @@ -0,0 +1,5 @@ +import { describe, it } from "vitest"; + +describe("DVMDDirectTransferHandler", () => { + it.skip("handle the direct transfer event", () => {}); +}); diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.spec.ts new file mode 100644 index 0000000..bfa3262 --- /dev/null +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.spec.ts @@ -0,0 +1,6 @@ +import { describe } from "node:test"; +import { it } from "vitest"; + +describe("DVMDDirectTransferDistributedHandler", () => { + it.skip("handle the distributed event", () => {}); +}); diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts new file mode 100644 index 0000000..519383d --- /dev/null +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts @@ -0,0 +1,6 @@ +import { describe } from "node:test"; +import { it } from "vitest"; + +describe("DVMDDirectTransferRegisteredHandler", () => { + it.skip("handle the registered event", () => {}); +}); diff --git a/packages/processors/test/strategy/strategyHandler.factory.spec.ts b/packages/processors/test/strategy/strategyHandler.factory.spec.ts new file mode 100644 index 0000000..ef941bd --- /dev/null +++ b/packages/processors/test/strategy/strategyHandler.factory.spec.ts @@ -0,0 +1,74 @@ +import { Hex } from "viem"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { EvmProvider } from "@grants-stack-indexer/chain-providers"; +import { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import { IPricingProvider } from "@grants-stack-indexer/pricing"; +import { IProjectReadRepository, IRoundReadRepository } from "@grants-stack-indexer/repository"; +import { ChainId } from "@grants-stack-indexer/shared"; + +import { ProcessorDependencies, UnsupportedStrategy } from "../../src/internal.js"; +import { DVMDDirectTransferHandler } from "../../src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; +import { StrategyHandlerFactory } from "../../src/strategy/strategyHandler.factory.js"; + +describe("StrategyHandlerFactory", () => { + const chainId = 10 as ChainId; + let mockEvmProvider: EvmProvider; + let mockPricingProvider: IPricingProvider; + let mockMetadataProvider: IMetadataProvider; + let mockRoundRepository: IRoundReadRepository; + let mockProjectRepository: IProjectReadRepository; + let mockProcessorDependencies: ProcessorDependencies; + + beforeEach(() => { + mockEvmProvider = {} as EvmProvider; + mockPricingProvider = {} as IPricingProvider; + mockMetadataProvider = {} as IMetadataProvider; + mockRoundRepository = {} as IRoundReadRepository; + mockProjectRepository = {} as IProjectReadRepository; + mockProcessorDependencies = { + evmProvider: mockEvmProvider, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + }; + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it("creates a DVMDDirectTransferHandler", () => { + const strategies: Hex[] = [ + "0x6f9291df02b2664139cec5703c124e4ebce32879c74b6297faa1468aa5ff9ebf", + "0x2f46bf157821dc41daa51479e94783bb0c8699eac63bf75ec450508ab03867ce", + "0x2f0250d534b2d59b8b5cfa5eb0d0848a59ccbf5de2eaf72d2ba4bfe73dce7c6b", + "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", + ]; + + strategies.forEach((strategyId) => { + const handler = StrategyHandlerFactory.createHandler( + chainId, + mockProcessorDependencies, + strategyId, + ); + + expect(handler).toBeDefined(); + expect(handler).toBeInstanceOf(DVMDDirectTransferHandler); + }); + }); + + it.skip("creates a DirectGrantsLiteHandler"); + it.skip("creates a DirectGrantsSimpleHandler"); + + it("throws an error if the strategy id is not supported", () => { + expect(() => + StrategyHandlerFactory.createHandler( + chainId, + mockProcessorDependencies, + "0xnot-supported", + ), + ).toThrow(UnsupportedStrategy); + }); +}); diff --git a/packages/shared/src/types/events/strategy.ts b/packages/shared/src/types/events/strategy.ts index e52e79d..e4b3d19 100644 --- a/packages/shared/src/types/events/strategy.ts +++ b/packages/shared/src/types/events/strategy.ts @@ -1,25 +1,42 @@ +import { Hex } from "viem"; + import { Address } from "../../internal.js"; /** * This type is used to represent a Strategy events. */ -export type StrategyEvent = "Registered" | "TimestampsUpdated" | "AllocatedWithToken"; +export type StrategyEvent = + | "Registered" + | "Distributed" + | "TimestampsUpdated" + | "AllocatedWithToken"; /** * This type maps Strategy events to their respective parameters. */ export type StrategyEventParams = T extends "Registered" ? RegisteredParams - : T extends "TimestampsUpdated" - ? TimestampsUpdatedParams - : T extends "AllocatedWithToken" - ? AllocatedWithTokenParams - : never; + : T extends "Distributed" + ? DistributedParams + : T extends "TimestampsUpdated" + ? TimestampsUpdatedParams + : T extends "AllocatedWithToken" + ? AllocatedWithTokenParams + : never; // ============================================================================= // =============================== Event Parameters ============================ // ============================================================================= export type RegisteredParams = { - contractAddress: Address; + recipientId: Address; + data: Hex; + sender: Address; +}; + +export type DistributedParams = { + recipientAddress: Address; + recipientId: Address; + sender: Address; + amount: number; }; export type TimestampsUpdatedParams = { From a180bf390f0f6aeb6ff9d06a1ae83eb3a2979bef Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:37:30 -0300 Subject: [PATCH 2/7] test: dvmd direct transfer strategy handler --- .../dvmdDirectTransfer.handler.spec.ts | 103 +++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts index daa6a88..6a6ee6a 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts @@ -1,5 +1,104 @@ -import { describe, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import type { + IProjectReadRepository, + IRoundReadRepository, +} from "@grants-stack-indexer/repository"; +import { ChainId, ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared"; + +import { UnsupportedEventException } from "../../../src/internal.js"; +import { DVMDDirectTransferHandler } from "../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; +import { + DVMDDistributedHandler, + DVMDRegisteredHandler, +} from "../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.js"; + +vi.mock( + "../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.js", + () => { + const DVMDRegisteredHandler = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + DVMDRegisteredHandler.prototype.handle = vi.fn(); + const DVMDDistributedHandler = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + DVMDDistributedHandler.prototype.handle = vi.fn(); + return { + DVMDRegisteredHandler, + DVMDDistributedHandler, + }; + }, +); describe("DVMDDirectTransferHandler", () => { - it.skip("handle the direct transfer event", () => {}); + const mockChainId = 10 as ChainId; + let handler: DVMDDirectTransferHandler; + let mockMetadataProvider: IMetadataProvider; + let mockRoundRepository: IRoundReadRepository; + let mockProjectRepository: IProjectReadRepository; + + beforeEach(() => { + mockMetadataProvider = {} as IMetadataProvider; + mockRoundRepository = {} as IRoundReadRepository; + mockProjectRepository = {} as IProjectReadRepository; + + handler = new DVMDDirectTransferHandler(mockChainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("calls RegisteredHandler for Registered event", async () => { + const mockEvent = { + eventName: "Registered", + } as ProtocolEvent<"Strategy", "Registered">; + + vi.spyOn(DVMDRegisteredHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(DVMDRegisteredHandler).toHaveBeenCalledWith(mockEvent, mockChainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + }); + expect(DVMDRegisteredHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("calls DistributedHandler for Distributed event", async () => { + const mockEvent = { + eventName: "Distributed", + } as ProtocolEvent<"Strategy", "Distributed">; + + vi.spyOn(DVMDDistributedHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(DVMDDistributedHandler).toHaveBeenCalledWith(mockEvent, mockChainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + }); + expect(DVMDDistributedHandler.prototype.handle).toHaveBeenCalled(); + }); + + it.skip("calls AllocatedHandler for Allocated event"); + it.skip("calls TimestampsUpdatedHandler for TimestampsUpdated event"); + it.skip("calls RecipientStatusUpdatedHandler for RecipientStatusUpdated event"); + it.skip("calls DistributionUpdatedHandler for DistributionUpdated event"); + it.skip("calls UpdatedRegistrationHandler for UpdatedRegistration event"); + it.skip("calls FundsDistributedHandler for FundsDistributed event"); + + it("throws UnsupportedEventException for unknown event names", async () => { + const mockEvent = { eventName: "UnknownEvent" } as unknown as ProtocolEvent< + "Strategy", + StrategyEvent + >; + await expect(() => handler.handle(mockEvent)).rejects.toThrow(UnsupportedEventException); + }); }); From 8dc11aa9540cb3815c2721fe835d9f0595eda8fd Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:42:30 -0300 Subject: [PATCH 3/7] fix: types error --- .husky/pre-commit | 2 +- packages/data-flow/test/unit/eventsFetcher.spec.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index e57b46b..ef863e8 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -pnpm lint-staged && pnpm check-types \ No newline at end of file +pnpm lint-staged && pnpm check-types --force \ No newline at end of file diff --git a/packages/data-flow/test/unit/eventsFetcher.spec.ts b/packages/data-flow/test/unit/eventsFetcher.spec.ts index c2f1dad..ee65a4e 100644 --- a/packages/data-flow/test/unit/eventsFetcher.spec.ts +++ b/packages/data-flow/test/unit/eventsFetcher.spec.ts @@ -27,7 +27,7 @@ describe("EventsFetcher", () => { eventName: "PoolCreated", srcAddress: "0x1234567890123456789012345678901234567890", logIndex: 0, - params: { contractAddress: "0x1234" }, + params: { contractAddress: "0x1234", tokenAddress: "0x1234", amount: 1000 }, transactionFields: { hash: "0x1234", transactionIndex: 0 }, }, { @@ -38,7 +38,11 @@ describe("EventsFetcher", () => { eventName: "PoolCreated", srcAddress: "0x1234567890123456789012345678901234567890", logIndex: 0, - params: { contractAddress: "0x1234" }, + params: { + contractAddress: "0x1234", + tokenAddress: "0x1234", + amount: 1000, + }, transactionFields: { hash: "0x1234", transactionIndex: 1 }, }, ]; From 72a6c5a5935905446b7f517b4754645fd8f482c5 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:22:36 -0300 Subject: [PATCH 4/7] Feat/dvmd dt event handlers (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🤖 Linear Closes GIT-63 ## Description - `Registered` and `Distributed` event handlers for Direct Voting Merkle Distribution Direct Transfer strategy - write an abstract class for Distributed since the logic is not attached to any strategy in particular, so it's reusable ## 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. --- packages/processors/src/exceptions/index.ts | 2 + .../exceptions/projectNotFound.exception.ts | 7 + .../src/exceptions/roundNotFound.exception.ts | 7 + packages/processors/src/internal.ts | 1 + .../common/baseDistributed.handler.ts | 53 +++++ .../processors/src/strategy/common/index.ts | 1 + .../dvmdDirectTransfer.handler.ts | 6 +- .../handlers/distributed.handler.ts | 19 -- .../handlers/index.ts | 1 - .../handlers/registered.handler.ts | 73 ++++++- .../helpers/decoder.ts | 41 ++++ .../types/index.ts | 11 ++ .../common/baseDistributed.handler.spec.ts | 85 ++++++++ .../dvmdDirectTransfer.handler.spec.ts | 24 +-- .../handlers/distributed.handler.spec.ts | 6 - .../handlers/registered.handler.spec.ts | 185 +++++++++++++++++- .../helpers/decoder.spec.ts | 31 +++ packages/repository/src/external.ts | 8 + .../repository/src/types/application.types.ts | 35 ++++ .../repository/src/types/changeset.types.ts | 5 + 20 files changed, 553 insertions(+), 48 deletions(-) create mode 100644 packages/processors/src/exceptions/projectNotFound.exception.ts create mode 100644 packages/processors/src/exceptions/roundNotFound.exception.ts create mode 100644 packages/processors/src/strategy/common/baseDistributed.handler.ts create mode 100644 packages/processors/src/strategy/common/index.ts delete mode 100644 packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.ts create mode 100644 packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.ts create mode 100644 packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/types/index.ts create mode 100644 packages/processors/test/strategy/common/baseDistributed.handler.spec.ts delete mode 100644 packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.spec.ts create mode 100644 packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.spec.ts create mode 100644 packages/repository/src/types/application.types.ts diff --git a/packages/processors/src/exceptions/index.ts b/packages/processors/src/exceptions/index.ts index 8d2208a..327f9db 100644 --- a/packages/processors/src/exceptions/index.ts +++ b/packages/processors/src/exceptions/index.ts @@ -2,3 +2,5 @@ export * from "./tokenPriceNotFound.exception.js"; export * from "./unsupportedEvent.exception.js"; export * from "./invalidArgument.exception.js"; export * from "./unsupportedStrategy.exception.js"; +export * from "./projectNotFound.exception.js"; +export * from "./roundNotFound.exception.js"; diff --git a/packages/processors/src/exceptions/projectNotFound.exception.ts b/packages/processors/src/exceptions/projectNotFound.exception.ts new file mode 100644 index 0000000..ba9d367 --- /dev/null +++ b/packages/processors/src/exceptions/projectNotFound.exception.ts @@ -0,0 +1,7 @@ +import { ChainId } from "@grants-stack-indexer/shared"; + +export class ProjectNotFound extends Error { + constructor(chainId: ChainId, anchorAddress: string) { + super(`Project not found for chainId: ${chainId} and anchorAddress: ${anchorAddress}`); + } +} diff --git a/packages/processors/src/exceptions/roundNotFound.exception.ts b/packages/processors/src/exceptions/roundNotFound.exception.ts new file mode 100644 index 0000000..6b68c91 --- /dev/null +++ b/packages/processors/src/exceptions/roundNotFound.exception.ts @@ -0,0 +1,7 @@ +import { ChainId } from "@grants-stack-indexer/shared"; + +export class RoundNotFound extends Error { + constructor(chainId: ChainId, strategyAddress: string) { + super(`Round not found for chainId: ${chainId} and strategyAddress: ${strategyAddress}`); + } +} diff --git a/packages/processors/src/internal.ts b/packages/processors/src/internal.ts index 9b54404..471dcf2 100644 --- a/packages/processors/src/internal.ts +++ b/packages/processors/src/internal.ts @@ -3,4 +3,5 @@ export * from "./types/index.js"; export * from "./interfaces/index.js"; export * from "./exceptions/index.js"; export * from "./allo/index.js"; +export * from "./strategy/common/index.js"; export * from "./strategy/index.js"; diff --git a/packages/processors/src/strategy/common/baseDistributed.handler.ts b/packages/processors/src/strategy/common/baseDistributed.handler.ts new file mode 100644 index 0000000..60a6dda --- /dev/null +++ b/packages/processors/src/strategy/common/baseDistributed.handler.ts @@ -0,0 +1,53 @@ +import { getAddress } from "viem"; + +import { Changeset } from "@grants-stack-indexer/repository"; +import { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; + +import { IEventHandler, ProcessorDependencies } from "../../internal.js"; + +type Dependencies = Pick; + +/** + * BaseDistributedHandler: Processes 'Distributed' events + * + * - Handles distribution events across all strategies. + * - Creates a changeset to increment the total distributed amount for a round. + * - Serves as a base class as all strategies share the same logic for this event. + * + * @dev: + * - Strategy handlers that want to handle the Distributed event should create an instance of this class corresponding to the event. + * + */ + +export class BaseDistributedHandler implements IEventHandler<"Strategy", "Distributed"> { + constructor( + readonly event: ProtocolEvent<"Strategy", "Distributed">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + async handle(): Promise { + const { roundRepository } = this.dependencies; + const strategyAddress = getAddress(this.event.srcAddress); + const round = await roundRepository.getRoundByStrategyAddress( + this.chainId, + strategyAddress, + ); + + if (!round) { + //TODO: add logging that round was not found + return []; + } + + return [ + { + type: "IncrementRoundTotalDistributed", + args: { + chainId: this.chainId, + roundId: round.id, + amount: BigInt(this.event.params.amount), + }, + }, + ]; + } +} diff --git a/packages/processors/src/strategy/common/index.ts b/packages/processors/src/strategy/common/index.ts new file mode 100644 index 0000000..f1ca948 --- /dev/null +++ b/packages/processors/src/strategy/common/index.ts @@ -0,0 +1 @@ +export * from "./baseDistributed.handler.js"; diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts index 58a792d..551d339 100644 --- a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts @@ -2,8 +2,8 @@ import { Changeset } from "@grants-stack-indexer/repository"; import { ChainId, ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared"; import type { IStrategyHandler, ProcessorDependencies } from "../../internal.js"; -import { UnsupportedEventException } from "../../internal.js"; -import { DVMDDistributedHandler, DVMDRegisteredHandler } from "./handlers/index.js"; +import { BaseDistributedHandler, UnsupportedEventException } from "../../internal.js"; +import { DVMDRegisteredHandler } from "./handlers/index.js"; type Dependencies = Pick< ProcessorDependencies, @@ -33,7 +33,7 @@ export class DVMDDirectTransferHandler implements IStrategyHandler, this.chainId, this.dependencies, diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.ts deleted file mode 100644 index 58316b1..0000000 --- a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Changeset } from "@grants-stack-indexer/repository"; -import { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; - -import { IEventHandler, ProcessorDependencies } from "../../../internal.js"; - -type Dependencies = Pick; - -export class DVMDDistributedHandler implements IEventHandler<"Strategy", "Distributed"> { - constructor( - readonly event: ProtocolEvent<"Strategy", "Distributed">, - private readonly chainId: ChainId, - private readonly dependencies: Dependencies, - ) {} - - async handle(): Promise { - //TODO: Implement - throw new Error("Method not implemented."); - } -} diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.ts index ccd560a..00c2f86 100644 --- a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.ts +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.ts @@ -1,2 +1 @@ -export * from "./distributed.handler.js"; export * from "./registered.handler.js"; diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts index 42e4f8a..6631aec 100644 --- a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts @@ -1,7 +1,15 @@ -import { Changeset } from "@grants-stack-indexer/repository"; +import { getAddress } from "viem"; + +import { Changeset, NewApplication } from "@grants-stack-indexer/repository"; import { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; -import { IEventHandler, ProcessorDependencies } from "../../../internal.js"; +import { + IEventHandler, + ProcessorDependencies, + ProjectNotFound, + RoundNotFound, +} from "../../../internal.js"; +import { decodeDVMDApplicationData } from "../helpers/decoder.js"; type Dependencies = Pick< ProcessorDependencies, @@ -16,7 +24,64 @@ export class DVMDRegisteredHandler implements IEventHandler<"Strategy", "Registe ) {} async handle(): Promise { - //TODO: Implement - throw new Error("Not implemented"); + const { projectRepository, roundRepository, metadataProvider } = this.dependencies; + const { data: encodedData, recipientId, sender } = this.event.params; + const { blockNumber, blockTimestamp } = this.event; + + const anchorAddress = getAddress(recipientId); + const project = await projectRepository.getProjectByAnchor(this.chainId, anchorAddress); + + if (!project) { + throw new ProjectNotFound(this.chainId, anchorAddress); + } + + const strategyAddress = getAddress(this.event.srcAddress); + const round = await roundRepository.getRoundByStrategyAddress( + this.chainId, + strategyAddress, + ); + + if (!round) { + throw new RoundNotFound(this.chainId, strategyAddress); + } + + const values = decodeDVMDApplicationData(encodedData); + // ID is defined as recipientsCounter - 1, which is a value emitted by the strategy + const id = (Number(values.recipientsCounter) - 1).toString(); + + const metadata = await metadataProvider.getMetadata(values.metadata.pointer); + + const application: NewApplication = { + chainId: this.chainId, + id: id, + projectId: project.id, + anchorAddress, + roundId: round.id, + status: "PENDING", + metadataCid: values.metadata.pointer, + metadata: metadata ?? null, + createdAtBlock: BigInt(blockNumber), + createdByAddress: getAddress(sender), + statusUpdatedAtBlock: BigInt(blockNumber), + statusSnapshots: [ + { + status: "PENDING", + updatedAtBlock: blockNumber.toString(), + updatedAt: new Date(blockTimestamp * 1000), // timestamp is in seconds, convert to ms + }, + ], + distributionTransaction: null, + totalAmountDonatedInUsd: 0, + totalDonationsCount: 0, + uniqueDonorsCount: 0, + tags: ["allo-v2"], + }; + + return [ + { + type: "InsertApplication", + args: application, + }, + ]; } } diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.ts new file mode 100644 index 0000000..f9378ac --- /dev/null +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.ts @@ -0,0 +1,41 @@ +import { decodeAbiParameters, Hex } from "viem"; + +import { Address } from "@grants-stack-indexer/shared"; + +import { DVMDApplicationData } from "../types/index.js"; + +const DVMD_EVENT_DATA_DECODER = [ + { name: "data", type: "bytes" }, + { name: "recipientsCounter", type: "uint256" }, +] as const; + +const DVMD_DATA_DECODER = [ + { name: "registryAnchor", type: "address" }, + { name: "recipientAddress", type: "address" }, + { + name: "metadata", + type: "tuple", + components: [ + { name: "protocol", type: "uint256" }, + { name: "pointer", type: "string" }, + ], + }, +] as const; + +export const decodeDVMDApplicationData = (encodedData: Hex): DVMDApplicationData => { + const values = decodeAbiParameters(DVMD_EVENT_DATA_DECODER, encodedData); + + const decodedData = decodeAbiParameters(DVMD_DATA_DECODER, values[0]); + + const results: DVMDApplicationData = { + recipientsCounter: values[1].toString(), + anchorAddress: decodedData[0] as Address, + recipientAddress: decodedData[1] as Address, + metadata: { + protocol: Number(decodedData[2].protocol), + pointer: decodedData[2].pointer, + }, + }; + + return results; +}; diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/types/index.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/types/index.ts new file mode 100644 index 0000000..10f42ff --- /dev/null +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/types/index.ts @@ -0,0 +1,11 @@ +import { Address } from "@grants-stack-indexer/shared"; + +export type DVMDApplicationData = { + recipientsCounter: string; + anchorAddress: Address; + recipientAddress: Address; + metadata: { + protocol: number; + pointer: string; + }; +}; diff --git a/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts b/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts new file mode 100644 index 0000000..395528d --- /dev/null +++ b/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts @@ -0,0 +1,85 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { IRoundReadRepository, Round } from "@grants-stack-indexer/repository"; +import { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; + +import { BaseDistributedHandler } from "../../../src/strategy/common/baseDistributed.handler.js"; + +function createMockEvent( + overrides: Partial> = {}, +): ProtocolEvent<"Strategy", "Distributed"> { + const defaultEvent: ProtocolEvent<"Strategy", "Distributed"> = { + params: { + amount: 1000, + recipientAddress: "0x1234567890123456789012345678901234567890", + recipientId: "0x1234567890123456789012345678901234567890", + sender: "0x1234567890123456789012345678901234567890", + }, + eventName: "Distributed", + srcAddress: "0x1234567890123456789012345678901234567890", + blockNumber: 12345, + blockTimestamp: 1000000000, + chainId: 10 as ChainId, + contractName: "Strategy", + logIndex: 1, + transactionFields: { + hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", + transactionIndex: 6, + from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + }, + strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", + }; + + return { ...defaultEvent, ...overrides }; +} + +describe("BaseDistributedHandler", () => { + let handler: BaseDistributedHandler; + let mockRoundRepository: IRoundReadRepository; + let mockEvent: ProtocolEvent<"Strategy", "Distributed">; + const chainId = 10 as ChainId; + + beforeEach(() => { + mockRoundRepository = { + getRoundByStrategyAddress: vi.fn(), + } as unknown as IRoundReadRepository; + }); + + it("increment round total distributed when round is found", async () => { + mockEvent = createMockEvent(); + const mockRound = { id: "round1" } as Round; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); + + handler = new BaseDistributedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + + expect(result).toEqual([ + { + type: "IncrementRoundTotalDistributed", + args: { + chainId, + roundId: "round1", + amount: BigInt(mockEvent.params.amount), + }, + }, + ]); + }); + + it("returns an empty array when round is not found", async () => { + mockEvent = createMockEvent(); + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(undefined); + + handler = new BaseDistributedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + + expect(result).toEqual([]); + }); +}); diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts index 6a6ee6a..d6acbb6 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts @@ -8,11 +8,9 @@ import type { import { ChainId, ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared"; import { UnsupportedEventException } from "../../../src/internal.js"; +import { BaseDistributedHandler } from "../../../src/strategy/common/index.js"; import { DVMDDirectTransferHandler } from "../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; -import { - DVMDDistributedHandler, - DVMDRegisteredHandler, -} from "../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.js"; +import { DVMDRegisteredHandler } from "../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.js"; vi.mock( "../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.js", @@ -20,15 +18,19 @@ vi.mock( const DVMDRegisteredHandler = vi.fn(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access DVMDRegisteredHandler.prototype.handle = vi.fn(); - const DVMDDistributedHandler = vi.fn(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - DVMDDistributedHandler.prototype.handle = vi.fn(); return { DVMDRegisteredHandler, - DVMDDistributedHandler, }; }, ); +vi.mock("../../../src/strategy/common/baseDistributed.handler.js", () => { + const BaseDistributedHandler = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + BaseDistributedHandler.prototype.handle = vi.fn(); + return { + BaseDistributedHandler, + }; +}); describe("DVMDDirectTransferHandler", () => { const mockChainId = 10 as ChainId; @@ -75,16 +77,16 @@ describe("DVMDDirectTransferHandler", () => { eventName: "Distributed", } as ProtocolEvent<"Strategy", "Distributed">; - vi.spyOn(DVMDDistributedHandler.prototype, "handle").mockResolvedValue([]); + vi.spyOn(BaseDistributedHandler.prototype, "handle").mockResolvedValue([]); await handler.handle(mockEvent); - expect(DVMDDistributedHandler).toHaveBeenCalledWith(mockEvent, mockChainId, { + expect(BaseDistributedHandler).toHaveBeenCalledWith(mockEvent, mockChainId, { metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, projectRepository: mockProjectRepository, }); - expect(DVMDDistributedHandler.prototype.handle).toHaveBeenCalled(); + expect(BaseDistributedHandler.prototype.handle).toHaveBeenCalled(); }); it.skip("calls AllocatedHandler for Allocated event"); diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.spec.ts deleted file mode 100644 index bfa3262..0000000 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/distributed.handler.spec.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { describe } from "node:test"; -import { it } from "vitest"; - -describe("DVMDDirectTransferDistributedHandler", () => { - it.skip("handle the distributed event", () => {}); -}); diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts index 519383d..ff494ee 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts @@ -1,6 +1,183 @@ -import { describe } from "node:test"; -import { it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; -describe("DVMDDirectTransferRegisteredHandler", () => { - it.skip("handle the registered event", () => {}); +import { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import { + IProjectReadRepository, + IRoundReadRepository, + NewApplication, + Project, + Round, +} from "@grants-stack-indexer/repository"; +import { ChainId, DeepPartial, mergeDeep, ProtocolEvent } from "@grants-stack-indexer/shared"; + +import { ProjectNotFound, RoundNotFound } from "../../../../src/exceptions/index.js"; +import { DVMDRegisteredHandler } from "../../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.js"; + +function createMockEvent( + overrides: DeepPartial> = {}, +): ProtocolEvent<"Strategy", "Registered"> { + const defaultEvent: ProtocolEvent<"Strategy", "Registered"> = { + params: { + recipientId: "0x1234567890123456789012345678901234567890", + sender: "0x0987654321098765432109876543210987654321", + data: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001000000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000", + }, + eventName: "Registered", + srcAddress: "0x1234567890123456789012345678901234567890", + blockNumber: 12345, + blockTimestamp: 1000000000, + chainId: 10 as ChainId, + contractName: "Strategy", + logIndex: 1, + transactionFields: { + hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", + transactionIndex: 6, + from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + }, + strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", + }; + + return mergeDeep(defaultEvent, overrides) as ProtocolEvent<"Strategy", "Registered">; +} + +describe("DVMDRegisteredHandler", () => { + let handler: DVMDRegisteredHandler; + let mockRoundRepository: IRoundReadRepository; + let mockProjectRepository: IProjectReadRepository; + let mockMetadataProvider: IMetadataProvider; + let mockEvent: ProtocolEvent<"Strategy", "Registered">; + const chainId = 10 as ChainId; + + beforeEach(() => { + mockRoundRepository = { + getRoundByStrategyAddress: vi.fn(), + } as unknown as IRoundReadRepository; + mockProjectRepository = { + getProjectByAnchor: vi.fn(), + } as unknown as IProjectReadRepository; + mockMetadataProvider = { + getMetadata: vi.fn(), + } as unknown as IMetadataProvider; + }); + + it("handle a valid registration event", async () => { + mockEvent = createMockEvent(); + const mockProject = { id: "project1" } as Project; + const mockRound = { id: "round1" } as Round; + const mockMetadata = { name: "Test Project" }; + + vi.spyOn(mockProjectRepository, "getProjectByAnchor").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(mockMetadata); + + handler = new DVMDRegisteredHandler(mockEvent, chainId, { + projectRepository: mockProjectRepository, + roundRepository: mockRoundRepository, + metadataProvider: mockMetadataProvider, + }); + + const result = await handler.handle(); + + expect(result).toEqual([ + { + type: "InsertApplication", + args: { + chainId, + id: "0", + projectId: "project1", + anchorAddress: "0x1234567890123456789012345678901234567890", + roundId: "round1", + status: "PENDING", + metadataCid: "bafkreigyc43cfinxlnahq5aawsghibetv6usrs7kkxf7vxmzbjyro76iwy", + metadata: mockMetadata, + createdAtBlock: BigInt(mockEvent.blockNumber), + createdByAddress: "0x0987654321098765432109876543210987654321", + statusUpdatedAtBlock: BigInt(mockEvent.blockNumber), + statusSnapshots: [ + { + status: "PENDING", + updatedAtBlock: mockEvent.blockNumber.toString(), + updatedAt: new Date(mockEvent.blockTimestamp * 1000), + }, + ], + distributionTransaction: null, + totalAmountDonatedInUsd: 0, + totalDonationsCount: 0, + uniqueDonorsCount: 0, + tags: ["allo-v2"], + }, + }, + ]); + }); + + it("throw ProjectNotFound if project is not found", async () => { + mockEvent = createMockEvent(); + vi.spyOn(mockProjectRepository, "getProjectByAnchor").mockResolvedValue(undefined); + + handler = new DVMDRegisteredHandler(mockEvent, chainId, { + projectRepository: mockProjectRepository, + roundRepository: mockRoundRepository, + metadataProvider: mockMetadataProvider, + }); + await expect(handler.handle()).rejects.toThrow(ProjectNotFound); + }); + + it("throw RoundNotFound if round is not found", async () => { + mockEvent = createMockEvent(); + const mockProject = { id: "project1" } as Project; + vi.spyOn(mockProjectRepository, "getProjectByAnchor").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(undefined); + + handler = new DVMDRegisteredHandler(mockEvent, chainId, { + projectRepository: mockProjectRepository, + roundRepository: mockRoundRepository, + metadataProvider: mockMetadataProvider, + }); + await expect(handler.handle()).rejects.toThrow(RoundNotFound); + }); + + it("handle registration with null metadata", async () => { + mockEvent = createMockEvent(); + const mockProject = { id: "project1" } as Project; + const mockRound = { id: "round1" } as Round; + + vi.spyOn(mockProjectRepository, "getProjectByAnchor").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); + + handler = new DVMDRegisteredHandler(mockEvent, chainId, { + projectRepository: mockProjectRepository, + roundRepository: mockRoundRepository, + metadataProvider: mockMetadataProvider, + }); + const result = await handler.handle(); + + const changeset = result[0] as { type: "InsertApplication"; args: NewApplication }; + expect(result).toBeDefined(); + expect(changeset.args.metadata).toBeNull(); + }); + + it("correctly calculate application ID based on recipientsCounter", async () => { + // recipientsCounter = 5 + const mockEvent = createMockEvent({ + params: { + data: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000001000000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000", + }, + }); + const mockProject = { id: "project1" } as Project; + const mockRound = { id: "round1" } as Round; + + vi.spyOn(mockProjectRepository, "getProjectByAnchor").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); + + handler = new DVMDRegisteredHandler(mockEvent, chainId, { + projectRepository: mockProjectRepository, + roundRepository: mockRoundRepository, + metadataProvider: mockMetadataProvider, + }); + const result = await handler.handle(); + + const changeset = result[0] as { type: "InsertApplication"; args: NewApplication }; + expect(changeset.args.id).toBe("4"); + }); }); diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.spec.ts new file mode 100644 index 0000000..51aa787 --- /dev/null +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; + +import { decodeDVMDApplicationData } from "../../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.js"; +import { DVMDApplicationData } from "../../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/types/index.js"; + +describe("decodeDVMDApplicationData", () => { + it("should correctly decode the encoded data", () => { + const encodedData = + "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001000000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000"; + + const expectedResult: DVMDApplicationData = { + recipientsCounter: "1", + anchorAddress: "0x2c7296a5eC0539f0A018C7176c97c92A9C44E2B4", + recipientAddress: "0xE7eB5D2b5b188777df902e89c54570E7Ef4F59CE", + metadata: { + protocol: 1, + pointer: "bafkreigyc43cfinxlnahq5aawsghibetv6usrs7kkxf7vxmzbjyro76iwy", + }, + }; + + const result = decodeDVMDApplicationData(encodedData); + + expect(result).toEqual(expectedResult); + }); + + it("throw an error for invalid encoded data", () => { + const invalidEncodedData = "0x1234"; + + expect(() => decodeDVMDApplicationData(invalidEncodedData)).toThrow(); + }); +}); diff --git a/packages/repository/src/external.ts b/packages/repository/src/external.ts index a37b59b..5a6ea55 100644 --- a/packages/repository/src/external.ts +++ b/packages/repository/src/external.ts @@ -25,6 +25,14 @@ export type { PendingRoundRole, } from "./types/round.types.js"; +export type { + ApplicationStatus, + StatusSnapshot, + Application, + NewApplication, + PartialApplication, +} from "./types/application.types.js"; + export type { Changeset } from "./types/index.js"; export { KyselyRoundRepository, KyselyProjectRepository } from "./repositories/kysely/index.js"; diff --git a/packages/repository/src/types/application.types.ts b/packages/repository/src/types/application.types.ts new file mode 100644 index 0000000..62453d1 --- /dev/null +++ b/packages/repository/src/types/application.types.ts @@ -0,0 +1,35 @@ +import { Address, ChainId } from "@grants-stack-indexer/shared"; + +export type ApplicationStatus = "PENDING" | "REJECTED" | "APPROVED"; + +export type StatusSnapshot = { + status: ApplicationStatus; + updatedAtBlock: string; + updatedAt: Date; +}; + +export type Application = { + id: string; + chainId: ChainId; + roundId: Address | string; + projectId: string; + anchorAddress: Address | null; + status: ApplicationStatus; + statusSnapshots: StatusSnapshot[] | string; + distributionTransaction: string | null; + metadataCid: string | null; + metadata: unknown | null; + createdByAddress: Address; + createdAtBlock: bigint; + statusUpdatedAtBlock: bigint; + totalDonationsCount: number; + totalAmountDonatedInUsd: number; + uniqueDonorsCount: number; + + tags: string[]; +}; + +export type NewApplication = Application; +export type PartialApplication = Partial; + +//TODO: create the corresponding repository implementation diff --git a/packages/repository/src/types/changeset.types.ts b/packages/repository/src/types/changeset.types.ts index 526332c..3704fde 100644 --- a/packages/repository/src/types/changeset.types.ts +++ b/packages/repository/src/types/changeset.types.ts @@ -1,5 +1,6 @@ import type { Address, ChainId } from "@grants-stack-indexer/shared"; +import { NewApplication } from "./application.types.js"; import { NewPendingProjectRole, NewProject, @@ -139,4 +140,8 @@ export type Changeset = args: { roundRole: Pick; }; + } + | { + type: "InsertApplication"; + args: NewApplication; }; From 8c254280cc3d3ad81aad4d305c09a07d572940e6 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:57:51 -0300 Subject: [PATCH 5/7] feat: base strategy class and refactor strategyId mappings --- .../src/allo/handlers/poolCreated.handler.ts | 45 ++-- packages/processors/src/external.ts | 2 + packages/processors/src/helpers/strategy.ts | 224 +----------------- .../interfaces/strategyHandler.interface.ts | 39 ++- packages/processors/src/internal.ts | 12 +- .../src/strategy/common/base.strategy.ts | 41 ++++ .../processors/src/strategy/common/index.ts | 1 + .../dvmdDirectTransfer.handler.ts | 118 ++++++++- packages/processors/src/strategy/index.ts | 4 + packages/processors/src/strategy/mapping.ts | 33 +++ .../src/strategy/strategy.processor.ts | 11 +- .../src/strategy/strategyHandler.factory.ts | 33 ++- .../processors/src/types/strategy.types.ts | 18 ++ .../strategy/common/base.strategy.spec.ts | 63 +++++ .../common/baseDistributed.handler.spec.ts | 2 +- .../dvmdDirectTransfer.handler.spec.ts | 144 ++++++++++- .../processors/test/strategy/mapping.spec.ts | 69 ++++++ .../test/strategy/strategy.processor.spec.ts | 48 ++++ .../strategy/strategyHandler.factory.spec.ts | 23 +- 19 files changed, 635 insertions(+), 295 deletions(-) create mode 100644 packages/processors/src/strategy/common/base.strategy.ts create mode 100644 packages/processors/src/strategy/mapping.ts create mode 100644 packages/processors/test/strategy/common/base.strategy.spec.ts create mode 100644 packages/processors/test/strategy/mapping.spec.ts create mode 100644 packages/processors/test/strategy/strategy.processor.spec.ts diff --git a/packages/processors/src/allo/handlers/poolCreated.handler.ts b/packages/processors/src/allo/handlers/poolCreated.handler.ts index e46f915..2661237 100644 --- a/packages/processors/src/allo/handlers/poolCreated.handler.ts +++ b/packages/processors/src/allo/handlers/poolCreated.handler.ts @@ -1,4 +1,4 @@ -import { getAddress, parseUnits, zeroAddress } from "viem"; +import { getAddress, zeroAddress } from "viem"; import type { Changeset, NewRound, PendingRoundRole } from "@grants-stack-indexer/repository"; import type { ChainId, ProtocolEvent, Token } from "@grants-stack-indexer/shared"; @@ -7,10 +7,10 @@ import { getToken } from "@grants-stack-indexer/shared/dist/src/internal.js"; import type { IEventHandler, ProcessorDependencies, StrategyTimings } from "../../internal.js"; import { getRoundRoles } from "../../helpers/roles.js"; -import { extractStrategyFromId, getStrategyTimings } from "../../helpers/strategy.js"; import { calculateAmountInUsd } from "../../helpers/tokenMath.js"; import { TokenPriceNotFoundError } from "../../internal.js"; import { RoundMetadataSchema } from "../../schemas/index.js"; +import { StrategyHandlerFactory } from "../../strategy/strategyHandler.factory.js"; type Dependencies = Pick< ProcessorDependencies, @@ -61,7 +61,13 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> ? zeroAddress : checksummedTokenAddress; - const strategy = extractStrategyFromId(strategyId); + const strategyHandler = StrategyHandlerFactory.createHandler( + this.chainId, + this.dependencies as ProcessorDependencies, + strategyId, + ); + + // const strategy = extractStrategyFromId(strategyId); const token = getToken(this.chainId, matchTokenAddress); @@ -72,26 +78,17 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> donationsEndTime: null, }; - let matchAmount = 0n; - let matchAmountInUsd = "0"; - - if (strategy) { - strategyTimings = await getStrategyTimings(evmProvider, strategy, strategyAddress); - - //TODO: when creating strategy handlers, should this be moved there? - if ( - strategy.name === "allov2.DonationVotingMerkleDistributionDirectTransferStrategy" && - parsedRoundMetadata.success && - token - ) { - matchAmount = parseUnits( - parsedRoundMetadata.data.quadraticFundingConfig.matchingFundsAvailable.toString(), - token.decimals, - ); + let matchAmount = { + matchAmount: 0n, + matchAmountInUsd: "0", + }; - matchAmountInUsd = await this.getTokenAmountInUsd( + if (strategyHandler) { + strategyTimings = await strategyHandler.fetchStrategyTimings(strategyAddress); + if (parsedRoundMetadata.success && token) { + matchAmount = await strategyHandler.fetchMatchAmount( + Number(parsedRoundMetadata.data.quadraticFundingConfig.matchingFundsAvailable), token, - matchAmount, this.event.blockTimestamp, ); } @@ -120,8 +117,8 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> totalAmountDonatedInUsd: "0", uniqueDonorsCount: 0, matchTokenAddress, - matchAmount, - matchAmountInUsd, + matchAmount: matchAmount.matchAmount, + matchAmountInUsd: matchAmount.matchAmountInUsd, fundedAmount, fundedAmountInUsd, applicationMetadataCid: metadataPointer, @@ -132,7 +129,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> ...roundRoles, strategyAddress, strategyId, - strategyName: strategy?.name ?? "", + strategyName: strategyHandler?.name ?? "", createdByAddress: getAddress(createdBy), createdAtBlock: BigInt(this.event.blockNumber), updatedAtBlock: BigInt(this.event.blockNumber), diff --git a/packages/processors/src/external.ts b/packages/processors/src/external.ts index a898320..02e1abc 100644 --- a/packages/processors/src/external.ts +++ b/packages/processors/src/external.ts @@ -1,3 +1,5 @@ // Add your external exports here export { StrategyProcessor, AlloProcessor } from "./internal.js"; export type { IProcessor } from "./internal.js"; + +export { existsHandler } from "./internal.js"; diff --git a/packages/processors/src/helpers/strategy.ts b/packages/processors/src/helpers/strategy.ts index a71937c..2acd549 100644 --- a/packages/processors/src/helpers/strategy.ts +++ b/packages/processors/src/helpers/strategy.ts @@ -1,231 +1,11 @@ import type { EvmProvider } from "@grants-stack-indexer/chain-providers"; -import type { Address, Branded } from "@grants-stack-indexer/shared"; +import type { Address } from "@grants-stack-indexer/shared"; import DirectGrantsLiteStrategy from "../abis/allo-v2/v1/DirectGrantsLiteStrategy.js"; -import DonationVotingMerkleDistributionDirectTransferStrategy from "../abis/allo-v2/v1/DonationVotingMerkleDistributionDirectTransferStrategy.js"; import { StrategyTimings } from "../internal.js"; import { getDateFromTimestamp } from "./utils.js"; -type SanitizedStrategyId = Branded; -type Strategy = { - id: SanitizedStrategyId; - name: string | null; - // TODO: check if groups are required - groups: string[]; -}; - -//TODO: refactor this into a mapping in Shared package from ID to the corresponding handler class -/* - * Extracts the strategy from the ID. - * @param _id - The ID of the strategy. - * @returns The strategy. - */ -export function extractStrategyFromId(_id: Address): Strategy | undefined { - const id = _id.toLowerCase(); - /* eslint-disable no-fallthrough */ - switch (id) { - // SQFSuperfluidv1 - case "0xf8a14294e80ff012e54157ec9d1b2827421f1e7f6bde38c06730b1c031b3f935": - return { - id: id as SanitizedStrategyId, - name: "allov2.SQFSuperFluidStrategy", - groups: ["allov2.SQFSuperFluidStrategy"], - }; - - // MicroGrantsv1 - case "0x697f0592ebd05466d2d24454477e11d69c475d7a7c4134f15ddc1ea9811bb16f": - return { - id: id as SanitizedStrategyId, - name: "allov2.MicroGrantsStrategy", - groups: ["allov2.MicroGrantsStrategy", "allov2.MicroGrantsCommon"], - }; - - // MicroGrantsGovv1 - case "0x741ac1e2f387d83f219f6b5349d35ec34902cf94019d117335e0045d2e0ed912": - return { - id: id as SanitizedStrategyId, - name: "allov2.MicroGrantsGovStrategy", - groups: ["allov2.MicroGrantsGovStrategy", "allov2.MicroGrantsCommon"], - }; - - // MicroGrantsHatsv1 - case "0x5aa24dcfcd55a1e059a172e987b3456736b4856c71e57aaf52e9a965897318dd": - return { - id: id as SanitizedStrategyId, - name: "allov2.MicroGrantsHatsStrategy", - groups: ["allov2.MicroGrantsHatsStrategy", "allov2.MicroGrantsCommon"], - }; - - // RFPSimpleStrategyv1.0 - case "0x0d459e12d9e91d2b2a8fa12be8c7eb2b4f1c35e74573990c34b436613bc2350f": - return { - id: id as SanitizedStrategyId, - name: "allov2.RFPSimpleStrategy", - groups: ["allov2.RFPSimpleStrategy"], - }; - - // RFPCommitteeStrategyv1.0 - case "0x7d143166a83c6a8a303ae32a6ccd287e48d79818f5d15d89e185391199909803": - return { - id: id as SanitizedStrategyId, - name: "allov2.RFPCommitteeStrategy", - groups: ["allov2.RFPCommitteeStrategy"], - }; - - // QVSimpleStrategyv1.0 - case "0x22d006e191d6dc5ff1a25bb0733f47f64a9c34860b6703df88dea7cb3987b4c3": - return { - id: id as SanitizedStrategyId, - name: "allov2.QVSimpleStrategy", - groups: ["allov2.QVSimpleStrategy"], - }; - - // DonationVotingMerkleDistributionDirectTransferStrategyv1.0 - case "0x6f9291df02b2664139cec5703c124e4ebce32879c74b6297faa1468aa5ff9ebf": - // DonationVotingMerkleDistributionDirectTransferStrategyv1.1 - case "0x2f46bf157821dc41daa51479e94783bb0c8699eac63bf75ec450508ab03867ce": - // DonationVotingMerkleDistributionDirectTransferStrategyv2.0 - case "0x2f0250d534b2d59b8b5cfa5eb0d0848a59ccbf5de2eaf72d2ba4bfe73dce7c6b": - // DonationVotingMerkleDistributionDirectTransferStrategyv2.1 - case "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0": - return { - id: id as SanitizedStrategyId, - name: "allov2.DonationVotingMerkleDistributionDirectTransferStrategy", - groups: ["allov2.DonationVotingMerkleDistributionDirectTransferStrategy"], - }; - - // DonationVotingMerkleDistributionVaultStrategyv1.0 - case "0x7e75375f0a7cd9f7ea159c8b065976e4f764f9dcef1edf692f31dd1842f70c87": - // DonationVotingMerkleDistributionVaultStrategyv1.1 - case "0x093072375737c0e8872fef36808849aeba7f865e182d495f2b98308115c9ef13": - return { - id: id as SanitizedStrategyId, - name: "allov2.DonationVotingMerkleDistributionVaultStrategy", - groups: ["allov2.DonationVotingMerkleDistributionVaultStrategy"], - }; - - // DirectGrantsSimpleStrategyv1.1 - case "0x263cb916541b6fc1fb5543a244829ccdba75264b097726e6ecc3c3cfce824bf5": - // DirectGrantsSimpleStrategyv2.1 - case "0x53fb9d3bce0956ca2db5bb1441f5ca23050cb1973b33789e04a5978acfd9ca93": - return { - id: id as SanitizedStrategyId, - name: "allov2.DirectGrantsSimpleStrategy", - groups: ["allov2.DirectGrantsSimpleStrategy"], - }; - - // DirectGrantsLiteStrategyv1.0 - case "0x103732a8e473467a510d4128ee11065262bdd978f0d9dad89ba68f2c56127e27": - return { - id: id as SanitizedStrategyId, - name: "allov2.DirectGrantsLiteStrategy", - groups: ["allov2.DirectGrantsLiteStrategy"], - }; - - // EasyRPGFStrategy1.0 - case "0x662f5a0d3ea7e9b6ed1b351a9d96ac636a3c3ed727390aeff4ec931ae760d5ae": - return { - id: id as SanitizedStrategyId, - name: "allov2.EasyRPGFStrategy", - groups: ["allov2.EasyRPGFStrategy"], - }; - - // DirectAllocationStrategyv1.1 - case "0x4cd0051913234cdd7d165b208851240d334786d6e5afbb4d0eec203515a9c6f3": - return { - id: id as SanitizedStrategyId, - name: "allov2.DirectAllocationStrategy", - groups: ["allov2.DirectAllocationStrategy"], - }; - } - - return undefined; -} - -//TODO: refactor this into the StrategyHandler when implemented -// see if we can use a common interface or abstract class for all strategies -// so we don't have to do this switch statement -// most of the strategies don't need to fetch anything and just return null for all the times -export const getStrategyTimings = async ( - evmProvider: EvmProvider, - strategy: Strategy, - strategyAddress: Address, -): Promise => { - switch (strategy.name) { - case "allov2.DonationVotingMerkleDistributionDirectTransferStrategy": - return getDonationVotingMerkleDistributionDirectTransferStrategyTimings( - evmProvider, - strategyAddress, - ); - case "allov2.DirectGrantsSimpleStrategy": - case "allov2.DirectGrantsLiteStrategy": - return getDirectGrantsStrategyTimings(evmProvider, strategyAddress); - default: - return { - applicationsStartTime: null, - applicationsEndTime: null, - donationsStartTime: null, - donationsEndTime: null, - }; - } -}; - -/** - * Gets the strategy data for the DonationVotingMerkleDistributionDirectTransferStrategy - * @param evmProvider - The evm provider - * @param strategyId - The address of the strategy - * @returns The strategy data - */ -export const getDonationVotingMerkleDistributionDirectTransferStrategyTimings = async ( - evmProvider: EvmProvider, - strategyId: Address, -): Promise => { - let results: [bigint, bigint, bigint, bigint] = [0n, 0n, 0n, 0n]; - - const contractCalls = [ - { - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "registrationStartTime", - address: strategyId, - }, - { - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "registrationEndTime", - address: strategyId, - }, - { - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "allocationStartTime", - address: strategyId, - }, - { - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "allocationEndTime", - address: strategyId, - }, - ] as const; - - if (evmProvider.getMulticall3Address()) { - results = await evmProvider.multicall({ - contracts: contractCalls, - allowFailure: false, - }); - } else { - results = (await Promise.all( - contractCalls.map((call) => - evmProvider.readContract(call.address, call.abi, call.functionName), - ), - )) as [bigint, bigint, bigint, bigint]; - } - - return { - applicationsStartTime: getDateFromTimestamp(results[0]), - applicationsEndTime: getDateFromTimestamp(results[1]), - donationsStartTime: getDateFromTimestamp(results[2]), - donationsEndTime: getDateFromTimestamp(results[3]), - }; -}; - +//TODO: move this to the DirectGrantsStrategyHandler when implemented /** * Gets the strategy data for the DirectGrantsStrategy * @param evmProvider - The evm provider diff --git a/packages/processors/src/interfaces/strategyHandler.interface.ts b/packages/processors/src/interfaces/strategyHandler.interface.ts index d5c2ec4..5e0c047 100644 --- a/packages/processors/src/interfaces/strategyHandler.interface.ts +++ b/packages/processors/src/interfaces/strategyHandler.interface.ts @@ -1,5 +1,12 @@ -import { Changeset } from "@grants-stack-indexer/repository"; -import { ContractToEventName, ProtocolEvent } from "@grants-stack-indexer/shared"; +import type { Changeset } from "@grants-stack-indexer/repository"; +import type { + Address, + ContractToEventName, + ProtocolEvent, + Token, +} from "@grants-stack-indexer/shared"; + +import type { StrategyTimings } from "../internal.js"; /** * Interface for an event handler. @@ -7,9 +14,37 @@ import { ContractToEventName, ProtocolEvent } from "@grants-stack-indexer/shared * @template E - The event name. */ export interface IStrategyHandler> { + /** + * The name of the strategy. + */ + name: string; + /** * Handles the event. * @returns A promise that resolves to an array of changesets. */ handle(event: ProtocolEvent<"Strategy", E>): Promise; + + /** + * Fetch the strategy timings data from the strategy contract + * @param strategyAddress - The address of the strategy + * @returns The strategy timings + */ + fetchStrategyTimings(strategyAddress: Address): Promise; + + /** + * Fetch the match amount for a strategy + * @param matchingFundsAvailable - The matching funds available + * @param token - The token + * @param blockTimestamp - The block timestamp + * @returns The match amount and match amount in USD + */ + fetchMatchAmount( + matchingFundsAvailable: number, + token: Token, + blockTimestamp: number, + ): Promise<{ + matchAmount: bigint; + matchAmountInUsd: string; + }>; } diff --git a/packages/processors/src/internal.ts b/packages/processors/src/internal.ts index 471dcf2..fa8511a 100644 --- a/packages/processors/src/internal.ts +++ b/packages/processors/src/internal.ts @@ -1,7 +1,15 @@ -// Add your internal exports here +// Types and interfaces export * from "./types/index.js"; export * from "./interfaces/index.js"; + +// Exceptions export * from "./exceptions/index.js"; + +// Allo export * from "./allo/index.js"; + +// Strategy export * from "./strategy/common/index.js"; -export * from "./strategy/index.js"; +export * from "./strategy/strategyHandler.factory.js"; +export * from "./strategy/strategy.processor.js"; +export { getHandler, existsHandler } from "./strategy/mapping.js"; diff --git a/packages/processors/src/strategy/common/base.strategy.ts b/packages/processors/src/strategy/common/base.strategy.ts new file mode 100644 index 0000000..aaab7a8 --- /dev/null +++ b/packages/processors/src/strategy/common/base.strategy.ts @@ -0,0 +1,41 @@ +import { Changeset } from "@grants-stack-indexer/repository"; +import { Address, ProtocolEvent, StrategyEvent, Token } from "@grants-stack-indexer/shared"; + +import { IStrategyHandler, StrategyTimings } from "../../internal.js"; + +/** + * @abstract + * Base class for all strategy handlers. + * + * Implementations of this class should be named like `StrategyHandler`. + * + */ +export abstract class BaseStrategyHandler implements IStrategyHandler { + readonly name: string; + + constructor(name: string) { + this.name = name; + } + + async fetchStrategyTimings(_strategyAddress: Address): Promise { + return { + applicationsStartTime: null, + applicationsEndTime: null, + donationsStartTime: null, + donationsEndTime: null, + }; + } + + async fetchMatchAmount( + _matchingFundsAvailable: number, + _token: Token, + _blockTimestamp: number, + ): Promise<{ matchAmount: bigint; matchAmountInUsd: string }> { + return { + matchAmount: 0n, + matchAmountInUsd: "0", + }; + } + + abstract handle(event: ProtocolEvent<"Strategy", StrategyEvent>): Promise; +} diff --git a/packages/processors/src/strategy/common/index.ts b/packages/processors/src/strategy/common/index.ts index f1ca948..428bb60 100644 --- a/packages/processors/src/strategy/common/index.ts +++ b/packages/processors/src/strategy/common/index.ts @@ -1 +1,2 @@ export * from "./baseDistributed.handler.js"; +export * from "./base.strategy.js"; diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts index 551d339..9ba3e29 100644 --- a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts @@ -1,15 +1,32 @@ +import { parseUnits } from "viem"; + import { Changeset } from "@grants-stack-indexer/repository"; -import { ChainId, ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared"; +import { + Address, + ChainId, + ProtocolEvent, + StrategyEvent, + Token, +} from "@grants-stack-indexer/shared"; -import type { IStrategyHandler, ProcessorDependencies } from "../../internal.js"; -import { BaseDistributedHandler, UnsupportedEventException } from "../../internal.js"; +import type { ProcessorDependencies, StrategyTimings } from "../../internal.js"; +import DonationVotingMerkleDistributionDirectTransferStrategy from "../../abis/allo-v2/v1/DonationVotingMerkleDistributionDirectTransferStrategy.js"; +import { calculateAmountInUsd } from "../../helpers/tokenMath.js"; +import { getDateFromTimestamp } from "../../helpers/utils.js"; +import { TokenPriceNotFoundError, UnsupportedEventException } from "../../internal.js"; +import { BaseDistributedHandler, BaseStrategyHandler } from "../common/index.js"; import { DVMDRegisteredHandler } from "./handlers/index.js"; type Dependencies = Pick< ProcessorDependencies, - "projectRepository" | "roundRepository" | "metadataProvider" + "projectRepository" | "roundRepository" | "metadataProvider" | "evmProvider" | "pricingProvider" >; +const STRATEGY_NAME = "allov2.DonationVotingMerkleDistributionDirectTransferStrategy"; + +// sometimes coingecko returns no prices for 1 hour range, 2 hours works better +export const TIMESTAMP_DELTA_RANGE = 2 * 60 * 60 * 1000; + /** * This handler is responsible for processing events related to the * Donation Voting Merkle Distribution Direct Transfer strategy. @@ -19,11 +36,15 @@ type Dependencies = Pick< * - Distributed */ -export class DVMDDirectTransferHandler implements IStrategyHandler { +export class DVMDDirectTransferStrategyHandler extends BaseStrategyHandler { constructor( private readonly chainId: ChainId, private readonly dependencies: Dependencies, - ) {} + ) { + super(STRATEGY_NAME); + } + + /** @inheritdoc */ async handle(event: ProtocolEvent<"Strategy", StrategyEvent>): Promise { switch (event.eventName) { case "Registered": @@ -42,4 +63,89 @@ export class DVMDDirectTransferHandler implements IStrategyHandler { + const matchAmount = parseUnits(matchingFundsAvailable.toString(), token.decimals); + + const matchAmountInUsd = await this.getTokenAmountInUsd(token, matchAmount, blockTimestamp); + + return { + matchAmount, + matchAmountInUsd, + }; + } + + /** @inheritdoc */ + override async fetchStrategyTimings(strategyId: Address): Promise { + const { evmProvider } = this.dependencies; + let results: [bigint, bigint, bigint, bigint] = [0n, 0n, 0n, 0n]; + + const contractCalls = [ + { + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "registrationStartTime", + address: strategyId, + }, + { + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "registrationEndTime", + address: strategyId, + }, + { + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "allocationStartTime", + address: strategyId, + }, + { + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "allocationEndTime", + address: strategyId, + }, + ] as const; + + if (evmProvider.getMulticall3Address()) { + results = await evmProvider.multicall({ + contracts: contractCalls, + allowFailure: false, + }); + } else { + results = (await Promise.all( + contractCalls.map((call) => + evmProvider.readContract(call.address, call.abi, call.functionName), + ), + )) as [bigint, bigint, bigint, bigint]; + } + + return { + applicationsStartTime: getDateFromTimestamp(results[0]), + applicationsEndTime: getDateFromTimestamp(results[1]), + donationsStartTime: getDateFromTimestamp(results[2]), + donationsEndTime: getDateFromTimestamp(results[3]), + }; + } + + /** @inheritdoc */ + private async getTokenAmountInUsd( + token: Token, + amount: bigint, + timestamp: number, + ): Promise { + const { pricingProvider } = this.dependencies; + const tokenPrice = await pricingProvider.getTokenPrice( + token.priceSourceCode, + timestamp, + timestamp + TIMESTAMP_DELTA_RANGE, + ); + + if (!tokenPrice) { + throw new TokenPriceNotFoundError(token.address, timestamp); + } + + return calculateAmountInUsd(amount, tokenPrice.priceUsd, token.decimals); + } } diff --git a/packages/processors/src/strategy/index.ts b/packages/processors/src/strategy/index.ts index 3bc73f5..a98b82d 100644 --- a/packages/processors/src/strategy/index.ts +++ b/packages/processors/src/strategy/index.ts @@ -1 +1,5 @@ +export * from "./common/index.js"; +export * from "./strategyHandler.factory.js"; export * from "./strategy.processor.js"; +// Export mapping separately to avoid circular dependencies +export { getHandler, existsHandler } from "./mapping.js"; diff --git a/packages/processors/src/strategy/mapping.ts b/packages/processors/src/strategy/mapping.ts new file mode 100644 index 0000000..20c1c67 --- /dev/null +++ b/packages/processors/src/strategy/mapping.ts @@ -0,0 +1,33 @@ +import { Hex } from "viem"; + +import type { StrategyHandlerConstructor } from "../internal.js"; +import { DVMDDirectTransferStrategyHandler } from "./donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; + +const strategyIdToHandler: Readonly> = { + "0x6f9291df02b2664139cec5703c124e4ebce32879c74b6297faa1468aa5ff9ebf": + DVMDDirectTransferStrategyHandler, // DonationVotingMerkleDistributionDirectTransferStrategyv1.0 + "0x2f46bf157821dc41daa51479e94783bb0c8699eac63bf75ec450508ab03867ce": + DVMDDirectTransferStrategyHandler, // DonationVotingMerkleDistributionDirectTransferStrategyv1.1 + "0x2f0250d534b2d59b8b5cfa5eb0d0848a59ccbf5de2eaf72d2ba4bfe73dce7c6b": + DVMDDirectTransferStrategyHandler, // DonationVotingMerkleDistributionDirectTransferStrategyv2.0 + "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0": + DVMDDirectTransferStrategyHandler, // DonationVotingMerkleDistributionDirectTransferStrategyv2.1 +} as const; + +/** + * Get a handler for a given strategy ID + * @param strategyId - The strategy ID to get the handler for + * @returns The handler for the strategy ID or undefined if it doesn't exist + */ +export const getHandler = (strategyId: Hex): StrategyHandlerConstructor | undefined => { + return strategyIdToHandler[strategyId.toLowerCase()]; +}; + +/** + * Check if a handler exists for a given strategy ID + * @param strategyId - The strategy ID to check + * @returns True if a handler exists, false otherwise + */ +export const existsHandler = (strategyId: Hex): boolean => { + return strategyIdToHandler[strategyId.toLowerCase()] !== undefined; +}; diff --git a/packages/processors/src/strategy/strategy.processor.ts b/packages/processors/src/strategy/strategy.processor.ts index c082a96..3b8ea94 100644 --- a/packages/processors/src/strategy/strategy.processor.ts +++ b/packages/processors/src/strategy/strategy.processor.ts @@ -2,6 +2,7 @@ import { Changeset } from "@grants-stack-indexer/repository"; import { ChainId, ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared"; import type { IProcessor, ProcessorDependencies } from "../internal.js"; +import { UnsupportedStrategy } from "../internal.js"; import { StrategyHandlerFactory } from "./strategyHandler.factory.js"; export class StrategyProcessor implements IProcessor<"Strategy", StrategyEvent> { @@ -13,10 +14,16 @@ export class StrategyProcessor implements IProcessor<"Strategy", StrategyEvent> async process(event: ProtocolEvent<"Strategy", StrategyEvent>): Promise { const strategyId = event.strategyId; - return StrategyHandlerFactory.createHandler( + const strategyHandler = StrategyHandlerFactory.createHandler( this.chainId, this.dependencies, strategyId, - ).handle(event); + ); + + if (!strategyHandler) { + throw new UnsupportedStrategy(strategyId); + } + + return strategyHandler.handle(event); } } diff --git a/packages/processors/src/strategy/strategyHandler.factory.ts b/packages/processors/src/strategy/strategyHandler.factory.ts index a084397..b0d1253 100644 --- a/packages/processors/src/strategy/strategyHandler.factory.ts +++ b/packages/processors/src/strategy/strategyHandler.factory.ts @@ -2,29 +2,28 @@ import { Hex } from "viem"; import { ChainId, StrategyEvent } from "@grants-stack-indexer/shared"; -import { IStrategyHandler, ProcessorDependencies, UnsupportedStrategy } from "../internal.js"; -import { DVMDDirectTransferHandler } from "./donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; +import { getHandler, IStrategyHandler, ProcessorDependencies } from "../internal.js"; +/** + * Factory for creating strategy handlers + */ export class StrategyHandlerFactory { + /** + * Create a new instance of a strategy handler for the given strategy ID + * @param chainId - The chain ID + * @param dependencies - The processor dependencies + * @param strategyId - The strategy ID + * @returns The strategy handler or undefined if it doesn't exist + */ static createHandler( chainId: ChainId, dependencies: ProcessorDependencies, strategyId: Hex, - ): IStrategyHandler { - const _strategyId = strategyId.toLowerCase(); + ): IStrategyHandler | undefined { + const _strategyId = strategyId.toLowerCase() as Hex; + const StrategyHandlerClass = getHandler(_strategyId); - switch (_strategyId) { - case "0x6f9291df02b2664139cec5703c124e4ebce32879c74b6297faa1468aa5ff9ebf": - // DonationVotingMerkleDistributionDirectTransferStrategyv1.1 - case "0x2f46bf157821dc41daa51479e94783bb0c8699eac63bf75ec450508ab03867ce": - // DonationVotingMerkleDistributionDirectTransferStrategyv2.0 - case "0x2f0250d534b2d59b8b5cfa5eb0d0848a59ccbf5de2eaf72d2ba4bfe73dce7c6b": - // DonationVotingMerkleDistributionDirectTransferStrategyv2.1 - case "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0": - return new DVMDDirectTransferHandler(chainId, dependencies); - - default: - throw new UnsupportedStrategy(strategyId); - } + console.log("StrategyHandlerClass", StrategyHandlerClass); + return StrategyHandlerClass ? new StrategyHandlerClass(chainId, dependencies) : undefined; } } diff --git a/packages/processors/src/types/strategy.types.ts b/packages/processors/src/types/strategy.types.ts index fc206a7..ff13ed4 100644 --- a/packages/processors/src/types/strategy.types.ts +++ b/packages/processors/src/types/strategy.types.ts @@ -1,3 +1,16 @@ +import { Branded, ChainId, StrategyEvent } from "@grants-stack-indexer/shared"; + +import { IStrategyHandler } from "../internal.js"; +import { ProcessorDependencies } from "./processor.types.js"; + +export type SanitizedStrategyId = Branded; +export type Strategy = { + id: SanitizedStrategyId; + name: string | null; + // TODO: check if groups are required + groups: string[]; +}; + /** * This type represents the time fields for a strategy. */ @@ -7,3 +20,8 @@ export type StrategyTimings = { donationsStartTime: Date | null; donationsEndTime: Date | null; }; + +export type StrategyHandlerConstructor = new ( + chainId: ChainId, + dependencies: ProcessorDependencies, +) => IStrategyHandler; diff --git a/packages/processors/test/strategy/common/base.strategy.spec.ts b/packages/processors/test/strategy/common/base.strategy.spec.ts new file mode 100644 index 0000000..7ff9c51 --- /dev/null +++ b/packages/processors/test/strategy/common/base.strategy.spec.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; + +import { Changeset } from "@grants-stack-indexer/repository"; +import { Address, Token, TokenCode } from "@grants-stack-indexer/shared"; + +import { BaseStrategyHandler } from "../../../src/strategy/common/base.strategy.js"; + +// Create a concrete implementation of BaseStrategyHandler for testing +class TestStrategyHandler extends BaseStrategyHandler { + constructor() { + super("TestStrategy"); + } + + async handle(): Promise { + return []; + } +} + +describe("BaseStrategyHandler", () => { + const handler = new TestStrategyHandler(); + + it("has the correct name", () => { + expect(handler.name).toBe("TestStrategy"); + }); + + describe("fetchStrategyTimings", () => { + it("returns default timings", async () => { + const address: Address = "0x1234567890123456789012345678901234567890"; + const timings = await handler.fetchStrategyTimings(address); + + expect(timings).toEqual({ + applicationsStartTime: null, + applicationsEndTime: null, + donationsStartTime: null, + donationsEndTime: null, + }); + }); + }); + + describe("fetchMatchAmount", () => { + it("returns default match amount", async () => { + const matchingFundsAvailable = 1000; + const token: Token = { + address: "0x1234567890123456789012345678901234567890", + decimals: 18, + code: "ETH" as TokenCode, + priceSourceCode: "ETH" as TokenCode, + }; + const blockTimestamp = 1625097600; // Example timestamp + + const result = await handler.fetchMatchAmount( + matchingFundsAvailable, + token, + blockTimestamp, + ); + + expect(result).toEqual({ + matchAmount: 0n, + matchAmountInUsd: "0", + }); + }); + }); +}); diff --git a/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts b/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts index 395528d..c05d38e 100644 --- a/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts +++ b/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts @@ -45,7 +45,7 @@ describe("BaseDistributedHandler", () => { } as unknown as IRoundReadRepository; }); - it("increment round total distributed when round is found", async () => { + it("increments round total distributed when round is found", async () => { mockEvent = createMockEvent(); const mockRound = { id: "round1" } as Round; diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts index d6acbb6..486da9e 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts @@ -1,3 +1,4 @@ +import { parseUnits } from "viem"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; @@ -5,11 +6,19 @@ import type { IProjectReadRepository, IRoundReadRepository, } from "@grants-stack-indexer/repository"; -import { ChainId, ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared"; - -import { UnsupportedEventException } from "../../../src/internal.js"; +import { EvmProvider } from "@grants-stack-indexer/chain-providers"; +import { IPricingProvider } from "@grants-stack-indexer/pricing"; +import { + ChainId, + ProtocolEvent, + StrategyEvent, + Token, + TokenCode, +} from "@grants-stack-indexer/shared"; + +import { TokenPriceNotFoundError, UnsupportedEventException } from "../../../src/internal.js"; import { BaseDistributedHandler } from "../../../src/strategy/common/index.js"; -import { DVMDDirectTransferHandler } from "../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; +import { DVMDDirectTransferStrategyHandler } from "../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; import { DVMDRegisteredHandler } from "../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.js"; vi.mock( @@ -34,20 +43,32 @@ vi.mock("../../../src/strategy/common/baseDistributed.handler.js", () => { describe("DVMDDirectTransferHandler", () => { const mockChainId = 10 as ChainId; - let handler: DVMDDirectTransferHandler; + let handler: DVMDDirectTransferStrategyHandler; let mockMetadataProvider: IMetadataProvider; let mockRoundRepository: IRoundReadRepository; let mockProjectRepository: IProjectReadRepository; + let mockEVMProvider: EvmProvider; + let mockPricingProvider: IPricingProvider; beforeEach(() => { mockMetadataProvider = {} as IMetadataProvider; mockRoundRepository = {} as IRoundReadRepository; mockProjectRepository = {} as IProjectReadRepository; - - handler = new DVMDDirectTransferHandler(mockChainId, { + mockEVMProvider = { + getMulticall3Address: vi.fn(), + multicall: vi.fn(), + readContract: vi.fn(), + } as unknown as EvmProvider; + mockPricingProvider = { + getTokenPrice: vi.fn(), + } as IPricingProvider; + + handler = new DVMDDirectTransferStrategyHandler(mockChainId, { metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, }); }); @@ -55,6 +76,10 @@ describe("DVMDDirectTransferHandler", () => { vi.clearAllMocks(); }); + it("gets correct name", () => { + expect(handler.name).toBe("allov2.DonationVotingMerkleDistributionDirectTransferStrategy"); + }); + it("calls RegisteredHandler for Registered event", async () => { const mockEvent = { eventName: "Registered", @@ -68,6 +93,8 @@ describe("DVMDDirectTransferHandler", () => { metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, }); expect(DVMDRegisteredHandler.prototype.handle).toHaveBeenCalled(); }); @@ -85,10 +112,113 @@ describe("DVMDDirectTransferHandler", () => { metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, }); expect(BaseDistributedHandler.prototype.handle).toHaveBeenCalled(); }); + describe("fetchMatchAmount", () => { + it("fetches the correct match amount and USD value", async () => { + const matchingFundsAvailable = 1000; + const token: Token = { + address: "0x1234567890123456789012345678901234567890", + decimals: 18, + code: "ETH" as TokenCode, + priceSourceCode: "ETH" as TokenCode, + }; + const blockTimestamp = 1625097600; + + vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue({ + priceUsd: 2000, + timestampMs: blockTimestamp, + }); + + const result = await handler.fetchMatchAmount( + matchingFundsAvailable, + token, + blockTimestamp, + ); + + expect(result).toEqual({ + matchAmount: parseUnits("1000", 18), + matchAmountInUsd: "2000000", + }); + }); + + it("throws TokenPriceNotFoundError when price is not available", async () => { + const matchingFundsAvailable = 1000; + const token: Token = { + address: "0x1234567890123456789012345678901234567890", + decimals: 18, + code: "ETH" as TokenCode, + priceSourceCode: "ETH" as TokenCode, + }; + const blockTimestamp = 1625097600; + + vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue(undefined); + + await expect( + handler.fetchMatchAmount(matchingFundsAvailable, token, blockTimestamp), + ).rejects.toThrow(TokenPriceNotFoundError); + }); + }); + + describe("fetchStrategyTimings", () => { + it("fetches correct timings using multicall", async () => { + const strategyId = "0x1234567890123456789012345678901234567890"; + const mockTimings = [1000n, 2000n, 3000n, 4000n]; + + vi.spyOn(mockEVMProvider, "getMulticall3Address").mockReturnValue("0xmulticalladdress"); + vi.spyOn(mockEVMProvider, "multicall").mockResolvedValue(mockTimings); + + const result = await handler.fetchStrategyTimings(strategyId); + + expect(result).toEqual({ + applicationsStartTime: new Date(Number(mockTimings[0]) * 1000), + applicationsEndTime: new Date(Number(mockTimings[1]) * 1000), + donationsStartTime: new Date(Number(mockTimings[2]) * 1000), + donationsEndTime: new Date(Number(mockTimings[3]) * 1000), + }); + + expect(mockEVMProvider.multicall).toHaveBeenCalled(); + expect(mockEVMProvider.readContract).not.toHaveBeenCalled(); + }); + + it("fetches correct timings when multicall is not available", async () => { + const strategyId = "0x1234567890123456789012345678901234567890"; + const mockTimings = [1000n, 2000n, 3000n, 4000n]; + + vi.spyOn(mockEVMProvider, "getMulticall3Address").mockReturnValue(undefined); + vi.spyOn(mockEVMProvider, "readContract").mockImplementation((_, __, functionName) => { + switch (functionName) { + case "registrationStartTime": + return Promise.resolve(mockTimings[0]); + case "registrationEndTime": + return Promise.resolve(mockTimings[1]); + case "allocationStartTime": + return Promise.resolve(mockTimings[2]); + case "allocationEndTime": + return Promise.resolve(mockTimings[3]); + default: + return Promise.resolve(undefined); + } + }); + + const result = await handler.fetchStrategyTimings(strategyId); + + expect(result).toEqual({ + applicationsStartTime: new Date(Number(mockTimings[0]) * 1000), + applicationsEndTime: new Date(Number(mockTimings[1]) * 1000), + donationsStartTime: new Date(Number(mockTimings[2]) * 1000), + donationsEndTime: new Date(Number(mockTimings[3]) * 1000), + }); + + expect(mockEVMProvider.readContract).toHaveBeenCalledTimes(4); + expect(mockEVMProvider.multicall).not.toHaveBeenCalled(); + }); + }); + it.skip("calls AllocatedHandler for Allocated event"); it.skip("calls TimestampsUpdatedHandler for TimestampsUpdated event"); it.skip("calls RecipientStatusUpdatedHandler for RecipientStatusUpdated event"); diff --git a/packages/processors/test/strategy/mapping.spec.ts b/packages/processors/test/strategy/mapping.spec.ts new file mode 100644 index 0000000..caa64a0 --- /dev/null +++ b/packages/processors/test/strategy/mapping.spec.ts @@ -0,0 +1,69 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; + +import { existsHandler } from "../../src/external.js"; +import { getHandler, StrategyHandlerConstructor } from "../../src/internal.js"; +import { DVMDDirectTransferStrategyHandler } from "../../src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; + +describe("Strategy Mapping", () => { + describe("getHandler", () => { + it("should return the correct handler for a valid strategy ID", () => { + const validStrategyId = + "0x6f9291df02b2664139cec5703c124e4ebce32879c74b6297faa1468aa5ff9ebf"; + + const handler = getHandler(validStrategyId); + + expect(handler).toBeDefined(); + expect(handler).toBe(DVMDDirectTransferStrategyHandler); + expectTypeOf(handler).toEqualTypeOf(); + }); + + it("should return the correct handler for a valid strategy ID in uppercase", () => { + const validStrategyId = + "0x6F9291DF02B2664139CEC5703C124E4EBCE32879C74B6297FAA1468AA5FF9EBF"; + + const handler = getHandler(validStrategyId); + + expect(handler).toBeDefined(); + expect(handler).toBe(DVMDDirectTransferStrategyHandler); + expectTypeOf(handler).toEqualTypeOf(); + }); + + it("should return undefined for an invalid strategy ID", () => { + const invalidStrategyId = + "0x1234567890123456789012345678901234567890123456789012345678901234"; + + const handler = getHandler(invalidStrategyId); + + expect(handler).toBeUndefined(); + }); + }); + + describe("existsHandler", () => { + it("should return true for a valid strategy ID", () => { + const validStrategyId = + "0x2f46bf157821dc41daa51479e94783bb0c8699eac63bf75ec450508ab03867ce"; + + const exists = existsHandler(validStrategyId); + + expect(exists).toBe(true); + }); + + it("should return true for a valid strategy ID in uppercase", () => { + const validStrategyId = + "0x2F46BF157821DC41DAA51479E94783BB0C8699EAC63BF75EC450508AB03867CE"; + + const exists = existsHandler(validStrategyId); + + expect(exists).toBe(true); + }); + + it("should return false for an invalid strategy ID", () => { + const invalidStrategyId = + "0x1234567890123456789012345678901234567890123456789012345678901234"; + + const exists = existsHandler(invalidStrategyId); + + expect(exists).toBe(false); + }); + }); +}); diff --git a/packages/processors/test/strategy/strategy.processor.spec.ts b/packages/processors/test/strategy/strategy.processor.spec.ts new file mode 100644 index 0000000..d860b8c --- /dev/null +++ b/packages/processors/test/strategy/strategy.processor.spec.ts @@ -0,0 +1,48 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type { EvmProvider } from "@grants-stack-indexer/chain-providers"; +import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import type { IPricingProvider } from "@grants-stack-indexer/pricing"; +import type { + IProjectReadRepository, + IRoundReadRepository, +} from "@grants-stack-indexer/repository"; +import type { ChainId, ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared"; + +import { StrategyProcessor, UnsupportedStrategy } from "../../src/internal.js"; + +describe("StrategyProcessor", () => { + const mockChainId = 10 as ChainId; + let processor: StrategyProcessor; + let mockEvmProvider: EvmProvider; + let mockPricingProvider: IPricingProvider; + let mockMetadataProvider: IMetadataProvider; + let mockRoundRepository: IRoundReadRepository; + + beforeEach(() => { + mockEvmProvider = {} as EvmProvider; + mockPricingProvider = {} as IPricingProvider; + mockMetadataProvider = {} as IMetadataProvider; + mockRoundRepository = {} as IRoundReadRepository; + + processor = new StrategyProcessor(mockChainId, { + evmProvider: mockEvmProvider, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: {} as IProjectReadRepository, + }); + + // Reset mocks before each test + vi.clearAllMocks(); + }); + + it("throw an error for unknown strategyId", async () => { + const mockEvent = { + eventName: "UnknownEvent", + strategyId: "0xunknown", + } as unknown as ProtocolEvent<"Strategy", StrategyEvent>; + + await expect(() => processor.process(mockEvent)).rejects.toThrow(UnsupportedStrategy); + }); +}); diff --git a/packages/processors/test/strategy/strategyHandler.factory.spec.ts b/packages/processors/test/strategy/strategyHandler.factory.spec.ts index ef941bd..2bc3902 100644 --- a/packages/processors/test/strategy/strategyHandler.factory.spec.ts +++ b/packages/processors/test/strategy/strategyHandler.factory.spec.ts @@ -7,9 +7,8 @@ import { IPricingProvider } from "@grants-stack-indexer/pricing"; import { IProjectReadRepository, IRoundReadRepository } from "@grants-stack-indexer/repository"; import { ChainId } from "@grants-stack-indexer/shared"; -import { ProcessorDependencies, UnsupportedStrategy } from "../../src/internal.js"; -import { DVMDDirectTransferHandler } from "../../src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; -import { StrategyHandlerFactory } from "../../src/strategy/strategyHandler.factory.js"; +import { ProcessorDependencies, StrategyHandlerFactory } from "../../src/internal.js"; +import { DVMDDirectTransferStrategyHandler } from "../../src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; describe("StrategyHandlerFactory", () => { const chainId = 10 as ChainId; @@ -55,20 +54,20 @@ describe("StrategyHandlerFactory", () => { ); expect(handler).toBeDefined(); - expect(handler).toBeInstanceOf(DVMDDirectTransferHandler); + expect(handler).toBeInstanceOf(DVMDDirectTransferStrategyHandler); }); }); it.skip("creates a DirectGrantsLiteHandler"); it.skip("creates a DirectGrantsSimpleHandler"); - it("throws an error if the strategy id is not supported", () => { - expect(() => - StrategyHandlerFactory.createHandler( - chainId, - mockProcessorDependencies, - "0xnot-supported", - ), - ).toThrow(UnsupportedStrategy); + it("returns undefined if the strategy id is not supported", () => { + const handler = StrategyHandlerFactory.createHandler( + chainId, + mockProcessorDependencies, + "0xnot-supported", + ); + + expect(handler).toBeUndefined(); }); }); From baf8c34dbc5de106fb34cfb94f4a25b652ce5a23 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:09:28 -0300 Subject: [PATCH 6/7] fix: pr comments --- .../src/allo/handlers/poolCreated.handler.ts | 10 ++++------ .../processors/src/strategy/common/base.strategy.ts | 2 ++ .../dvmdDirectTransfer.handler.ts | 9 ++++++++- .../handlers/registered.handler.ts | 12 ++++++++++++ .../src/strategy/strategyHandler.factory.ts | 1 - 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/processors/src/allo/handlers/poolCreated.handler.ts b/packages/processors/src/allo/handlers/poolCreated.handler.ts index 2661237..eec68e4 100644 --- a/packages/processors/src/allo/handlers/poolCreated.handler.ts +++ b/packages/processors/src/allo/handlers/poolCreated.handler.ts @@ -67,8 +67,6 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> strategyId, ); - // const strategy = extractStrategyFromId(strategyId); - const token = getToken(this.chainId, matchTokenAddress); let strategyTimings: StrategyTimings = { @@ -78,7 +76,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> donationsEndTime: null, }; - let matchAmount = { + let matchAmountObj = { matchAmount: 0n, matchAmountInUsd: "0", }; @@ -86,7 +84,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> if (strategyHandler) { strategyTimings = await strategyHandler.fetchStrategyTimings(strategyAddress); if (parsedRoundMetadata.success && token) { - matchAmount = await strategyHandler.fetchMatchAmount( + matchAmountObj = await strategyHandler.fetchMatchAmount( Number(parsedRoundMetadata.data.quadraticFundingConfig.matchingFundsAvailable), token, this.event.blockTimestamp, @@ -117,8 +115,8 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> totalAmountDonatedInUsd: "0", uniqueDonorsCount: 0, matchTokenAddress, - matchAmount: matchAmount.matchAmount, - matchAmountInUsd: matchAmount.matchAmountInUsd, + matchAmount: matchAmountObj.matchAmount, + matchAmountInUsd: matchAmountObj.matchAmountInUsd, fundedAmount, fundedAmountInUsd, applicationMetadataCid: metadataPointer, diff --git a/packages/processors/src/strategy/common/base.strategy.ts b/packages/processors/src/strategy/common/base.strategy.ts index aaab7a8..eacefbf 100644 --- a/packages/processors/src/strategy/common/base.strategy.ts +++ b/packages/processors/src/strategy/common/base.strategy.ts @@ -17,6 +17,7 @@ export abstract class BaseStrategyHandler implements IStrategyHandler { return { applicationsStartTime: null, @@ -26,6 +27,7 @@ export abstract class BaseStrategyHandler implements IStrategyHandler; +/** + * Handles the Registered event for the Donation Voting Merkle Distribution Direct Transfer strategy. + * + * This handler performs the following core actions when a project registers for a round: + * - Validates that both the project and round exist + * - Decodes the application data from the event + * - Retrieves the application metadata + * - Creates a new application record with PENDING status + * - Links the application to both the project and round + */ + export class DVMDRegisteredHandler implements IEventHandler<"Strategy", "Registered"> { constructor( readonly event: ProtocolEvent<"Strategy", "Registered">, @@ -23,6 +34,7 @@ export class DVMDRegisteredHandler implements IEventHandler<"Strategy", "Registe private readonly dependencies: Dependencies, ) {} + /** @inheritdoc */ async handle(): Promise { const { projectRepository, roundRepository, metadataProvider } = this.dependencies; const { data: encodedData, recipientId, sender } = this.event.params; diff --git a/packages/processors/src/strategy/strategyHandler.factory.ts b/packages/processors/src/strategy/strategyHandler.factory.ts index b0d1253..7b466db 100644 --- a/packages/processors/src/strategy/strategyHandler.factory.ts +++ b/packages/processors/src/strategy/strategyHandler.factory.ts @@ -23,7 +23,6 @@ export class StrategyHandlerFactory { const _strategyId = strategyId.toLowerCase() as Hex; const StrategyHandlerClass = getHandler(_strategyId); - console.log("StrategyHandlerClass", StrategyHandlerClass); return StrategyHandlerClass ? new StrategyHandlerClass(chainId, dependencies) : undefined; } } From 5bd2d20b9992086132b1e6c876a484b90a7f7b7d Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:45:13 -0300 Subject: [PATCH 7/7] fix: pr comments --- .../processors/src/allo/handlers/poolCreated.handler.ts | 9 +++------ packages/processors/src/helpers/index.ts | 3 +++ packages/processors/src/internal.ts | 5 +---- .../src/strategy/common/baseDistributed.handler.ts | 1 + .../dvmdDirectTransfer.handler.ts | 4 ++-- .../handlers/registered.handler.ts | 2 +- .../helpers/index.ts | 1 + packages/processors/src/strategy/mapping.ts | 6 ++++++ packages/shared/src/external.ts | 2 +- 9 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 packages/processors/src/helpers/index.ts create mode 100644 packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/index.ts diff --git a/packages/processors/src/allo/handlers/poolCreated.handler.ts b/packages/processors/src/allo/handlers/poolCreated.handler.ts index eec68e4..1a38ac4 100644 --- a/packages/processors/src/allo/handlers/poolCreated.handler.ts +++ b/packages/processors/src/allo/handlers/poolCreated.handler.ts @@ -2,15 +2,12 @@ import { getAddress, zeroAddress } from "viem"; import type { Changeset, NewRound, PendingRoundRole } from "@grants-stack-indexer/repository"; import type { ChainId, ProtocolEvent, Token } from "@grants-stack-indexer/shared"; -import { isAlloNativeToken } from "@grants-stack-indexer/shared"; -import { getToken } from "@grants-stack-indexer/shared/dist/src/internal.js"; +import { getToken, isAlloNativeToken } from "@grants-stack-indexer/shared"; import type { IEventHandler, ProcessorDependencies, StrategyTimings } from "../../internal.js"; -import { getRoundRoles } from "../../helpers/roles.js"; -import { calculateAmountInUsd } from "../../helpers/tokenMath.js"; -import { TokenPriceNotFoundError } from "../../internal.js"; +import { calculateAmountInUsd, getRoundRoles } from "../../helpers/index.js"; +import { StrategyHandlerFactory, TokenPriceNotFoundError } from "../../internal.js"; import { RoundMetadataSchema } from "../../schemas/index.js"; -import { StrategyHandlerFactory } from "../../strategy/strategyHandler.factory.js"; type Dependencies = Pick< ProcessorDependencies, diff --git a/packages/processors/src/helpers/index.ts b/packages/processors/src/helpers/index.ts new file mode 100644 index 0000000..0160fc0 --- /dev/null +++ b/packages/processors/src/helpers/index.ts @@ -0,0 +1,3 @@ +export * from "./roles.js"; +export * from "./utils.js"; +export * from "./tokenMath.js"; diff --git a/packages/processors/src/internal.ts b/packages/processors/src/internal.ts index fa8511a..b71289f 100644 --- a/packages/processors/src/internal.ts +++ b/packages/processors/src/internal.ts @@ -9,7 +9,4 @@ export * from "./exceptions/index.js"; export * from "./allo/index.js"; // Strategy -export * from "./strategy/common/index.js"; -export * from "./strategy/strategyHandler.factory.js"; -export * from "./strategy/strategy.processor.js"; -export { getHandler, existsHandler } from "./strategy/mapping.js"; +export * from "./strategy/index.js"; diff --git a/packages/processors/src/strategy/common/baseDistributed.handler.ts b/packages/processors/src/strategy/common/baseDistributed.handler.ts index 60a6dda..138e918 100644 --- a/packages/processors/src/strategy/common/baseDistributed.handler.ts +++ b/packages/processors/src/strategy/common/baseDistributed.handler.ts @@ -36,6 +36,7 @@ export class BaseDistributedHandler implements IEventHandler<"Strategy", "Distri if (!round) { //TODO: add logging that round was not found + console.log("Round not found for strategy address", strategyAddress); return []; } diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts index 561028d..fb1ee1e 100644 --- a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts @@ -11,8 +11,7 @@ import { import type { ProcessorDependencies, StrategyTimings } from "../../internal.js"; import DonationVotingMerkleDistributionDirectTransferStrategy from "../../abis/allo-v2/v1/DonationVotingMerkleDistributionDirectTransferStrategy.js"; -import { calculateAmountInUsd } from "../../helpers/tokenMath.js"; -import { getDateFromTimestamp } from "../../helpers/utils.js"; +import { calculateAmountInUsd, getDateFromTimestamp } from "../../helpers/index.js"; import { TokenPriceNotFoundError, UnsupportedEventException } from "../../internal.js"; import { BaseDistributedHandler, BaseStrategyHandler } from "../common/index.js"; import { DVMDRegisteredHandler } from "./handlers/index.js"; @@ -108,6 +107,7 @@ export class DVMDDirectTransferStrategyHandler extends BaseStrategyHandler { }, ] as const; + // TODO: refactor when evmProvider implements this natively if (evmProvider.getMulticall3Address()) { results = await evmProvider.multicall({ contracts: contractCalls, diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts index eb170ac..8fd10c8 100644 --- a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts @@ -9,7 +9,7 @@ import { ProjectNotFound, RoundNotFound, } from "../../../internal.js"; -import { decodeDVMDApplicationData } from "../helpers/decoder.js"; +import { decodeDVMDApplicationData } from "../helpers/index.js"; type Dependencies = Pick< ProcessorDependencies, diff --git a/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/index.ts b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/index.ts new file mode 100644 index 0000000..1616d7f --- /dev/null +++ b/packages/processors/src/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/index.ts @@ -0,0 +1 @@ +export * from "./decoder.js"; diff --git a/packages/processors/src/strategy/mapping.ts b/packages/processors/src/strategy/mapping.ts index 20c1c67..9913bf2 100644 --- a/packages/processors/src/strategy/mapping.ts +++ b/packages/processors/src/strategy/mapping.ts @@ -3,6 +3,12 @@ import { Hex } from "viem"; import type { StrategyHandlerConstructor } from "../internal.js"; import { DVMDDirectTransferStrategyHandler } from "./donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; +/** + * This mapping connects strategy IDs to their corresponding handler classes. + * When a new strategy event is received, the system uses this mapping to instantiate the appropriate handler + * based on the strategy ID from the event. Each handler implements specific logic for processing events + * from that strategy type. + */ const strategyIdToHandler: Readonly> = { "0x6f9291df02b2664139cec5703c124e4ebce32879c74b6297faa1468aa5ff9ebf": DVMDDirectTransferStrategyHandler, // DonationVotingMerkleDistributionDirectTransferStrategyv1.0 diff --git a/packages/shared/src/external.ts b/packages/shared/src/external.ts index 2843728..0216b8e 100644 --- a/packages/shared/src/external.ts +++ b/packages/shared/src/external.ts @@ -16,4 +16,4 @@ export { BigNumber } from "./internal.js"; export type { BigNumberType } from "./internal.js"; export type { TokenCode, Token } from "./internal.js"; -export { TOKENS } from "./tokens/tokens.js"; +export { TOKENS, getToken } from "./tokens/tokens.js";