From fc23ce324557c3c5cb3e50dcb74185bc31a1b58b Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:40:50 -0300 Subject: [PATCH 1/2] feat: direct allocation strategy & direct allocated event handler --- apps/indexer/config.yaml | 2 +- apps/processing/.env.example | 2 +- .../data-flow/test/unit/orchestrator.spec.ts | 1 + .../directAllocation.handler.ts | 38 ++++ .../handlers/directAllocated.handler.ts | 91 ++++++++ .../directAllocation/handlers/index.ts | 1 + .../strategy/directAllocation/index.ts | 2 + .../handlers/allocated.handler.ts | 12 +- .../processors/strategy/helpers/allocated.ts | 8 + .../src/processors/strategy/helpers/index.ts | 1 + .../src/processors/strategy/index.ts | 1 + .../src/processors/strategy/mapping.ts | 3 + .../directAllocation.handler.spec.ts | 94 ++++++++ .../handlers/directAllocated.handler.spec.ts | 202 ++++++++++++++++++ .../interfaces/projectRepository.interface.ts | 9 + .../repositories/kysely/project.repository.ts | 7 + packages/shared/src/types/events/strategy.ts | 14 +- 17 files changed, 476 insertions(+), 12 deletions(-) create mode 100644 packages/processors/src/processors/strategy/directAllocation/directAllocation.handler.ts create mode 100644 packages/processors/src/processors/strategy/directAllocation/handlers/directAllocated.handler.ts create mode 100644 packages/processors/src/processors/strategy/directAllocation/handlers/index.ts create mode 100644 packages/processors/src/processors/strategy/directAllocation/index.ts create mode 100644 packages/processors/src/processors/strategy/helpers/allocated.ts create mode 100644 packages/processors/test/strategy/directAllocation/directAllocation.handler.spec.ts create mode 100644 packages/processors/test/strategy/directAllocation/handlers/directAllocated.handler.spec.ts diff --git a/apps/indexer/config.yaml b/apps/indexer/config.yaml index 7e16494..befb356 100644 --- a/apps/indexer/config.yaml +++ b/apps/indexer/config.yaml @@ -76,7 +76,7 @@ contracts: - event: AllocatedWithNft(address indexed recipientId, uint256 votes, address nft, address allocator) # DirectAllocated - - event: DirectAllocated(address indexed recipient, uint256 amount, address token, address sender) + - event: DirectAllocated(bytes32 indexed profileId, address profileOwner, uint256 amount, address token, address sender) # RecipientStatusUpdated - event: RecipientStatusUpdated(address indexed recipientId, uint256 applicationId, uint8 status, address sender) diff --git a/apps/processing/.env.example b/apps/processing/.env.example index 3e9bba5..79ded2e 100644 --- a/apps/processing/.env.example +++ b/apps/processing/.env.example @@ -13,7 +13,7 @@ INDEXER_ADMIN_SECRET=testing IPFS_GATEWAYS_URL=["https://ipfs.io","https://gateway.pinata.cloud","https://dweb.link", "https://ipfs.eth.aragon.network"] -PRICING_SOURCE= #coingecko | dummy +PRICING_SOURCE= # 'coingecko' or 'dummy' COINGECKO_API_KEY={{YOUR_KEY}} COINGECKO_API_TYPE=demo \ No newline at end of file diff --git a/packages/data-flow/test/unit/orchestrator.spec.ts b/packages/data-flow/test/unit/orchestrator.spec.ts index 830b9fe..ffc8ce7 100644 --- a/packages/data-flow/test/unit/orchestrator.spec.ts +++ b/packages/data-flow/test/unit/orchestrator.spec.ts @@ -284,6 +284,7 @@ describe("Orchestrator", { sequential: true }, () => { UpdatedRegistrationWithStatus: "", UpdatedRegistration: "", UpdatedRegistrationWithApplicationId: "", + DirectAllocated: "", }; for (const event of Object.keys(strategyEvents) as StrategyEvent[]) { diff --git a/packages/processors/src/processors/strategy/directAllocation/directAllocation.handler.ts b/packages/processors/src/processors/strategy/directAllocation/directAllocation.handler.ts new file mode 100644 index 0000000..4b8396d --- /dev/null +++ b/packages/processors/src/processors/strategy/directAllocation/directAllocation.handler.ts @@ -0,0 +1,38 @@ +import { Changeset } from "@grants-stack-indexer/repository"; +import { ChainId, ProcessorEvent, StrategyEvent } from "@grants-stack-indexer/shared"; + +import { ProcessorDependencies, UnsupportedEventException } from "../../../internal.js"; +import { BaseStrategyHandler } from "../index.js"; +import { DirectAllocatedHandler } from "./handlers/index.js"; + +const STRATEGY_NAME = "allov2.DirectAllocationStrategy"; + +/** + * This handler is responsible for processing events related to the + * Direct Allocation strategy. + * + * The following events are currently handled by this strategy: + * - DirectAllocated + */ +export class DirectAllocationStrategyHandler 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 "DirectAllocated": + return new DirectAllocatedHandler( + event as ProcessorEvent<"Strategy", "DirectAllocated">, + this.chainId, + this.dependencies, + ).handle(); + default: + throw new UnsupportedEventException("Strategy", event.eventName); + } + } +} diff --git a/packages/processors/src/processors/strategy/directAllocation/handlers/directAllocated.handler.ts b/packages/processors/src/processors/strategy/directAllocation/handlers/directAllocated.handler.ts new file mode 100644 index 0000000..7be00f4 --- /dev/null +++ b/packages/processors/src/processors/strategy/directAllocation/handlers/directAllocated.handler.ts @@ -0,0 +1,91 @@ +import { getAddress, zeroAddress } from "viem"; + +import { Changeset, Donation } from "@grants-stack-indexer/repository"; +import { ChainId, getTokenOrThrow, ProcessorEvent } from "@grants-stack-indexer/shared"; + +import { getTokenAmountInUsd } from "../../../../helpers/index.js"; +import { IEventHandler, ProcessorDependencies } from "../../../../internal.js"; +import { getDonationId } from "../../helpers/index.js"; + +type Dependencies = Pick< + ProcessorDependencies, + "projectRepository" | "roundRepository" | "pricingProvider" | "logger" +>; + +/** + * Handles the DirectAllocated event for the Direct Allocation strategy. + * + * This handler processes direct allocations of funds to a project by: + * - Validating that both the round and project exist + * - Retrieving token price data to calculate USD amounts + * - Creating a new donation record with the allocated amount + * + * Unlike other allocation handlers, this one does not require an application + * since funds are allocated directly to projects. + */ +export class DirectAllocatedHandler implements IEventHandler<"Strategy", "DirectAllocated"> { + constructor( + readonly event: ProcessorEvent<"Strategy", "DirectAllocated">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + /** + * Handles the DirectAllocated event for the Direct Allocation strategy. + * @returns {Changeset[]} The changeset containing an InsertDonation change + * @throws {ProjectNotFound} if the project does not exist + * @throws {RoundNotFound} if the round does not exist + * @throws {UnknownToken} if the token does not exist + * @throws {TokenPriceNotFoundError} if the token price is not found + */ + async handle(): Promise { + const { projectRepository, roundRepository, pricingProvider } = this.dependencies; + const strategyAddress = getAddress(this.event.srcAddress); + + const round = await roundRepository.getRoundByStrategyAddressOrThrow( + this.chainId, + strategyAddress, + ); + const project = await projectRepository.getProjectByIdOrThrow( + this.chainId, + this.event.params.profileId, + ); + + const donationId = getDonationId(this.event.blockNumber, this.event.logIndex); + + const amount = BigInt(this.event.params.amount); + const token = getTokenOrThrow(this.chainId, this.event.params.token); + const sender = getAddress(this.event.params.sender); + + const { amountInUsd, timestamp: priceTimestamp } = await getTokenAmountInUsd( + pricingProvider, + token, + amount, + this.event.blockTimestamp, + ); + + const donation: Donation = { + id: donationId, + chainId: this.chainId, + roundId: round.id, + applicationId: zeroAddress, + donorAddress: sender, + recipientAddress: getAddress(this.event.params.profileOwner), + projectId: project.id, + transactionHash: this.event.transactionFields.hash, + blockNumber: BigInt(this.event.blockNumber), + tokenAddress: token.address, + amount: amount, + amountInUsd, + amountInRoundMatchToken: 0n, + timestamp: new Date(priceTimestamp), + }; + + return [ + { + type: "InsertDonation", + args: { donation }, + }, + ]; + } +} diff --git a/packages/processors/src/processors/strategy/directAllocation/handlers/index.ts b/packages/processors/src/processors/strategy/directAllocation/handlers/index.ts new file mode 100644 index 0000000..bbb5218 --- /dev/null +++ b/packages/processors/src/processors/strategy/directAllocation/handlers/index.ts @@ -0,0 +1 @@ +export * from "./directAllocated.handler.js"; diff --git a/packages/processors/src/processors/strategy/directAllocation/index.ts b/packages/processors/src/processors/strategy/directAllocation/index.ts new file mode 100644 index 0000000..0671bb8 --- /dev/null +++ b/packages/processors/src/processors/strategy/directAllocation/index.ts @@ -0,0 +1,2 @@ +export * from "./handlers/index.js"; +export * from "./directAllocation.handler.js"; diff --git a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.ts b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.ts index eeba5e4..1d5c9f5 100644 --- a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.ts +++ b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.ts @@ -1,4 +1,4 @@ -import { encodePacked, getAddress, keccak256 } from "viem"; +import { getAddress } from "viem"; import { Changeset, Donation } from "@grants-stack-indexer/repository"; import { ChainId, getTokenOrThrow, ProcessorEvent } from "@grants-stack-indexer/shared"; @@ -10,6 +10,7 @@ import { ProcessorDependencies, } from "../../../../internal.js"; import { ApplicationMetadata, ApplicationMetadataSchema } from "../../../../schemas/index.js"; +import { getDonationId } from "../../helpers/index.js"; type Dependencies = Pick< ProcessorDependencies, @@ -59,7 +60,7 @@ export class DVMDAllocatedHandler implements IEventHandler<"Strategy", "Allocate getAddress(_recipientId), ); - const donationId = this.getDonationId(this.event.blockNumber, this.event.logIndex); + const donationId = getDonationId(this.event.blockNumber, this.event.logIndex); const token = getTokenOrThrow(this.chainId, _token); const matchToken = getTokenOrThrow(this.chainId, round.matchTokenAddress); @@ -110,13 +111,6 @@ export class DVMDAllocatedHandler implements IEventHandler<"Strategy", "Allocate ]; } - /** - * DONATION_ID = keccak256(abi.encodePacked(blockNumber, "-", logIndex)); - */ - private getDonationId(blockNumber: number, logIndex: number): string { - return keccak256(encodePacked(["string"], [`${blockNumber}-${logIndex}`])); - } - /** * Parses the application metadata. * @param {unknown} metadata - The metadata to parse. diff --git a/packages/processors/src/processors/strategy/helpers/allocated.ts b/packages/processors/src/processors/strategy/helpers/allocated.ts new file mode 100644 index 0000000..f394145 --- /dev/null +++ b/packages/processors/src/processors/strategy/helpers/allocated.ts @@ -0,0 +1,8 @@ +import { encodePacked, keccak256 } from "viem/utils"; + +/** + * DONATION_ID = keccak256(abi.encodePacked(blockNumber, "-", logIndex)); + */ +export const getDonationId = (blockNumber: number, logIndex: number): string => { + return keccak256(encodePacked(["string"], [`${blockNumber}-${logIndex}`])); +}; diff --git a/packages/processors/src/processors/strategy/helpers/index.ts b/packages/processors/src/processors/strategy/helpers/index.ts index 1f38b97..579fbd9 100644 --- a/packages/processors/src/processors/strategy/helpers/index.ts +++ b/packages/processors/src/processors/strategy/helpers/index.ts @@ -1,2 +1,3 @@ export * from "./decoder.js"; export * from "./applicationStatus.js"; +export * from "./allocated.js"; diff --git a/packages/processors/src/processors/strategy/index.ts b/packages/processors/src/processors/strategy/index.ts index a0fecc1..df97376 100644 --- a/packages/processors/src/processors/strategy/index.ts +++ b/packages/processors/src/processors/strategy/index.ts @@ -1,5 +1,6 @@ export * from "./common/index.js"; export * from "./donationVotingMerkleDistributionDirectTransfer/handlers/index.js"; +export * from "./directAllocation/index.js"; export * from "./strategyHandler.factory.js"; export * from "./strategy.processor.js"; // Export mapping separately to avoid circular dependencies diff --git a/packages/processors/src/processors/strategy/mapping.ts b/packages/processors/src/processors/strategy/mapping.ts index 98eb429..83c92e9 100644 --- a/packages/processors/src/processors/strategy/mapping.ts +++ b/packages/processors/src/processors/strategy/mapping.ts @@ -1,6 +1,7 @@ import { Hex } from "viem"; import type { StrategyHandlerConstructor } from "../../internal.js"; +import { DirectAllocationStrategyHandler } from "./directAllocation/index.js"; import { DVMDDirectTransferStrategyHandler } from "./donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; /** @@ -18,6 +19,8 @@ const strategyIdToHandler: Readonly> DVMDDirectTransferStrategyHandler, // DonationVotingMerkleDistributionDirectTransferStrategyv2.0 "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0": DVMDDirectTransferStrategyHandler, // DonationVotingMerkleDistributionDirectTransferStrategyv2.1 + "0x4cd0051913234cdd7d165b208851240d334786d6e5afbb4d0eec203515a9c6f3": + DirectAllocationStrategyHandler, } as const; /** diff --git a/packages/processors/test/strategy/directAllocation/directAllocation.handler.spec.ts b/packages/processors/test/strategy/directAllocation/directAllocation.handler.spec.ts new file mode 100644 index 0000000..02486b2 --- /dev/null +++ b/packages/processors/test/strategy/directAllocation/directAllocation.handler.spec.ts @@ -0,0 +1,94 @@ +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/internal.js"; +import { DirectAllocationStrategyHandler } from "../../../src/processors/strategy/directAllocation/directAllocation.handler.js"; +import { DirectAllocatedHandler } from "../../../src/processors/strategy/directAllocation/handlers/directAllocated.handler.js"; + +vi.mock( + "../../../src/processors/strategy/directAllocation/handlers/directAllocated.handler.js", + () => { + const DirectAllocatedHandler = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + DirectAllocatedHandler.prototype.handle = vi.fn(); + return { DirectAllocatedHandler }; + }, +); + +describe("DirectAllocationStrategyHandler", () => { + let handler: DirectAllocationStrategyHandler; + let mockMetadataProvider: IMetadataProvider; + let mockRoundRepository: IRoundReadRepository; + let mockProjectRepository: IProjectReadRepository; + let mockEVMProvider: EvmProvider; + let mockPricingProvider: IPricingProvider; + let mockApplicationRepository: IApplicationReadRepository; + let mockLogger: ILogger; + const chainId = 10 as ChainId; + + beforeEach(() => { + mockMetadataProvider = {} as IMetadataProvider; + mockRoundRepository = {} as IRoundReadRepository; + mockProjectRepository = {} as IProjectReadRepository; + mockEVMProvider = {} as unknown as EvmProvider; + mockPricingProvider = {} as IPricingProvider; + mockApplicationRepository = {} as IApplicationReadRepository; + mockLogger = {} as ILogger; + + handler = new DirectAllocationStrategyHandler(chainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, + applicationRepository: mockApplicationRepository, + logger: mockLogger, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("returns correct name", () => { + expect(handler.name).toBe("allov2.DirectAllocationStrategy"); + }); + + it("calls DirectAllocatedHandler for DirectAllocated event", async () => { + const mockEvent = { + eventName: "DirectAllocated", + } as ProcessorEvent<"Strategy", "DirectAllocated">; + + vi.spyOn(DirectAllocatedHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(DirectAllocatedHandler).toHaveBeenCalledWith(mockEvent, chainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, + applicationRepository: mockApplicationRepository, + logger: mockLogger, + }); + expect(DirectAllocatedHandler.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/directAllocation/handlers/directAllocated.handler.spec.ts b/packages/processors/test/strategy/directAllocation/handlers/directAllocated.handler.spec.ts new file mode 100644 index 0000000..24b38b2 --- /dev/null +++ b/packages/processors/test/strategy/directAllocation/handlers/directAllocated.handler.spec.ts @@ -0,0 +1,202 @@ +import { getAddress, parseEther, zeroAddress } from "viem"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { IPricingProvider } from "@grants-stack-indexer/pricing"; +import { + IProjectReadRepository, + IRoundReadRepository, + Project, + ProjectNotFound, + Round, + RoundNotFound, +} from "@grants-stack-indexer/repository"; +import { + Bytes32String, + ChainId, + DeepPartial, + ILogger, + mergeDeep, + ProcessorEvent, +} from "@grants-stack-indexer/shared"; + +import { TokenPriceNotFoundError } from "../../../../src/exceptions/index.js"; +import { getDonationId } from "../../../../src/processors/strategy/helpers/index.js"; +import { DirectAllocatedHandler } from "../../../../src/processors/strategy/index.js"; + +function createMockEvent( + overrides: DeepPartial> = {}, +): ProcessorEvent<"Strategy", "DirectAllocated"> { + const defaultEvent: ProcessorEvent<"Strategy", "DirectAllocated"> = { + params: { + profileId: "0x1234567890123456789012345678901234567890" as Bytes32String, + profileOwner: "0x1234567890123456789012345678901234567890", + amount: parseEther("10").toString(), + token: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + sender: "0x1234567890123456789012345678901234567890", + }, + eventName: "DirectAllocated", + srcAddress: "0x1234567890123456789012345678901234567890", + blockNumber: 118034410, + blockTimestamp: 1000000000, + chainId: 10 as ChainId, + contractName: "Strategy", + logIndex: 92, + transactionFields: { + hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", + transactionIndex: 6, + from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + }, + strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", + }; + + return mergeDeep(defaultEvent, overrides); +} + +describe("DirectAllocatedHandler", () => { + let handler: DirectAllocatedHandler; + let mockRoundRepository: IRoundReadRepository; + let mockProjectRepository: IProjectReadRepository; + let mockPricingProvider: IPricingProvider; + let mockEvent: ProcessorEvent<"Strategy", "DirectAllocated">; + let mockLogger: ILogger; + const chainId = 10 as ChainId; + + beforeEach(() => { + mockRoundRepository = { + getRoundByStrategyAddressOrThrow: vi.fn(), + } as unknown as IRoundReadRepository; + mockPricingProvider = { + getTokenPrice: vi.fn(), + } as IPricingProvider; + mockProjectRepository = { + getProjectByIdOrThrow: vi.fn(), + } as unknown as IProjectReadRepository; + mockLogger = { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + } as unknown as ILogger; + }); + + it("handles a valid direct allocation event", async () => { + const amount = parseEther("10").toString(); + mockEvent = createMockEvent({ params: { amount } }); + const mockRound = { + id: "round1", + } as unknown as Round; + + const mockProject = { + id: mockEvent.params.profileId, + } as unknown as Project; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn(mockProjectRepository, "getProjectByIdOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue({ + timestampMs: mockEvent.blockTimestamp, + priceUsd: 2000, + }); + + handler = new DirectAllocatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + pricingProvider: mockPricingProvider, + logger: mockLogger, + }); + + const donationId = getDonationId(mockEvent.blockNumber, mockEvent.logIndex); + + const result = await handler.handle(); + + expect(result).toEqual([ + { + type: "InsertDonation", + args: { + donation: { + id: donationId, + chainId, + roundId: mockRound.id, + applicationId: zeroAddress, + projectId: mockEvent.params.profileId, + donorAddress: getAddress(mockEvent.params.sender), + recipientAddress: getAddress(mockEvent.params.profileOwner), + transactionHash: mockEvent.transactionFields.hash, + blockNumber: BigInt(mockEvent.blockNumber), + tokenAddress: getAddress(mockEvent.params.token), + amount: BigInt(amount), + amountInUsd: "20000", + amountInRoundMatchToken: 0n, + timestamp: new Date(mockEvent.blockTimestamp), + }, + }, + }, + ]); + }); + + it("throws RoundNotFound if round is not found", async () => { + mockEvent = createMockEvent(); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( + new RoundNotFound(chainId, mockEvent.srcAddress), + ); + + handler = new DirectAllocatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + pricingProvider: mockPricingProvider, + logger: mockLogger, + }); + + await expect(handler.handle()).rejects.toThrow(RoundNotFound); + }); + + it("throws ProjectNotFound if project is not found", async () => { + mockEvent = createMockEvent(); + const mockRound = { + id: mockEvent.params.profileId, + matchTokenAddress: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + } as unknown as Round; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn(mockProjectRepository, "getProjectByIdOrThrow").mockRejectedValue( + new ProjectNotFound(chainId, mockEvent.params.profileId), + ); + + handler = new DirectAllocatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + pricingProvider: mockPricingProvider, + logger: mockLogger, + }); + + await expect(handler.handle()).rejects.toThrow(ProjectNotFound); + }); + + it("throws TokenPriceNotFoundError if token price is not found", async () => { + mockEvent = createMockEvent(); + const mockRound = { + id: "round1", + } as unknown as Round; + const mockProject = { + id: "project1", + } as Project; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn(mockProjectRepository, "getProjectByIdOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue(undefined); + + handler = new DirectAllocatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + pricingProvider: mockPricingProvider, + logger: mockLogger, + }); + + await expect(handler.handle()).rejects.toThrow(TokenPriceNotFoundError); + }); +}); diff --git a/packages/repository/src/interfaces/projectRepository.interface.ts b/packages/repository/src/interfaces/projectRepository.interface.ts index a3dce7d..4e70c98 100644 --- a/packages/repository/src/interfaces/projectRepository.interface.ts +++ b/packages/repository/src/interfaces/projectRepository.interface.ts @@ -26,6 +26,15 @@ export interface IProjectReadRepository { */ getProjectById(chainId: ChainId, projectId: string): Promise; + /** + * Retrieves a specific project by its ID and chain ID. + * @param chainId The chain ID of the project. + * @param projectId The unique identifier of the project. + * @returns A promise that resolves to a Project object. + * @throws {ProjectNotFound} if the project does not exist + */ + getProjectByIdOrThrow(chainId: ChainId, projectId: string): Promise; + /** * Retrieves all pending project roles. * @returns A promise that resolves to an array of PendingProjectRole objects. diff --git a/packages/repository/src/repositories/kysely/project.repository.ts b/packages/repository/src/repositories/kysely/project.repository.ts index 70e48d0..abba90f 100644 --- a/packages/repository/src/repositories/kysely/project.repository.ts +++ b/packages/repository/src/repositories/kysely/project.repository.ts @@ -44,6 +44,13 @@ export class KyselyProjectRepository implements IProjectRepository { .executeTakeFirst(); } + /* @inheritdoc */ + async getProjectByIdOrThrow(chainId: ChainId, projectId: string): Promise { + const project = await this.getProjectById(chainId, projectId); + if (!project) throw new ProjectNotFound(chainId, projectId); + return project; + } + /* @inheritdoc */ async getProjectByAnchor( chainId: ChainId, diff --git a/packages/shared/src/types/events/strategy.ts b/packages/shared/src/types/events/strategy.ts index a752339..d0c98bc 100644 --- a/packages/shared/src/types/events/strategy.ts +++ b/packages/shared/src/types/events/strategy.ts @@ -26,6 +26,7 @@ const StrategyEventArray = [ "UpdatedRegistrationWithStatus", "UpdatedRegistration", "UpdatedRegistrationWithApplicationId", + "DirectAllocated", ] as const; /** @@ -72,7 +73,9 @@ export type StrategyEventParams = T extends "Registered ? UpdatedRegistrationParams : T extends "UpdatedRegistrationWithApplicationId" ? UpdatedRegistrationWithApplicationIdParams - : never; + : T extends "DirectAllocated" + ? DirectAllocatedParams + : never; // ============================================================================= // =============================== Event Parameters ============================ @@ -208,6 +211,15 @@ export type UpdatedRegistrationWithApplicationIdParams = { status: string; //uint8 }; +// ======================= DirectAllocated ======================= +export type DirectAllocatedParams = { + profileId: Bytes32String; + profileOwner: Address; + amount: string; //uint256 + token: Address; + sender: Address; +}; + /** * Type guard for Strategy events. * @param event The event to check. From e4a2f636ece3c914ff68e82586e528da45527a73 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:10:34 -0300 Subject: [PATCH 2/2] feat: add strategyName to UnsupportedEventException --- .../processors/src/exceptions/unsupportedEvent.exception.ts | 5 ++++- .../strategy/directAllocation/directAllocation.handler.ts | 2 +- .../dvmdDirectTransfer.handler.ts | 2 +- .../directAllocation/directAllocation.handler.spec.ts | 4 +++- .../dvmdDirectTransfer.handler.spec.ts | 4 +++- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/processors/src/exceptions/unsupportedEvent.exception.ts b/packages/processors/src/exceptions/unsupportedEvent.exception.ts index 4760f12..07533f8 100644 --- a/packages/processors/src/exceptions/unsupportedEvent.exception.ts +++ b/packages/processors/src/exceptions/unsupportedEvent.exception.ts @@ -4,7 +4,10 @@ export class UnsupportedEventException extends Error { constructor( contract: ContractName, public readonly eventName: string, + strategyName?: string, ) { - super(`Event ${eventName} unsupported for ${contract} processor`); + super( + `Event ${eventName} unsupported for ${contract} processor${strategyName ? `, strategy ${strategyName}` : ""}`, + ); } } diff --git a/packages/processors/src/processors/strategy/directAllocation/directAllocation.handler.ts b/packages/processors/src/processors/strategy/directAllocation/directAllocation.handler.ts index 4b8396d..1c48c2b 100644 --- a/packages/processors/src/processors/strategy/directAllocation/directAllocation.handler.ts +++ b/packages/processors/src/processors/strategy/directAllocation/directAllocation.handler.ts @@ -32,7 +32,7 @@ export class DirectAllocationStrategyHandler extends BaseStrategyHandler { this.dependencies, ).handle(); default: - throw new UnsupportedEventException("Strategy", event.eventName); + throw new UnsupportedEventException("Strategy", event.eventName, this.name); } } } diff --git a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts index d480ead..c998e26 100644 --- a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts +++ b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts @@ -112,7 +112,7 @@ export class DVMDDirectTransferStrategyHandler extends BaseStrategyHandler { this.dependencies, ).handle(); default: - throw new UnsupportedEventException("Strategy", event.eventName); + throw new UnsupportedEventException("Strategy", event.eventName, this.name); } } diff --git a/packages/processors/test/strategy/directAllocation/directAllocation.handler.spec.ts b/packages/processors/test/strategy/directAllocation/directAllocation.handler.spec.ts index 02486b2..193c8b8 100644 --- a/packages/processors/test/strategy/directAllocation/directAllocation.handler.spec.ts +++ b/packages/processors/test/strategy/directAllocation/directAllocation.handler.spec.ts @@ -89,6 +89,8 @@ describe("DirectAllocationStrategyHandler", () => { eventName: "UnknownEvent", } as unknown as ProcessorEvent<"Strategy", StrategyEvent>; - await expect(handler.handle(mockEvent)).rejects.toThrow(UnsupportedEventException); + await expect(handler.handle(mockEvent)).rejects.toThrow( + new UnsupportedEventException("Strategy", "UnknownEvent", handler.name), + ); }); }); diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts index ded3141..171de57 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts @@ -407,6 +407,8 @@ describe("DVMDDirectTransferHandler", () => { "Strategy", StrategyEvent >; - await expect(() => handler.handle(mockEvent)).rejects.toThrow(UnsupportedEventException); + await expect(() => handler.handle(mockEvent)).rejects.toThrow( + new UnsupportedEventException("Strategy", "UnknownEvent", handler.name), + ); }); });