diff --git a/package.json b/package.json index 7a101172..9c8b3879 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,20 @@ "deploy:development": "docker buildx build . --platform=linux/amd64 -t registry.fly.io/indexer-development:latest && docker push registry.fly.io/indexer-development:latest && flyctl -c fly.development.toml --app indexer-development deploy -i registry.fly.io/indexer-development:latest" }, "imports": { - "#abis/*": { - "default": "./src/indexer/abis/*" + "#src/*": { + "default": "./src/*" + }, + "#database/*": { + "default": "./src/database/*" + }, + "#indexer/*": { + "default": "./src/indexer/*" + }, + "#prices/*": { + "default": "./src/prices/*" + }, + "#test/*": { + "default": "./src/test/*" } }, "author": "", diff --git a/src/index.ts b/src/index.ts index 0be278aa..79d5a32a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,7 @@ import { decodeJsonWithBigInts } from "./utils/index.js"; import { Block } from "chainsauce/dist/cache.js"; import { createPublicClient, http } from "viem"; import { IndexerEvents } from "chainsauce/dist/indexer.js"; +import { getEventHandler } from "./indexer/utils/getEventHandler.js"; const RESOURCE_MONITOR_INTERVAL_MS = 1 * 60 * 1000; // every minute @@ -539,9 +540,45 @@ async function catchupAndWatchChain( indexer.on("event", async (args) => { try { - // console.time(args.event.name); - // do not await donation inserts as they are write only - if (args.event.name === "Voted") { + const { + event: { contractName, name: eventName }, + } = args; + + // for now we check for handlers based on: + // - protocol name: AlloV1 / AlloV2 + // - contract name example: ProjectRegistry + // - contract version: V1 / V2 + // and then the event name. + // but we can combine in the future this way + // with another object that has event handlers by name + // like generic event handlers that are not depending on Protocol/Name/Version contracts + const handler = getEventHandler(contractName, eventName); + if (handler) { + const changesets = await handler(args); + if (["Voted", "Allocated"].includes(eventName)) { + try { + await db.applyChanges(changesets); + } catch (err: unknown) { + if (args.event.name === "Voted") { + indexerLogger.warn({ + msg: "error while processing vote", + err, + }); + } else if (args.event.name === "Allocated") { + indexerLogger.warn({ + msg: "error while processing allocation", + err, + }); + } + } + } else { + for (const changeset of changesets) { + await db.applyChange(changeset); + } + } + } else if (args.event.name === "Voted") { + // console.time(args.event.name); + // do not await donation inserts as they are write only handleAlloV1Event(args) .then((changesets) => db.applyChanges(changesets)) .catch((err: unknown) => { diff --git a/src/indexer/abis/index.ts b/src/indexer/abis/index.ts index 1082fb03..abd9f94e 100644 --- a/src/indexer/abis/index.ts +++ b/src/indexer/abis/index.ts @@ -1,5 +1,5 @@ // V1.1 -import ProjectRegistryV1 from "./allo-v1/v1/ProjectRegistry.js"; +import ProjectRegistryV1 from "../contracts/alloV1/projectRegistry/v1/abi/ProjectRegistry.js"; import RoundFactoryV1 from "./allo-v1/v1/RoundFactory.js"; import RoundImplementationV1 from "./allo-v1/v1/RoundImplementation.js"; import QuadraticFundingVotingStrategyFactoryV1 from "./allo-v1/v1/QuadraticFundingVotingStrategyFactory.js"; @@ -8,7 +8,7 @@ import ProgramFactoryV1 from "./allo-v1/v1/ProgramFactory.js"; import ProgramImplementationV1 from "./allo-v1/v1/ProgramImplementation.js"; // V1.2 -import ProjectRegistryV2 from "./allo-v1/v2/ProjectRegistry.js"; +import ProjectRegistryV2 from "../contracts/alloV1/projectRegistry/v2/abi/ProjectRegistry.js"; import RoundFactoryV2 from "./allo-v1/v2/RoundFactory.js"; import RoundImplementationV2 from "./allo-v1/v2/RoundImplementation.js"; import QuadraticFundingVotingStrategyFactoryV2 from "./allo-v1/v2/QuadraticFundingVotingStrategyFactory.js"; diff --git a/src/indexer/allo/v1/handleEvent.test.ts b/src/indexer/allo/v1/handleEvent.test.ts index a36bc328..626c1364 100644 --- a/src/indexer/allo/v1/handleEvent.test.ts +++ b/src/indexer/allo/v1/handleEvent.test.ts @@ -112,59 +112,6 @@ describe("handleEvent", () => { vi.resetAllMocks(); }); - describe("ProjectCreated", () => { - test("should insert project", async () => { - const changesets = await handleEvent({ - ...DEFAULT_ARGS, - event: { - ...DEFAULT_ARGS.event, - contractName: "AlloV1/ProjectRegistry/V2", - name: "ProjectCreated", - params: { - projectID: 1n, - owner: addressTwo, - }, - }, - context: { - ...DEFAULT_ARGS.context, - rpcClient: MOCK_RPC_CLIENT(), - }, - }); - - expect(changesets).toHaveLength(2); - - expect(changesets[0]).toEqual({ - type: "InsertProject", - project: { - chainId: 1, - createdByAddress: addressTwo, - createdAtBlock: 1n, - updatedAtBlock: 1n, - id: "0xe31382b762a33e568e1e9ef38d64f4a2b4dbb51ec0f79ec41779fc5be79ead32", - name: "", - metadata: null, - metadataCid: null, - projectNumber: 1, - registryAddress: addressOne, - tags: ["allo-v1"], - projectType: "canonical", - }, - }); - - expect(changesets[1]).toEqual({ - type: "InsertProjectRole", - projectRole: { - chainId: 1, - projectId: - "0xe31382b762a33e568e1e9ef38d64f4a2b4dbb51ec0f79ec41779fc5be79ead32", - address: addressTwo, - role: "owner", - createdAtBlock: 1n, - }, - }); - }); - }); - describe("MetadataUpdated", () => { test("should fetch and update metadata", async () => { const changesets = await handleEvent({ diff --git a/src/indexer/allo/v1/handleEvent.ts b/src/indexer/allo/v1/handleEvent.ts index 95ddf7db..f684c223 100644 --- a/src/indexer/allo/v1/handleEvent.ts +++ b/src/indexer/allo/v1/handleEvent.ts @@ -34,6 +34,7 @@ import { import { ProjectMetadataSchema } from "../../projectMetadata.js"; import { updateApplicationStatus } from "../application.js"; import { getDateFromTimestamp } from "../../../utils/index.js"; +import { fullProjectId } from "../../utils/index.js"; enum ApplicationStatus { PENDING = 0, @@ -43,17 +44,6 @@ enum ApplicationStatus { IN_REVIEW, } -function fullProjectId( - projectChainId: number, - projectId: number, - projectRegistryAddress: string -) { - return ethers.utils.solidityKeccak256( - ["uint256", "address", "uint256"], - [projectChainId, projectRegistryAddress, projectId] - ); -} - export async function handleEvent( args: EventHandlerArgs ): Promise { @@ -75,49 +65,6 @@ export async function handleEvent( switch (event.name) { // -- PROJECTS - case "ProjectCreated": { - const projectId = fullProjectId( - chainId, - Number(event.params.projectID), - event.address - ); - - const tx = await rpcClient.getTransaction({ - hash: event.transactionHash, - }); - - const createdBy = tx.from; - - return [ - { - type: "InsertProject", - project: { - tags: ["allo-v1"], - chainId, - registryAddress: parseAddress(event.address), - id: projectId, - name: "", - projectNumber: Number(event.params.projectID), - metadataCid: null, - metadata: null, - createdByAddress: parseAddress(createdBy), - createdAtBlock: event.blockNumber, - updatedAtBlock: event.blockNumber, - projectType: "canonical", - }, - }, - { - type: "InsertProjectRole", - projectRole: { - chainId, - projectId, - address: parseAddress(event.params.owner), - role: "owner", - createdAtBlock: event.blockNumber, - }, - }, - ]; - } case "MetadataUpdated": { const projectId = fullProjectId( diff --git a/src/indexer/contracts/__tests__/eventHandlers.test.ts b/src/indexer/contracts/__tests__/eventHandlers.test.ts new file mode 100644 index 00000000..30a2dceb --- /dev/null +++ b/src/indexer/contracts/__tests__/eventHandlers.test.ts @@ -0,0 +1,249 @@ +import { vi, describe, test, expect, beforeEach } from "vitest"; +import { Address as ChecksumAddress, Hex, PublicClient } from "viem"; +import { EventHandlerArgs } from "chainsauce"; +import { Logger } from "pino"; +import { Database } from "#database/index.js"; +import { PriceProvider } from "#prices/provider.js"; +import { Indexer } from "#indexer/indexer.js"; +import { getEventHandler } from "#indexer/utils/getEventHandler.js"; +import { TestPriceProvider } from "#test/utils.js"; + +const addressOne = + "0x0000000000000000000000000000000000000001" as ChecksumAddress; +const addressTwo = + "0x0000000000000000000000000000000000000002" as ChecksumAddress; + +const MOCK_PRICE_PROVIDER = new TestPriceProvider() as unknown as PriceProvider; + +// eslint-disable-next-line @typescript-eslint/require-await +async function MOCK_IPFS_GET(cid: string) { + switch (cid) { + case "project-cid": + return { + title: "my project", + description: "my project description", + } as TReturn; + + case "program-cid": + return { + name: "my program", + } as TReturn; + + case "round-cid": + return { + name: "my round", + } as TReturn; + + default: + throw new Error(`unexpected cid: ${cid}`); + } +} + +function MOCK_RPC_CLIENT() { + return { + getTransaction: vi + .fn() + .mockResolvedValue({ blockNumber: 1n, from: addressTwo }), + } as unknown as PublicClient; +} + +function MOCK_BLOCK_TIMESTAMP_IN_MS() { + return vi.fn().mockResolvedValue(0); +} + +const MOCK_LOGGER = { + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, +} as unknown as Logger; + +const MOCK_DB = { + query: vi.fn(), +} as unknown as Database; + +const MOCK_READ_CONTRACT: EventHandlerArgs["readContract"] = vi.fn(); + +const MOCK_SUBSCRIBE_TO_CONTRACT: EventHandlerArgs["subscribeToContract"] = + vi.fn(); + +const MOCK_UNSUBSCRIBE_FROM_CONTRACT: EventHandlerArgs["unsubscribeFromContract"] = + vi.fn(); + +const DEFAULT_ARGS = { + chainId: 1, + event: { + name: null, + blockNumber: 1n, + logIndex: 0, + transactionHash: "0x" as Hex, + address: addressOne, + topic: "0x" as Hex, + params: {}, + }, + subscribeToContract: MOCK_SUBSCRIBE_TO_CONTRACT, + unsubscribeFromContract: MOCK_UNSUBSCRIBE_FROM_CONTRACT, + readContract: MOCK_READ_CONTRACT, + getBlock: vi.fn().mockResolvedValue({ timestamp: 0 }), + context: { + priceProvider: MOCK_PRICE_PROVIDER, + ipfsGet: MOCK_IPFS_GET, + chainId: 1, + logger: MOCK_LOGGER, + db: MOCK_DB, + rpcClient: MOCK_RPC_CLIENT(), + blockTimestampInMs: MOCK_BLOCK_TIMESTAMP_IN_MS(), + }, +}; + +describe("handleEvent", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe("AlloV1/ProjectRegistry/V1", () => { + test("ProjectCreated event should insert project", async () => { + const contractName = "AlloV1/ProjectRegistry/V1"; + const eventName = "ProjectCreated"; + + const args: EventHandlerArgs = { + ...DEFAULT_ARGS, + event: { + ...DEFAULT_ARGS.event, + contractName, + name: eventName, + params: { + projectID: 1n, + owner: addressTwo, + }, + }, + context: { + ...DEFAULT_ARGS.context, + rpcClient: MOCK_RPC_CLIENT(), + }, + }; + + const handler = getEventHandler(contractName, eventName); + + expect(handler).toBeDefined(); + if (!handler) return; + + const changesets = await handler(args); + + expect(changesets).toHaveLength(2); + + expect(changesets[0]).toEqual({ + type: "InsertProject", + project: { + chainId: 1, + createdByAddress: addressTwo, + createdAtBlock: 1n, + updatedAtBlock: 1n, + id: "0xe31382b762a33e568e1e9ef38d64f4a2b4dbb51ec0f79ec41779fc5be79ead32", + name: "", + metadata: null, + metadataCid: null, + projectNumber: 1, + registryAddress: addressOne, + tags: ["allo-v1"], + projectType: "canonical", + }, + }); + + expect(changesets[1]).toEqual({ + type: "InsertProjectRole", + projectRole: { + chainId: 1, + projectId: + "0xe31382b762a33e568e1e9ef38d64f4a2b4dbb51ec0f79ec41779fc5be79ead32", + address: addressTwo, + role: "owner", + createdAtBlock: 1n, + }, + }); + }); + }); + + describe("AlloV1/ProjectCreated/V2", () => { + test("ProjectCreated event should insert project", async () => { + const contractName = "AlloV1/ProjectRegistry/V2"; + const eventName = "ProjectCreated"; + + const args: EventHandlerArgs = { + ...DEFAULT_ARGS, + event: { + ...DEFAULT_ARGS.event, + contractName, + name: eventName, + params: { + projectID: 1n, + owner: addressTwo, + }, + }, + context: { + ...DEFAULT_ARGS.context, + rpcClient: MOCK_RPC_CLIENT(), + }, + }; + + const handler = getEventHandler(contractName, eventName); + + expect(handler).toBeDefined(); + if (!handler) return; + + const changesets = await handler(args); + + expect(changesets).toHaveLength(2); + + expect(changesets[0]).toEqual({ + type: "InsertProject", + project: { + chainId: 1, + createdByAddress: addressTwo, + createdAtBlock: 1n, + updatedAtBlock: 1n, + id: "0xe31382b762a33e568e1e9ef38d64f4a2b4dbb51ec0f79ec41779fc5be79ead32", + name: "", + metadata: null, + metadataCid: null, + projectNumber: 1, + registryAddress: addressOne, + tags: ["allo-v1"], + projectType: "canonical", + }, + }); + + expect(changesets[1]).toEqual({ + type: "InsertProjectRole", + projectRole: { + chainId: 1, + projectId: + "0xe31382b762a33e568e1e9ef38d64f4a2b4dbb51ec0f79ec41779fc5be79ead32", + address: addressTwo, + role: "owner", + createdAtBlock: 1n, + }, + }); + }); + + test("MetadataUpdated event, NOT MIGRATED -> getEventHandler should return undefined", () => { + const contractName = "AlloV1/ProjectRegistry/V2"; + const eventName = "MetadataUpdated"; + + const handler = getEventHandler(contractName, eventName); + + expect(handler).toBeUndefined(); + }); + }); + + describe("Unknown contract name", () => { + test("getEventHandler should return undefined", () => { + const contractName = "Unknown"; + const eventName = "ProjectCreated"; + + const handler = getEventHandler(contractName, eventName); + + expect(handler).toBeUndefined(); + }); + }); +}); diff --git a/src/indexer/contracts/alloV1/index.ts b/src/indexer/contracts/alloV1/index.ts new file mode 100644 index 00000000..3d017147 --- /dev/null +++ b/src/indexer/contracts/alloV1/index.ts @@ -0,0 +1,6 @@ +import { ProtocolContracts } from "#indexer/contracts/types.js"; +import { projectRegistry } from "./projectRegistry/index.js"; + +export const AlloV1: ProtocolContracts = { + ProjectRegistry: projectRegistry, +}; diff --git a/src/indexer/contracts/alloV1/projectRegistry/index.ts b/src/indexer/contracts/alloV1/projectRegistry/index.ts new file mode 100644 index 00000000..bc9d0ece --- /dev/null +++ b/src/indexer/contracts/alloV1/projectRegistry/index.ts @@ -0,0 +1,8 @@ +import { NameContracts } from "#indexer/contracts/types.js"; +import { projectRegistryV1 } from "./v1/index.js"; +import { projectRegistryV2 } from "./v2/index.js"; + +export const projectRegistry: NameContracts = { + V1: projectRegistryV1, + V2: projectRegistryV2, +}; diff --git a/src/indexer/abis/allo-v1/v1/ProjectRegistry.ts b/src/indexer/contracts/alloV1/projectRegistry/v1/abi/ProjectRegistry.ts similarity index 100% rename from src/indexer/abis/allo-v1/v1/ProjectRegistry.ts rename to src/indexer/contracts/alloV1/projectRegistry/v1/abi/ProjectRegistry.ts diff --git a/src/indexer/contracts/alloV1/projectRegistry/v1/index.ts b/src/indexer/contracts/alloV1/projectRegistry/v1/index.ts new file mode 100644 index 00000000..510ae9b9 --- /dev/null +++ b/src/indexer/contracts/alloV1/projectRegistry/v1/index.ts @@ -0,0 +1,10 @@ +import abi from "./abi/ProjectRegistry.js"; +import { Contract } from "#indexer/contracts/types.js"; +import { projectCreatedHandler } from "../v2/eventHandlers/projectCreated.js"; + +export const projectRegistryV1: Contract = { + abi, + handlers: { + ProjectCreated: projectCreatedHandler, + }, +}; diff --git a/src/indexer/abis/allo-v1/v2/ProjectRegistry.ts b/src/indexer/contracts/alloV1/projectRegistry/v2/abi/ProjectRegistry.ts similarity index 100% rename from src/indexer/abis/allo-v1/v2/ProjectRegistry.ts rename to src/indexer/contracts/alloV1/projectRegistry/v2/abi/ProjectRegistry.ts diff --git a/src/indexer/contracts/alloV1/projectRegistry/v2/eventHandlers/index.ts b/src/indexer/contracts/alloV1/projectRegistry/v2/eventHandlers/index.ts new file mode 100644 index 00000000..6aebdd73 --- /dev/null +++ b/src/indexer/contracts/alloV1/projectRegistry/v2/eventHandlers/index.ts @@ -0,0 +1,5 @@ +import { projectCreatedHandler } from "./projectCreated.js"; + +export const projectRegistryV2Handlers = { + ProjectCreated: projectCreatedHandler, +}; diff --git a/src/indexer/contracts/alloV1/projectRegistry/v2/eventHandlers/projectCreated.ts b/src/indexer/contracts/alloV1/projectRegistry/v2/eventHandlers/projectCreated.ts new file mode 100644 index 00000000..1cbd92c2 --- /dev/null +++ b/src/indexer/contracts/alloV1/projectRegistry/v2/eventHandlers/projectCreated.ts @@ -0,0 +1,63 @@ +import { EventHandlerArgs } from "chainsauce"; +import type { Indexer } from "#indexer/indexer.js"; +import { fullProjectId } from "#indexer/utils/fullProjectId.js"; +import { EventHandler } from "#indexer/contracts/types.js"; +import { parseAddress } from "#src/address.js"; + +export const projectCreatedHandler: EventHandler = async ( + args: EventHandlerArgs +) => { + const { + chainId, + event, + context: { rpcClient }, + } = args; + + // We check here the event name only to avoid type errors + // of event.params.projectID + //! TODO Fix it to remove the event.name check + if (event.name === "ProjectCreated") { + const projectId = fullProjectId( + chainId, + Number(event.params.projectID), + event.address + ); + + const tx = await rpcClient.getTransaction({ + hash: event.transactionHash, + }); + + const createdBy = tx.from; + + return [ + { + type: "InsertProject", + project: { + tags: ["allo-v1"], + chainId, + registryAddress: parseAddress(event.address), + id: projectId, + name: "", + projectNumber: Number(event.params.projectID), + metadataCid: null, + metadata: null, + createdByAddress: parseAddress(createdBy), + createdAtBlock: event.blockNumber, + updatedAtBlock: event.blockNumber, + projectType: "canonical", + }, + }, + { + type: "InsertProjectRole", + projectRole: { + chainId, + projectId, + address: parseAddress(event.params.owner), + role: "owner", + createdAtBlock: event.blockNumber, + }, + }, + ]; + } + return []; +}; diff --git a/src/indexer/contracts/alloV1/projectRegistry/v2/index.ts b/src/indexer/contracts/alloV1/projectRegistry/v2/index.ts new file mode 100644 index 00000000..7b435b7e --- /dev/null +++ b/src/indexer/contracts/alloV1/projectRegistry/v2/index.ts @@ -0,0 +1,8 @@ +import abi from "./abi/ProjectRegistry.js"; +import { Contract } from "#indexer/contracts/types.js"; +import { projectRegistryV2Handlers } from "./eventHandlers/index.js"; + +export const projectRegistryV2: Contract = { + abi, + handlers: projectRegistryV2Handlers, +}; diff --git a/src/indexer/contracts/alloV2/index.ts b/src/indexer/contracts/alloV2/index.ts new file mode 100644 index 00000000..c2c8fcac --- /dev/null +++ b/src/indexer/contracts/alloV2/index.ts @@ -0,0 +1 @@ +export const AlloV2 = {}; diff --git a/src/indexer/contracts/index.ts b/src/indexer/contracts/index.ts new file mode 100644 index 00000000..5eea4fba --- /dev/null +++ b/src/indexer/contracts/index.ts @@ -0,0 +1,8 @@ +import { AlloV1 } from "./alloV1/index.js"; +import { AlloV2 } from "./alloV2/index.js"; +import { Contracts } from "./types.js"; + +export const contracts: Contracts = { + AlloV1, + AlloV2, +}; diff --git a/src/indexer/contracts/types.ts b/src/indexer/contracts/types.ts new file mode 100644 index 00000000..cfff5daa --- /dev/null +++ b/src/indexer/contracts/types.ts @@ -0,0 +1,24 @@ +import { EventHandlerArgs } from "chainsauce"; +import { Changeset } from "#database/index.js"; +import { Indexer } from "#indexer/indexer.js"; + +export interface EventHandler { + (args: EventHandlerArgs): Promise; +} + +export interface Contract { + abi?: any; + handlers?: { [eventName: string]: EventHandler }; +} + +export interface NameContracts { + [version: string]: Contract; +} + +export interface ProtocolContracts { + [name: string]: NameContracts; +} + +export interface Contracts { + [protocol: string]: ProtocolContracts; +} diff --git a/src/indexer/utils/fullProjectId.ts b/src/indexer/utils/fullProjectId.ts new file mode 100644 index 00000000..0e3f4a32 --- /dev/null +++ b/src/indexer/utils/fullProjectId.ts @@ -0,0 +1,12 @@ +import { ethers } from "ethers"; + +export function fullProjectId( + projectChainId: number, + projectId: number, + projectRegistryAddress: string +) { + return ethers.utils.solidityKeccak256( + ["uint256", "address", "uint256"], + [projectChainId, projectRegistryAddress, projectId] + ); +} diff --git a/src/indexer/utils/getEventHandler.ts b/src/indexer/utils/getEventHandler.ts new file mode 100644 index 00000000..8b95faca --- /dev/null +++ b/src/indexer/utils/getEventHandler.ts @@ -0,0 +1,45 @@ +import { contracts } from "#indexer/contracts/index.js"; +import { EventHandler } from "#indexer/contracts/types.js"; + +export const getEventHandler = ( + contractName: string, + eventName: string +): EventHandler | undefined => { + const contractNameParts = contractName.split("/"); + + if (contractNameParts.length !== 3) { + // Invalid format, return undefined to allow fallback to legacy event handler + // Expected format: protocol/name/version, example: AlloV1/ProjectRegistry/V2 + return undefined; + } + + const [protocol, name, version] = contractNameParts; + + const protocolContracts = contracts[protocol as keyof typeof contracts]; + if (!protocolContracts) { + // Protocol not found, return undefined to allow fallback to legacy event handler + return undefined; + } + + const nameContracts = + protocolContracts[name as keyof typeof protocolContracts]; + if (!nameContracts) { + // Name not found, return undefined to allow fallback to legacy event handler + return undefined; + } + + const contract = nameContracts[version as keyof typeof nameContracts]; + if (!contract) { + // Version not found, return undefined to allow fallback to legacy event handler + return undefined; + } + + // Check for event handler within the contract version + const eventHandler = contract.handlers?.[eventName]; + if (!eventHandler) { + // Event handler not found, return undefined to allow fallback to legacy event handler + return undefined; + } + + return eventHandler; +}; diff --git a/src/indexer/utils/index.ts b/src/indexer/utils/index.ts new file mode 100644 index 00000000..aea38178 --- /dev/null +++ b/src/indexer/utils/index.ts @@ -0,0 +1,2 @@ +export { fullProjectId } from "./fullProjectId.js"; +export { getEventHandler } from "./getEventHandler.js";