diff --git a/packages/processors/src/processors/strategy/directGrantsSimple/directGrantsSimple.handler.ts b/packages/processors/src/processors/strategy/directGrantsSimple/directGrantsSimple.handler.ts new file mode 100644 index 0000000..5334f3c --- /dev/null +++ b/packages/processors/src/processors/strategy/directGrantsSimple/directGrantsSimple.handler.ts @@ -0,0 +1,52 @@ +import { Changeset } from "@grants-stack-indexer/repository"; +import { ChainId, ProcessorEvent, StrategyEvent } from "@grants-stack-indexer/shared"; + +import { ProcessorDependencies, UnsupportedEventException } from "../../../internal.js"; +import { BaseDistributedHandler, BaseStrategyHandler } from "../index.js"; +import { DGSimpleRegisteredHandler, DGSimpleTimestampsUpdatedHandler } from "./handlers/index.js"; + +const STRATEGY_NAME = "allov2.DirectGrantsSimpleStrategy"; + +/** + * This handler is responsible for processing events related to the + * Direct Grants Simple strategy. + * + * The following events are currently handled by this strategy: + * - TimestampsUpdated + * - RegisteredWithSender + * - DistributedWithRecipientAddress + */ +export class DGSimpleStrategyHandler extends BaseStrategyHandler { + constructor( + private readonly chainId: ChainId, + private readonly dependencies: ProcessorDependencies, + ) { + super(STRATEGY_NAME); + } + + /** @inheritdoc */ + async handle(event: ProcessorEvent<"Strategy", StrategyEvent>): Promise { + switch (event.eventName) { + case "TimestampsUpdated": + return new DGSimpleTimestampsUpdatedHandler( + event as ProcessorEvent<"Strategy", "TimestampsUpdated">, + this.chainId, + this.dependencies, + ).handle(); + case "RegisteredWithSender": + return new DGSimpleRegisteredHandler( + event as ProcessorEvent<"Strategy", "RegisteredWithSender">, + this.chainId, + this.dependencies, + ).handle(); + case "DistributedWithRecipientAddress": + return new BaseDistributedHandler( + event as ProcessorEvent<"Strategy", "DistributedWithRecipientAddress">, + this.chainId, + this.dependencies, + ).handle(); + default: + throw new UnsupportedEventException("Strategy", event.eventName, this.name); + } + } +} diff --git a/packages/processors/src/processors/strategy/directGrantsSimple/handlers/index.ts b/packages/processors/src/processors/strategy/directGrantsSimple/handlers/index.ts new file mode 100644 index 0000000..fd37bae --- /dev/null +++ b/packages/processors/src/processors/strategy/directGrantsSimple/handlers/index.ts @@ -0,0 +1,2 @@ +export * from "./timestampsUpdated.handler.js"; +export * from "./registered.handler.js"; diff --git a/packages/processors/src/processors/strategy/directGrantsSimple/handlers/registered.handler.ts b/packages/processors/src/processors/strategy/directGrantsSimple/handlers/registered.handler.ts new file mode 100644 index 0000000..40e6483 --- /dev/null +++ b/packages/processors/src/processors/strategy/directGrantsSimple/handlers/registered.handler.ts @@ -0,0 +1,95 @@ +import { getAddress } from "viem"; + +import { Changeset, NewApplication } from "@grants-stack-indexer/repository"; +import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; + +import { IEventHandler, ProcessorDependencies } from "../../../../internal.js"; +import { decodeDGApplicationData } from "../../helpers/index.js"; + +type Dependencies = Pick< + ProcessorDependencies, + "roundRepository" | "projectRepository" | "metadataProvider" +>; + +/** + * 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 DGSimpleRegisteredHandler + implements IEventHandler<"Strategy", "RegisteredWithSender"> +{ + constructor( + readonly event: ProcessorEvent<"Strategy", "RegisteredWithSender">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + /** + * Handles the RegisteredWithSender event for the Direct Grants Simple strategy. + * @returns The changeset with an InsertApplication operation. + * @throws ProjectNotFound if the project is not found. + * @throws RoundNotFound if the round is not found. + */ + async handle(): Promise { + 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.getProjectByAnchorOrThrow( + this.chainId, + anchorAddress, + ); + + const strategyAddress = getAddress(this.event.srcAddress); + const round = await roundRepository.getRoundByStrategyAddressOrThrow( + this.chainId, + strategyAddress, + ); + + const values = decodeDGApplicationData(encodedData); + const id = recipientId; + + 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/processors/strategy/directGrantsSimple/handlers/timestampsUpdated.handler.ts b/packages/processors/src/processors/strategy/directGrantsSimple/handlers/timestampsUpdated.handler.ts new file mode 100644 index 0000000..e5292b6 --- /dev/null +++ b/packages/processors/src/processors/strategy/directGrantsSimple/handlers/timestampsUpdated.handler.ts @@ -0,0 +1,59 @@ +import { getAddress } from "viem"; + +import { Changeset } from "@grants-stack-indexer/repository"; +import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; + +import { getDateFromTimestamp } from "../../../../helpers/index.js"; +import { IEventHandler, ProcessorDependencies } from "../../../../internal.js"; + +type Dependencies = Pick; + +/** + * Handles the TimestampsUpdated event for the Direct Grants Simple strategy. + * + * This handler processes updates to the round timestamps: + * - Validates the round exists for the strategy address + * - Converts the updated registration timestamps to dates + * - Returns a changeset to update the round's application timestamps + */ +export class DGSimpleTimestampsUpdatedHandler + implements IEventHandler<"Strategy", "TimestampsUpdated"> +{ + constructor( + readonly event: ProcessorEvent<"Strategy", "TimestampsUpdated">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + /** + * Handles the TimestampsUpdated event for the Direct Grants Simple strategy. + * @returns The changeset with an UpdateRound operation. + * @throws RoundNotFound if the round is not found. + */ + async handle(): Promise { + const strategyAddress = getAddress(this.event.srcAddress); + const round = await this.dependencies.roundRepository.getRoundByStrategyAddressOrThrow( + this.chainId, + strategyAddress, + ); + + const { startTime: strStartTime, endTime: strEndTime } = this.event.params; + + const applicationsStartTime = getDateFromTimestamp(BigInt(strStartTime)); + const applicationsEndTime = getDateFromTimestamp(BigInt(strEndTime)); + + return [ + { + type: "UpdateRound", + args: { + chainId: this.chainId, + roundId: round.id, + round: { + applicationsStartTime, + applicationsEndTime, + }, + }, + }, + ]; + } +} diff --git a/packages/processors/src/processors/strategy/directGrantsSimple/index.ts b/packages/processors/src/processors/strategy/directGrantsSimple/index.ts new file mode 100644 index 0000000..11a106d --- /dev/null +++ b/packages/processors/src/processors/strategy/directGrantsSimple/index.ts @@ -0,0 +1,2 @@ +export * from "./handlers/index.js"; +export * from "./directGrantsSimple.handler.js"; diff --git a/packages/processors/src/processors/strategy/mapping.ts b/packages/processors/src/processors/strategy/mapping.ts index 29d9b6b..0c0bad1 100644 --- a/packages/processors/src/processors/strategy/mapping.ts +++ b/packages/processors/src/processors/strategy/mapping.ts @@ -3,6 +3,7 @@ import { Hex } from "viem"; import type { StrategyHandlerConstructor } from "../../internal.js"; import { DirectAllocationStrategyHandler } from "./directAllocation/index.js"; import { DirectGrantsLiteStrategyHandler } from "./directGrantsLite/index.js"; +import { DGSimpleStrategyHandler } from "./directGrantsSimple/index.js"; import { DVMDDirectTransferStrategyHandler } from "./donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; /** @@ -22,6 +23,8 @@ const strategyIdToHandler: Readonly> DVMDDirectTransferStrategyHandler, // DonationVotingMerkleDistributionDirectTransferStrategyv2.1 "0x4cd0051913234cdd7d165b208851240d334786d6e5afbb4d0eec203515a9c6f3": DirectAllocationStrategyHandler, + "0x263cb916541b6fc1fb5543a244829ccdba75264b097726e6ecc3c3cfce824bf5": DGSimpleStrategyHandler, + "0x53fb9d3bce0956ca2db5bb1441f5ca23050cb1973b33789e04a5978acfd9ca93": DGSimpleStrategyHandler, "0x103732a8e473467a510d4128ee11065262bdd978f0d9dad89ba68f2c56127e27": DirectGrantsLiteStrategyHandler, } as const; diff --git a/packages/processors/test/strategy/directGrantsSimple/directGrantsSimple.handler.spec.ts b/packages/processors/test/strategy/directGrantsSimple/directGrantsSimple.handler.spec.ts new file mode 100644 index 0000000..a48eb61 --- /dev/null +++ b/packages/processors/test/strategy/directGrantsSimple/directGrantsSimple.handler.spec.ts @@ -0,0 +1,161 @@ +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 { + IApplicationReadRepository, + IProjectReadRepository, + IRoundReadRepository, +} from "@grants-stack-indexer/repository"; +import { ChainId, ILogger, ProcessorEvent, StrategyEvent } from "@grants-stack-indexer/shared"; + +import { UnsupportedEventException } from "../../../src/exceptions/index.js"; +import { BaseDistributedHandler } from "../../../src/processors/strategy/common/index.js"; +import { DGSimpleStrategyHandler } from "../../../src/processors/strategy/directGrantsSimple/directGrantsSimple.handler.js"; +import { + DGSimpleRegisteredHandler, + DGSimpleTimestampsUpdatedHandler, +} from "../../../src/processors/strategy/directGrantsSimple/handlers/index.js"; + +vi.mock("../../../src/processors/strategy/directGrantsSimple/handlers/index.js", () => { + const DGSimpleRegisteredHandler = vi.fn(); + const DGSimpleTimestampsUpdatedHandler = vi.fn(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + DGSimpleRegisteredHandler.prototype.handle = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + DGSimpleTimestampsUpdatedHandler.prototype.handle = vi.fn(); + + return { + DGSimpleRegisteredHandler, + DGSimpleTimestampsUpdatedHandler, + }; +}); + +vi.mock("../../../src/processors/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("DirectGrantsSimpleStrategyHandler", () => { + let handler: DGSimpleStrategyHandler; + let mockMetadataProvider: IMetadataProvider; + let mockRoundRepository: IRoundReadRepository; + let mockProjectRepository: IProjectReadRepository; + let mockEVMProvider: EvmProvider; + let mockPricingProvider: IPricingProvider; + let mockApplicationRepository: IApplicationReadRepository; + const chainId = 10 as ChainId; + + const logger: ILogger = { + debug: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }; + + beforeEach(() => { + mockMetadataProvider = {} as IMetadataProvider; + mockRoundRepository = {} as IRoundReadRepository; + mockProjectRepository = {} as IProjectReadRepository; + mockEVMProvider = {} as EvmProvider; + mockPricingProvider = {} as IPricingProvider; + mockApplicationRepository = {} as IApplicationReadRepository; + + handler = new DGSimpleStrategyHandler(chainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, + applicationRepository: mockApplicationRepository, + logger, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("returns correct name", () => { + expect(handler.name).toBe("allov2.DirectGrantsSimpleStrategy"); + }); + + it("calls DGSimpleTimestampsUpdatedHandler for TimestampsUpdated event", async () => { + const mockEvent = { + eventName: "TimestampsUpdated", + } as ProcessorEvent<"Strategy", "TimestampsUpdated">; + + vi.spyOn(DGSimpleTimestampsUpdatedHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(DGSimpleTimestampsUpdatedHandler).toHaveBeenCalledWith(mockEvent, chainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, + applicationRepository: mockApplicationRepository, + logger, + }); + expect(DGSimpleTimestampsUpdatedHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("calls DGSimpleRegisteredHandler for RegisteredWithSender event", async () => { + const mockEvent = { + eventName: "RegisteredWithSender", + } as ProcessorEvent<"Strategy", "RegisteredWithSender">; + + vi.spyOn(DGSimpleRegisteredHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(DGSimpleRegisteredHandler).toHaveBeenCalledWith(mockEvent, chainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, + applicationRepository: mockApplicationRepository, + logger, + }); + expect(DGSimpleRegisteredHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("calls BaseDistributedHandler for DistributedWithRecipientAddress event", async () => { + const mockEvent = { + eventName: "DistributedWithRecipientAddress", + } as ProcessorEvent<"Strategy", "DistributedWithRecipientAddress">; + + vi.spyOn(BaseDistributedHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(BaseDistributedHandler).toHaveBeenCalledWith(mockEvent, chainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, + applicationRepository: mockApplicationRepository, + logger, + }); + expect(BaseDistributedHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("throws UnsupportedEventException for unknown events", async () => { + const mockEvent = { + eventName: "UnknownEvent", + } as unknown as ProcessorEvent<"Strategy", StrategyEvent>; + + await expect(handler.handle(mockEvent)).rejects.toThrow(UnsupportedEventException); + }); +}); diff --git a/packages/processors/test/strategy/directGrantsSimple/handlers/registered.handler.spec.ts b/packages/processors/test/strategy/directGrantsSimple/handlers/registered.handler.spec.ts new file mode 100644 index 0000000..0a944c8 --- /dev/null +++ b/packages/processors/test/strategy/directGrantsSimple/handlers/registered.handler.spec.ts @@ -0,0 +1,163 @@ +import { getAddress } from "viem"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import { + IProjectRepository, + IRoundRepository, + Project, + ProjectNotFound, + Round, + RoundNotFound, +} from "@grants-stack-indexer/repository"; +import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; + +import { DGSimpleRegisteredHandler } from "../../../../src/processors/strategy/directGrantsSimple/handlers/registered.handler.js"; +import { createMockEvent } from "../../../mocks/index.js"; + +describe("DGSimpleRegisteredHandler", () => { + let handler: DGSimpleRegisteredHandler; + let mockRoundRepository: IRoundRepository; + let mockProjectRepository: IProjectRepository; + let mockMetadataProvider: IMetadataProvider; + let mockEvent: ProcessorEvent<"Strategy", "RegisteredWithSender">; + const chainId = 10 as ChainId; + const eventName = "RegisteredWithSender"; + const defaultParams = { + recipientId: "0x1234567890123456789012345678901234567890", + data: "0x0000000000000000000000001234567890123456789012345678901234567890000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000", + sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + } as const; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; + + beforeEach(() => { + mockRoundRepository = { + getRoundByStrategyAddressOrThrow: vi.fn(), + } as unknown as IRoundRepository; + mockProjectRepository = { + getProjectByAnchorOrThrow: vi.fn(), + } as unknown as IProjectRepository; + mockMetadataProvider = { + getMetadata: vi.fn(), + } as unknown as IMetadataProvider; + }); + + it("handles a valid registration event", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); + const mockProject = { + id: "project1", + anchorAddress: mockEvent.params.recipientId, + } as Project; + const mockRound = { id: "round1" } as Round; + const mockMetadata = { name: "Test Project" }; + + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(mockMetadata); + + handler = new DGSimpleRegisteredHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + }); + + const result = await handler.handle(); + + expect(result).toEqual([ + { + type: "InsertApplication", + args: { + chainId, + id: mockEvent.params.recipientId, + projectId: "project1", + anchorAddress: getAddress(mockEvent.params.recipientId), + roundId: "round1", + status: "PENDING", + metadataCid: "bafkreigyc43cfinxlnahq5aawsghibetv6usrs7kkxf7vxmzbjyro76iwy", + metadata: mockMetadata, + createdAtBlock: BigInt(mockEvent.blockNumber), + createdByAddress: getAddress(mockEvent.params.sender), + 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("throws ProjectNotFound if project is not found", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockRejectedValue( + new ProjectNotFound(chainId, mockEvent.params.recipientId), + ); + + handler = new DGSimpleRegisteredHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + }); + + await expect(handler.handle()).rejects.toThrow(ProjectNotFound); + }); + + it("throws RoundNotFound if round is not found", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); + const mockProject = { + id: "project1", + anchorAddress: mockEvent.params.recipientId, + } as Project; + + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( + new RoundNotFound(chainId, mockEvent.strategyId), + ); + + handler = new DGSimpleRegisteredHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + }); + + await expect(handler.handle()).rejects.toThrow(RoundNotFound); + }); + + it("handles undefined metadata", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); + const mockProject = { + id: "project1", + anchorAddress: mockEvent.params.recipientId, + } as Project; + const mockRound = { id: "round1" } as Round; + + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(null); + + handler = new DGSimpleRegisteredHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + }); + + const result = await handler.handle(); + const changeset = result[0] as { + type: "InsertApplication"; + args: { metadata: null }; + }; + expect(changeset.args.metadata).toBeNull(); + }); +}); diff --git a/packages/processors/test/strategy/directGrantsSimple/handlers/timestampsUpdated.handler.spec.ts b/packages/processors/test/strategy/directGrantsSimple/handlers/timestampsUpdated.handler.spec.ts new file mode 100644 index 0000000..c207480 --- /dev/null +++ b/packages/processors/test/strategy/directGrantsSimple/handlers/timestampsUpdated.handler.spec.ts @@ -0,0 +1,69 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { IRoundRepository, Round, RoundNotFound } from "@grants-stack-indexer/repository"; +import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; + +import { DGSimpleTimestampsUpdatedHandler } from "../../../../src/processors/strategy/directGrantsSimple/handlers/timestampsUpdated.handler.js"; +import { createMockEvent } from "../../../mocks/index.js"; + +describe("DGSimpleTimestampsUpdatedHandler", () => { + let handler: DGSimpleTimestampsUpdatedHandler; + let mockRoundRepository: IRoundRepository; + let mockEvent: ProcessorEvent<"Strategy", "TimestampsUpdated">; + const chainId = 10 as ChainId; + const eventName = "TimestampsUpdated"; + const defaultParams = { + startTime: "1704067200", // 2024-01-01 00:00:00 + endTime: "1704153600", // 2024-01-02 00:00:00 + sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + } as const; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; + + beforeEach(() => { + mockRoundRepository = { + getRoundByStrategyAddressOrThrow: vi.fn(), + } as unknown as IRoundRepository; + }); + + it("handles a valid timestamps update event", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); + const mockRound = { id: "round1" } as Round; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + + handler = new DGSimpleTimestampsUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + + expect(result).toEqual([ + { + type: "UpdateRound", + args: { + chainId, + roundId: "round1", + round: { + applicationsStartTime: new Date("2024-01-01T00:00:00.000Z"), + applicationsEndTime: new Date("2024-01-02T00:00:00.000Z"), + }, + }, + }, + ]); + }); + + it("throws RoundNotFound if round is not found", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( + new RoundNotFound(chainId, mockEvent.strategyId), + ); + + handler = new DGSimpleTimestampsUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + }); + + await expect(handler.handle()).rejects.toThrow(RoundNotFound); + }); +});