diff --git a/packages/indexer-client/tsconfig.json b/packages/indexer-client/tsconfig.json index 66bb87a..21c1c5b 100644 --- a/packages/indexer-client/tsconfig.json +++ b/packages/indexer-client/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.json", - "include": ["src/**/*"] + "include": ["src/**/*", "test/**/*"] } diff --git a/packages/processors/src/allo/handlers/poolCreated.handler.ts b/packages/processors/src/allo/handlers/poolCreated.handler.ts index 392b587..d876f07 100644 --- a/packages/processors/src/allo/handlers/poolCreated.handler.ts +++ b/packages/processors/src/allo/handlers/poolCreated.handler.ts @@ -6,10 +6,10 @@ import { isAlloNativeToken } from "@grants-stack-indexer/shared"; import type { IEventHandler, ProcessorDependencies, StrategyTimings } from "../../internal.js"; import { getRoundRoles } from "../../helpers/roles.js"; -import { RoundMetadataSchema } from "../../helpers/schemas.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"; type Dependencies = Pick< ProcessorDependencies, diff --git a/packages/processors/src/registry/handlers/index.ts b/packages/processors/src/registry/handlers/index.ts new file mode 100644 index 0000000..79e1e8b --- /dev/null +++ b/packages/processors/src/registry/handlers/index.ts @@ -0,0 +1,2 @@ +export * from "./profileCreated.hanlder.js"; +export * from "./roleGranted.handler.js"; diff --git a/packages/processors/src/registry/handlers/profileCreated.hanlder.ts b/packages/processors/src/registry/handlers/profileCreated.hanlder.ts index 9e34fdb..c463e0f 100644 --- a/packages/processors/src/registry/handlers/profileCreated.hanlder.ts +++ b/packages/processors/src/registry/handlers/profileCreated.hanlder.ts @@ -1,11 +1,14 @@ -import { Changeset } from "@grants-stack-indexer/repository"; +import { getAddress } from "viem"; + +import { Changeset, ProjectType } from "@grants-stack-indexer/repository"; import { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; import { IEventHandler, ProcessorDependencies } from "../../internal.js"; +import { ProjectMetadata, ProjectMetadataSchema } from "../../schemas/projectMetadata.js"; type Dependencies = Pick< ProcessorDependencies, - "projectRepository" | "evmProvider" | "pricingProvider" + "projectRepository" | "evmProvider" | "pricingProvider" | "metadataProvider" >; export class ProfileCreatedHandler implements IEventHandler<"Registry", "ProfileCreated"> { @@ -15,6 +18,110 @@ export class ProfileCreatedHandler implements IEventHandler<"Registry", "Profile private dependencies: Dependencies, ) {} async handle(): Promise { - return []; + const profileId = this.event.params.profileId; + const metadataCid = this.event.params.metadata[1]; + const metadata = await this.dependencies.metadataProvider.getMetadata(metadataCid); + + const parsedMetadata = ProjectMetadataSchema.safeParse(metadata); + + let projectType: ProjectType = "canonical"; + let isProgram = false; + let metadataValue = null; + + if (parsedMetadata.success) { + projectType = this.getProjectTypeFromMetadata(parsedMetadata.data); + isProgram = parsedMetadata.data.type === "program"; + metadataValue = parsedMetadata.data; + } else { + //TODO: Replace with logger + console.warn({ + msg: `ProfileCreated: Failed to parse metadata for profile ${profileId}`, + event: this.event, + metadataCid, + metadata, + }); + } + + const tx = await this.dependencies.evmProvider.getTransaction( + this.event.transactionFields.hash, + ); + + const createdBy = tx.from; + const programTags = isProgram ? ["program"] : []; + + const changes: Changeset[] = [ + { + type: "InsertProject", + args: { + project: { + tags: ["allo-v2", ...programTags], + chainId: this.chainId, + registryAddress: getAddress(this.event.srcAddress), + id: profileId, + name: this.event.params.name, + nonce: this.event.params.nonce, + anchorAddress: getAddress(this.event.params.anchor), + projectNumber: null, + metadataCid: metadataCid, + metadata: metadataValue, + createdByAddress: getAddress(createdBy), + createdAtBlock: BigInt(this.event.blockNumber), + updatedAtBlock: BigInt(this.event.blockNumber), + projectType, + }, + }, + }, + { + type: "InsertProjectRole", + args: { + projectRole: { + chainId: this.chainId, + projectId: this.event.params.profileId, + address: getAddress(this.event.params.owner), + role: "owner", + createdAtBlock: BigInt(this.event.blockNumber), + }, + }, + }, + ]; + + const pendingProjectRoles = + await this.dependencies.projectRepository.getPendingProjectRolesByRole( + this.chainId, + profileId, + ); + + if (pendingProjectRoles.length !== 0) { + for (const role of pendingProjectRoles) { + changes.push({ + type: "InsertProjectRole", + args: { + projectRole: { + chainId: this.chainId, + projectId: profileId, + address: getAddress(role.address), + role: "member", + createdAtBlock: BigInt(this.event.blockNumber), + }, + }, + }); + } + + changes.push({ + type: "DeletePendingProjectRoles", + args: { ids: pendingProjectRoles.map((r) => r.id!) }, + }); + } + + return changes; + } + + private getProjectTypeFromMetadata(metadata: ProjectMetadata): ProjectType { + // if the metadata contains a canonical reference, it's a linked project + if ("canonical" in metadata) { + return "linked"; + } + + return "canonical"; } } diff --git a/packages/processors/src/registry/registry.processor.ts b/packages/processors/src/registry/registry.processor.ts index ce3023e..d96e363 100644 --- a/packages/processors/src/registry/registry.processor.ts +++ b/packages/processors/src/registry/registry.processor.ts @@ -2,17 +2,33 @@ import { Changeset } from "@grants-stack-indexer/repository"; import { ChainId, ProtocolEvent, RegistryEvent } from "@grants-stack-indexer/shared"; import type { IProcessor } from "../internal.js"; +import { UnsupportedEventException } from "../internal.js"; import { ProcessorDependencies } from "../types/processor.types.js"; -import { RegistryHandlerFactory } from "./registryProcessorFactory.js"; +import { ProfileCreatedHandler, RoleGrantedHandler } from "./handlers/index.js"; export class RegistryProcessor implements IProcessor<"Registry", RegistryEvent> { - private factory: RegistryHandlerFactory = new RegistryHandlerFactory(); constructor( private readonly chainId: ChainId, private readonly dependencies: ProcessorDependencies, ) {} - //TODO: Implement - async process(_event: ProtocolEvent<"Registry", RegistryEvent>): Promise { - return await this.factory.createHandler(_event, this.chainId, this.dependencies).handle(); + + async process(event: ProtocolEvent<"Registry", RegistryEvent>): Promise { + //TODO: Implement robust error handling and retry logic + switch (event.eventName) { + case "RoleGranted": + return new RoleGrantedHandler( + event as ProtocolEvent<"Registry", "RoleGranted">, + this.chainId, + this.dependencies, + ).handle(); + case "ProfileCreated": + return new ProfileCreatedHandler( + event as ProtocolEvent<"Registry", "ProfileCreated">, + this.chainId, + this.dependencies, + ).handle(); + default: + throw new UnsupportedEventException("Registry", event.eventName); + } } } diff --git a/packages/processors/src/registry/registryProcessorFactory.ts b/packages/processors/src/registry/registryProcessorFactory.ts deleted file mode 100644 index b92e7bd..0000000 --- a/packages/processors/src/registry/registryProcessorFactory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ChainId, ProtocolEvent, RegistryEvent } from "@grants-stack-indexer/shared"; - -import { UnsupportedEventException } from "../exceptions/unsupportedEvent.exception.js"; -import { IEventHandler, IEventHandlerFactory } from "../internal.js"; -import { ProcessorDependencies } from "../types/processor.types.js"; -import { RoleGrantedHandler } from "./handlers/roleGranted.handler.js"; - -export class RegistryHandlerFactory implements IEventHandlerFactory<"Registry", RegistryEvent> { - public createHandler( - event: ProtocolEvent<"Registry", RegistryEvent>, - chainId: ChainId, - dependencies: ProcessorDependencies, - ): IEventHandler<"Registry", RegistryEvent> { - if (isRoleGranted(event)) { - return new RoleGrantedHandler(event, chainId, dependencies); - } - throw new UnsupportedEventException("Registry", event.eventName as string); - } -} - -const isRoleGranted = ( - event: ProtocolEvent<"Registry", RegistryEvent>, -): event is ProtocolEvent<"Registry", "RoleGranted"> => { - return event.eventName === "RoleGranted"; -}; diff --git a/packages/processors/src/schemas/index.ts b/packages/processors/src/schemas/index.ts new file mode 100644 index 0000000..d7789f5 --- /dev/null +++ b/packages/processors/src/schemas/index.ts @@ -0,0 +1,2 @@ +export * from "./projectMetadata.js"; +export * from "./roundMetadata.js"; diff --git a/packages/processors/src/schemas/projectMetadata.ts b/packages/processors/src/schemas/projectMetadata.ts new file mode 100644 index 0000000..1543c31 --- /dev/null +++ b/packages/processors/src/schemas/projectMetadata.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; + +export const ProjectMetadataSchema = z.union([ + z + .object({ + title: z.string(), + description: z.string(), + }) + .passthrough() + .transform((data) => ({ type: "project" as const, ...data })), + z + .object({ + canonical: z.object({ + registryAddress: z.string(), + chainId: z.coerce.number(), + }), + }) + .transform((data) => ({ type: "project" as const, ...data })), + z.object({ + type: z.literal("program"), + name: z.string(), + }), + z + .object({ + name: z.string(), + }) + .transform((data) => ({ type: "program" as const, ...data })), +]); + +export type ProjectMetadata = z.infer; diff --git a/packages/processors/src/helpers/schemas.ts b/packages/processors/src/schemas/roundMetadata.ts similarity index 100% rename from packages/processors/src/helpers/schemas.ts rename to packages/processors/src/schemas/roundMetadata.ts diff --git a/packages/processors/test/index.spec.ts b/packages/processors/test/index.spec.ts deleted file mode 100644 index e383c0a..0000000 --- a/packages/processors/test/index.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { describe, it } from "vitest"; - -describe("dummy", () => { - it.skip("dummy", () => {}); -}); diff --git a/packages/processors/test/registry/handlers/profileCreated.handler.spec.ts b/packages/processors/test/registry/handlers/profileCreated.handler.spec.ts new file mode 100644 index 0000000..d2b1828 --- /dev/null +++ b/packages/processors/test/registry/handlers/profileCreated.handler.spec.ts @@ -0,0 +1,356 @@ +import { getAddress } from "viem"; +import { afterEach, beforeEach, describe, expect, it, Mock, 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 { Bytes32String, ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; + +import { ProcessorDependencies } from "../../../src/internal.js"; +import { ProfileCreatedHandler } from "../../../src/registry/handlers/index.js"; + +describe("ProfileCreatedHandler", () => { + let mockEvent: ProtocolEvent<"Registry", "ProfileCreated">; + let mockChainId: ChainId; + let mockDependencies: ProcessorDependencies; + const mockedTxHash = "0x6e5a7115323ac1712f7c27adff46df2216324a4ad615a8c9ce488c32a1f3a035"; + const mockedAddress = "0x48f33AE41E1762e1688125C4f1C536B1491dF803"; + + beforeEach(() => { + mockEvent = { + blockTimestamp: 123123123, + chainId: 10 as ChainId, + contractName: "Registry", + eventName: "ProfileCreated", + logIndex: 10, + srcAddress: mockedAddress, + transactionFields: { + hash: mockedTxHash, + transactionIndex: 10, + }, + blockNumber: 123, + params: { + profileId: "0x1231231234" as Bytes32String, + metadata: [1n, "cid-metadata"], + name: "Test Project", + nonce: 1n, + anchor: mockedAddress, + owner: mockedAddress, + }, + } as ProtocolEvent<"Registry", "ProfileCreated">; + + mockChainId = 10 as ChainId; + + mockDependencies = { + projectRepository: { + getPendingProjectRolesByRole: vi.fn().mockResolvedValue([]), + } as unknown as IProjectReadRepository, + evmProvider: { + getTransaction: vi.fn().mockResolvedValue({ from: mockedAddress }), + } as unknown as EvmProvider, + pricingProvider: { + getTokenPrice: vi.fn(), + } as unknown as IPricingProvider, + metadataProvider: { + getMetadata: vi.fn(), + } as unknown as IMetadataProvider, + roundRepository: {} as unknown as IRoundReadRepository, + }; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("handles ProfileCreated event and return the correct changeset", async () => { + (mockDependencies.metadataProvider.getMetadata as Mock).mockResolvedValueOnce({ + type: "program", + name: "Test Project", + }); + const profileCreatedHandler = new ProfileCreatedHandler( + mockEvent, + mockChainId, + mockDependencies, + ); + + const result = await profileCreatedHandler.handle(); + + expect(result).toEqual([ + { + type: "InsertProject", + args: { + project: { + tags: ["allo-v2", "program"], + chainId: mockChainId, + registryAddress: mockEvent.srcAddress, + id: mockEvent.params.profileId, + name: "Test Project", + nonce: 1n, + anchorAddress: mockEvent.params.anchor, + projectNumber: null, + metadataCid: mockEvent.params.metadata[1], + metadata: { type: "program", name: "Test Project" }, + createdByAddress: mockEvent.srcAddress, + createdAtBlock: BigInt(123), + updatedAtBlock: BigInt(123), + projectType: "canonical", + }, + }, + }, + { + type: "InsertProjectRole", + args: { + projectRole: { + chainId: mockChainId, + projectId: mockEvent.params.profileId, + address: mockEvent.params.owner, + role: "owner", + createdAtBlock: BigInt(123), + }, + }, + }, + ]); + + expect( + mockDependencies.projectRepository.getPendingProjectRolesByRole, + ).toHaveBeenCalledWith(mockChainId, mockEvent.params.profileId); + expect(mockDependencies.evmProvider.getTransaction).toHaveBeenCalledWith( + mockEvent.transactionFields.hash, + ); + expect(mockDependencies.metadataProvider.getMetadata).toHaveBeenCalledWith( + mockEvent.params.metadata[1], + ); + }); + + it("logs a warning if metadata parsing fails", async () => { + (mockDependencies.metadataProvider.getMetadata as Mock).mockResolvedValueOnce({ + invalid: "data", + }); + + const handler = new ProfileCreatedHandler(mockEvent, mockChainId, mockDependencies); + const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await handler.handle(); + + expect(consoleWarnSpy).toHaveBeenCalledWith({ + msg: `ProfileCreated: Failed to parse metadata for profile ${mockEvent.params.profileId}`, + event: mockEvent, + metadataCid: "cid-metadata", + metadata: { invalid: "data" }, + }); + }); + + it("returns an null metadata on changeset parsing fails", async () => { + (mockDependencies.metadataProvider.getMetadata as Mock).mockResolvedValueOnce({ + invalid: "data", + }); + + const handler = new ProfileCreatedHandler(mockEvent, mockChainId, mockDependencies); + + const result = await handler.handle(); + + expect(mockDependencies.metadataProvider.getMetadata).toHaveBeenCalledWith( + mockEvent.params.metadata[1], + ); + expect(mockDependencies.evmProvider.getTransaction).toHaveBeenCalledWith( + mockEvent.transactionFields.hash, + ); + expect(result).toEqual([ + { + type: "InsertProject", + args: { + project: { + tags: ["allo-v2"], + chainId: mockChainId, + registryAddress: mockEvent.srcAddress, + id: mockEvent.params.profileId, + name: "Test Project", + nonce: 1n, + anchorAddress: mockEvent.params.anchor, + projectNumber: null, + metadataCid: mockEvent.params.metadata[1], + metadata: null, + createdByAddress: mockEvent.srcAddress, + createdAtBlock: BigInt(123), + updatedAtBlock: BigInt(123), + projectType: "canonical", + }, + }, + }, + { + type: "InsertProjectRole", + args: { + projectRole: { + chainId: mockChainId, + projectId: mockEvent.params.profileId, + address: mockEvent.params.owner, + role: "owner", + createdAtBlock: BigInt(123), + }, + }, + }, + ]); + }); + + it("includes pending project roles in the changeset", async () => { + ( + mockDependencies.projectRepository.getPendingProjectRolesByRole as Mock + ).mockResolvedValueOnce([{ id: "1", address: mockedAddress }]); + + const handler = new ProfileCreatedHandler(mockEvent, mockChainId, mockDependencies); + const result = await handler.handle(); + + expect(result).toContainEqual({ + type: "InsertProjectRole", + args: { + projectRole: { + chainId: mockChainId, + projectId: mockEvent.params.profileId, + address: getAddress(mockedAddress), + role: "member", + createdAtBlock: BigInt(123), + }, + }, + }); + expect(result).toContainEqual({ + type: "DeletePendingProjectRoles", + args: { ids: ["1"] }, + }); + }); + + it("throws an error if getTransaction fails", async () => { + (mockDependencies.evmProvider.getTransaction as Mock).mockRejectedValueOnce( + new Error("Transaction not found"), + ); + + const handler = new ProfileCreatedHandler(mockEvent, mockChainId, mockDependencies); + + await expect(handler.handle()).rejects.toThrow("Transaction not found"); + expect(mockDependencies.evmProvider.getTransaction).toHaveBeenCalledWith( + mockEvent.transactionFields.hash, + ); + }); + + it("processes valid metadata successfully", async () => { + (mockDependencies.metadataProvider.getMetadata as Mock).mockResolvedValueOnce({ + canonical: { + registryAddress: "0x1234567890abcdef", + chainId: 1, + }, + }); + + const handler = new ProfileCreatedHandler(mockEvent, mockChainId, mockDependencies); + + const result = await handler.handle(); + + expect(result).toContainEqual({ + type: "InsertProject", + args: { + project: { + tags: ["allo-v2"], + chainId: mockChainId, + registryAddress: getAddress(mockEvent.srcAddress), + id: mockEvent.params.profileId, + name: "Test Project", + nonce: 1n, + anchorAddress: getAddress(mockEvent.params.anchor), + projectNumber: null, + metadataCid: mockEvent.params.metadata[1], + metadata: { + type: "project", + canonical: { + registryAddress: "0x1234567890abcdef", + chainId: 1, + }, + }, + createdByAddress: getAddress(mockedAddress), + createdAtBlock: BigInt(mockEvent.blockNumber), + updatedAtBlock: BigInt(mockEvent.blockNumber), + projectType: "linked", // As the metadata contains canonical, it should be "linked" + }, + }, + }); + }); + + it("returns correct changeset without pending roles", async () => { + (mockDependencies.metadataProvider.getMetadata as Mock).mockResolvedValueOnce({ + canonical: { + registryAddress: "0x1234567890abcdef", + chainId: 1, + }, + }); + + ( + mockDependencies.projectRepository.getPendingProjectRolesByRole as Mock + ).mockResolvedValueOnce([]); + + const handler = new ProfileCreatedHandler(mockEvent, mockChainId, mockDependencies); + const result = await handler.handle(); + + expect(result).toEqual([ + { + type: "InsertProject", + args: { + project: { + tags: ["allo-v2"], + chainId: mockChainId, + registryAddress: getAddress(mockEvent.srcAddress), + id: mockEvent.params.profileId, + name: "Test Project", + nonce: 1n, + anchorAddress: getAddress(mockEvent.params.anchor), + projectNumber: null, + metadataCid: mockEvent.params.metadata[1], + metadata: { + type: "project", + canonical: { + registryAddress: "0x1234567890abcdef", + chainId: 1, + }, + }, + createdByAddress: getAddress(mockedAddress), + createdAtBlock: BigInt(mockEvent.blockNumber), + updatedAtBlock: BigInt(mockEvent.blockNumber), + projectType: "linked", // As the metadata contains canonical, it should be "linked" + }, + }, + }, + { + type: "InsertProjectRole", + args: { + projectRole: { + chainId: mockChainId, + projectId: mockEvent.params.profileId, + address: getAddress(mockEvent.params.owner), + role: "owner", + createdAtBlock: BigInt(mockEvent.blockNumber), + }, + }, + }, + ]); + + expect( + mockDependencies.projectRepository.getPendingProjectRolesByRole, + ).toHaveBeenCalledWith(mockChainId, mockEvent.params.profileId); + expect(mockDependencies.metadataProvider.getMetadata).toHaveBeenCalledWith( + mockEvent.params.metadata[1], + ); + expect(mockDependencies.evmProvider.getTransaction).toHaveBeenCalledWith( + mockEvent.transactionFields.hash, + ); + }); + + it("throws when metadata provider fails", async () => { + (mockDependencies.metadataProvider.getMetadata as Mock).mockRejectedValueOnce( + new Error("Failed to fetch metadata"), + ); + + const handler = new ProfileCreatedHandler(mockEvent, mockChainId, mockDependencies); + + await expect(handler.handle()).rejects.toThrow("Failed to fetch metadata"); + expect(mockDependencies.metadataProvider.getMetadata).toHaveBeenCalledWith( + mockEvent.params.metadata[1], + ); + }); +}); diff --git a/packages/processors/test/registry/handlers/roleGranted.handler.spec.ts b/packages/processors/test/registry/handlers/roleGranted.handler.spec.ts new file mode 100644 index 0000000..32b4a32 --- /dev/null +++ b/packages/processors/test/registry/handlers/roleGranted.handler.spec.ts @@ -0,0 +1,125 @@ +import { getAddress, InvalidAddressError } from "viem"; +import { describe, expect, it, vi } from "vitest"; + +import { + ALLO_OWNER_ROLE, + Bytes32String, + ChainId, + ProtocolEvent, +} from "@grants-stack-indexer/shared"; + +import { ProcessorDependencies } from "../../../src/internal.js"; +import { RoleGrantedHandler } from "../../../src/registry/handlers/index.js"; // Adjust path if needed + +describe("RoleGrantedHandler", () => { + const mockProjectRepository = { + getProjectById: vi.fn(), + }; + + const dependencies = { + projectRepository: mockProjectRepository, + } as unknown as ProcessorDependencies; + + const chainId = 10 as ChainId; // Example chainId + const blockNumber = 123456; // Example blockNumber + const mockedAccount = "0x48f33AE41E1762e1688125C4f1C536B1491dF803"; + const mockedSender = "0xc0969723D577D31aB4bdF7e53C540c11298c56AF"; + const mockedEvent: ProtocolEvent<"Registry", "RoleGranted"> = { + blockTimestamp: 123123123, + chainId: 10, + contractName: "Registry", + eventName: "RoleGranted", + params: { + role: ALLO_OWNER_ROLE, + account: mockedAccount, + sender: mockedSender, + }, + blockNumber, + logIndex: 10, + srcAddress: mockedAccount, + transactionFields: { + hash: "0x123", + transactionIndex: 1, + }, + }; + + it("returns an empty array if role is ALLO_OWNER_ROLE", async () => { + const event = mockedEvent; + const handler = new RoleGrantedHandler(event, chainId, dependencies); + + const result = await handler.handle(); + + expect(result).toEqual([]); + }); + + it("returns InsertProjectRole if project exists", async () => { + mockedEvent["params"]["role"] = "0x1231231234" as Bytes32String; + const event = mockedEvent; + + mockProjectRepository.getProjectById.mockResolvedValueOnce({ id: "projectId" }); + + const handler = new RoleGrantedHandler(event, chainId, dependencies); + + const result = await handler.handle(); + + expect(result).toEqual([ + { + type: "InsertProjectRole", + args: { + projectRole: { + chainId: chainId, + projectId: "projectId", + address: getAddress(mockedAccount), + role: "member", + createdAtBlock: BigInt(blockNumber), + }, + }, + }, + ]); + }); + + it("returns InsertPendingProjectRole if project does not exist", async () => { + mockedEvent["params"]["role"] = "0x1231231234" as Bytes32String; + const event = mockedEvent; + + mockProjectRepository.getProjectById.mockResolvedValueOnce(null); + + const handler = new RoleGrantedHandler(event, chainId, dependencies); + + const result = await handler.handle(); + + expect(result).toEqual([ + { + type: "InsertPendingProjectRole", + args: { + pendingProjectRole: { + chainId: chainId, + role: event.params.role.toLowerCase(), + address: getAddress(mockedAccount), + createdAtBlock: BigInt(blockNumber), + }, + }, + }, + ]); + }); + + it("throws an error if getAddress throws an error for an invalid account", async () => { + mockedEvent["params"]["account"] = "0xinvalid-address"; // Invalid account address + const event = mockedEvent; + + const handler = new RoleGrantedHandler(event, chainId, dependencies); + + await expect(handler.handle()).rejects.toThrow(InvalidAddressError); + }); + + it("should throw an error if projectRepository throws an error", async () => { + mockedEvent["params"]["role"] = "0x1231231234" as Bytes32String; + const event = mockedEvent; + + mockProjectRepository.getProjectById.mockRejectedValueOnce(new Error()); + + const handler = new RoleGrantedHandler(event, chainId, dependencies); + + await expect(handler.handle()).rejects.toThrow(Error); + }); +}); diff --git a/packages/processors/test/registry/registry.processor.spec.ts b/packages/processors/test/registry/registry.processor.spec.ts new file mode 100644 index 0000000..bc5a252 --- /dev/null +++ b/packages/processors/test/registry/registry.processor.spec.ts @@ -0,0 +1,75 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { ChainId, ProtocolEvent, RegistryEvent } from "@grants-stack-indexer/shared"; + +import { ProcessorDependencies, UnsupportedEventException } from "../../src/internal.js"; +import { ProfileCreatedHandler } from "../../src/registry/handlers/profileCreated.hanlder.js"; +import { RoleGrantedHandler } from "../../src/registry/handlers/roleGranted.handler.js"; +import { RegistryProcessor } from "../../src/registry/registry.processor.js"; + +// Mock the handlers and their handle methods +vi.mock("../../src/registry/handlers/roleGranted.handler.js", () => { + const RoleGrantedHandler = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + RoleGrantedHandler.prototype.handle = vi.fn(); + return { + RoleGrantedHandler, + }; +}); + +// Mock the handlers and their handle methods +vi.mock("../../src/registry/handlers/profileCreated.handler.js", () => { + const ProfileCreatedHandler = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + ProfileCreatedHandler.prototype.handle = vi.fn(); + return { + ProfileCreatedHandler, + }; +}); + +describe("RegistryProcessor", () => { + const chainId: ChainId = 10 as ChainId; // Replace with appropriate chainId + const dependencies: ProcessorDependencies = {} as ProcessorDependencies; // Replace with actual dependencies + + afterEach(() => { + vi.resetAllMocks(); + }); + + it("should throw UnsupportedEventException for unsupported events", async () => { + const event: ProtocolEvent<"Registry", RegistryEvent> = { + eventName: "UnsupportedEvent", + } as unknown as ProtocolEvent<"Registry", RegistryEvent>; + + const processor = new RegistryProcessor(chainId, dependencies); + + await expect(processor.process(event)).rejects.toThrow(UnsupportedEventException); + }); + + it("should call ProfileCreatedHandler", async () => { + const event: ProtocolEvent<"Registry", "ProfileCreated"> = { + eventName: "ProfileCreated", + } as ProtocolEvent<"Registry", "ProfileCreated">; + + vi.spyOn(ProfileCreatedHandler.prototype, "handle").mockResolvedValue([]); + + const processor = new RegistryProcessor(chainId, dependencies); + const result = await processor.process(event); + + expect(ProfileCreatedHandler.prototype.handle).toHaveBeenCalled(); + expect(result).toEqual([]); // Check if handle returns [] + }); + + it("should call RoleGrantedHandler", async () => { + const event: ProtocolEvent<"Registry", "RoleGranted"> = { + eventName: "RoleGranted", + } as ProtocolEvent<"Registry", "RoleGranted">; + + vi.spyOn(RoleGrantedHandler.prototype, "handle").mockResolvedValue([]); + + const processor = new RegistryProcessor(chainId, dependencies); + const result = await processor.process(event); + + expect(RoleGrantedHandler.prototype.handle).toHaveBeenCalled(); + expect(result).toEqual([]); // Check if handle returns [] + }); +});