diff --git a/apps/processing/src/services/sharedDependencies.service.ts b/apps/processing/src/services/sharedDependencies.service.ts index 9b41999..80e4bc4 100644 --- a/apps/processing/src/services/sharedDependencies.service.ts +++ b/apps/processing/src/services/sharedDependencies.service.ts @@ -8,6 +8,7 @@ import { IpfsProvider } from "@grants-stack-indexer/metadata"; import { PricingProviderFactory } from "@grants-stack-indexer/pricing"; import { createKyselyDatabase, + KyselyApplicationPayoutRepository, KyselyApplicationRepository, KyselyDonationRepository, KyselyProjectRepository, @@ -50,6 +51,10 @@ export class SharedDependenciesService { kyselyDatabase, env.DATABASE_SCHEMA, ); + const applicationPayoutRepository = new KyselyApplicationPayoutRepository( + kyselyDatabase, + env.DATABASE_SCHEMA, + ); const pricingProvider = PricingProviderFactory.create(env, { logger }); const metadataProvider = new IpfsProvider(env.IPFS_GATEWAYS_URL, logger); @@ -72,6 +77,7 @@ export class SharedDependenciesService { pricingProvider, donationRepository, metadataProvider, + applicationPayoutRepository, }, registries: { eventsRegistry, diff --git a/packages/data-flow/src/data-loader/dataLoader.ts b/packages/data-flow/src/data-loader/dataLoader.ts index 9aefdef..2f07d1a 100644 --- a/packages/data-flow/src/data-loader/dataLoader.ts +++ b/packages/data-flow/src/data-loader/dataLoader.ts @@ -1,5 +1,6 @@ import { Changeset, + IApplicationPayoutRepository, IApplicationRepository, IDonationRepository, IProjectRepository, @@ -10,6 +11,7 @@ import { ILogger, stringify } from "@grants-stack-indexer/shared"; import { ExecutionResult, IDataLoader, InvalidChangeset } from "../internal.js"; import { createApplicationHandlers, + createApplicationPayoutHandlers, createDonationHandlers, createProjectHandlers, createRoundHandlers, @@ -38,6 +40,7 @@ export class DataLoader implements IDataLoader { round: IRoundRepository; application: IApplicationRepository; donation: IDonationRepository; + applicationPayout: IApplicationPayoutRepository; }, private readonly logger: ILogger, ) { @@ -46,6 +49,7 @@ export class DataLoader implements IDataLoader { ...createRoundHandlers(repositories.round), ...createApplicationHandlers(repositories.application), ...createDonationHandlers(repositories.donation), + ...createApplicationPayoutHandlers(repositories.applicationPayout), }; } diff --git a/packages/data-flow/src/data-loader/handlers/applicationPayout.handlers.ts b/packages/data-flow/src/data-loader/handlers/applicationPayout.handlers.ts new file mode 100644 index 0000000..f14fc24 --- /dev/null +++ b/packages/data-flow/src/data-loader/handlers/applicationPayout.handlers.ts @@ -0,0 +1,28 @@ +import { + ApplicationPayoutChangeset, + IApplicationPayoutRepository, +} from "@grants-stack-indexer/repository"; + +import { ChangesetHandler } from "../types/index.js"; + +/** + * Collection of handlers for application-related operations. + * Each handler corresponds to a specific Application changeset type. + */ +export type ApplicationPayoutHandlers = { + [K in ApplicationPayoutChangeset["type"]]: ChangesetHandler; +}; + +/** + * Creates handlers for managing application-related operations. + * + * @param repository - The application repository instance used for database operations + * @returns An object containing all application-related handlers + */ +export const createApplicationPayoutHandlers = ( + repository: IApplicationPayoutRepository, +): ApplicationPayoutHandlers => ({ + InsertApplicationPayout: (async (changeset): Promise => { + await repository.insertApplicationPayout(changeset.args.applicationPayout); + }) satisfies ChangesetHandler<"InsertApplicationPayout">, +}); diff --git a/packages/data-flow/src/data-loader/handlers/index.ts b/packages/data-flow/src/data-loader/handlers/index.ts index dc27b0b..6efbd54 100644 --- a/packages/data-flow/src/data-loader/handlers/index.ts +++ b/packages/data-flow/src/data-loader/handlers/index.ts @@ -2,3 +2,4 @@ export * from "./application.handlers.js"; export * from "./project.handlers.js"; export * from "./round.handlers.js"; export * from "./donation.handlers.js"; +export * from "./applicationPayout.handlers.js"; diff --git a/packages/data-flow/src/orchestrator.ts b/packages/data-flow/src/orchestrator.ts index 52fbd2d..52d58dc 100644 --- a/packages/data-flow/src/orchestrator.ts +++ b/packages/data-flow/src/orchestrator.ts @@ -91,6 +91,7 @@ export class Orchestrator { round: this.dependencies.roundRepository, application: this.dependencies.applicationRepository, donation: this.dependencies.donationRepository, + applicationPayout: this.dependencies.applicationPayoutRepository, }, this.logger, ); diff --git a/packages/data-flow/src/types/index.ts b/packages/data-flow/src/types/index.ts index 7778122..a83c8d8 100644 --- a/packages/data-flow/src/types/index.ts +++ b/packages/data-flow/src/types/index.ts @@ -1,6 +1,7 @@ import { ProcessorDependencies } from "@grants-stack-indexer/processors"; import { Changeset, + IApplicationPayoutRepository, IApplicationRepository, IDonationRepository, IProjectRepository, @@ -33,4 +34,5 @@ export type CoreDependencies = Pick< projectRepository: IProjectRepository; applicationRepository: IApplicationRepository; donationRepository: IDonationRepository; + applicationPayoutRepository: IApplicationPayoutRepository; }; diff --git a/packages/data-flow/test/data-loader/dataLoader.spec.ts b/packages/data-flow/test/data-loader/dataLoader.spec.ts index 3eef1fc..167284b 100644 --- a/packages/data-flow/test/data-loader/dataLoader.spec.ts +++ b/packages/data-flow/test/data-loader/dataLoader.spec.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { Changeset, + IApplicationPayoutRepository, IApplicationRepository, IDonationRepository, IProjectRepository, @@ -33,6 +34,10 @@ describe("DataLoader", () => { insertManyDonations: vi.fn(), } as IDonationRepository; + const mockApplicationPayoutRepository = { + insertApplicationPayout: vi.fn(), + } as IApplicationPayoutRepository; + const logger: ILogger = { debug: vi.fn(), error: vi.fn(), @@ -46,6 +51,7 @@ describe("DataLoader", () => { round: mockRoundRepository, application: mockApplicationRepository, donation: mockDonationRepository, + applicationPayout: mockApplicationPayoutRepository, }, logger, ); diff --git a/packages/data-flow/test/unit/orchestrator.spec.ts b/packages/data-flow/test/unit/orchestrator.spec.ts index ffc8ce7..0ad1bb3 100644 --- a/packages/data-flow/test/unit/orchestrator.spec.ts +++ b/packages/data-flow/test/unit/orchestrator.spec.ts @@ -6,6 +6,7 @@ import { IIndexerClient } from "@grants-stack-indexer/indexer-client"; import { UnsupportedStrategy } from "@grants-stack-indexer/processors"; import { Changeset, + IApplicationPayoutRepository, IApplicationRepository, IDonationRepository, IProjectRepository, @@ -94,6 +95,7 @@ describe("Orchestrator", { sequential: true }, () => { roundRepository: {} as unknown as IRoundRepository, applicationRepository: {} as unknown as IApplicationRepository, donationRepository: {} as unknown as IDonationRepository, + applicationPayoutRepository: {} as unknown as IApplicationPayoutRepository, pricingProvider: { getTokenPrice: vi.fn(), }, diff --git a/packages/processors/src/processors/strategy/directGrantsLite/directGrantsLite.handler.ts b/packages/processors/src/processors/strategy/directGrantsLite/directGrantsLite.handler.ts new file mode 100644 index 0000000..1ed788b --- /dev/null +++ b/packages/processors/src/processors/strategy/directGrantsLite/directGrantsLite.handler.ts @@ -0,0 +1,118 @@ +import { Changeset } from "@grants-stack-indexer/repository"; +import { Address, ChainId, ProcessorEvent, StrategyEvent } from "@grants-stack-indexer/shared"; + +import DirectGrantsLiteStrategy from "../../../abis/allo-v2/v1/DirectGrantsLiteStrategy.js"; +import { getDateFromTimestamp } from "../../../helpers/index.js"; +import { + BaseRecipientStatusUpdatedHandler, + ProcessorDependencies, + StrategyTimings, + UnsupportedEventException, +} from "../../../internal.js"; +import { BaseStrategyHandler } from "../index.js"; +import { + DGLiteAllocatedHandler, + DGLiteRegisteredHandler, + DGLiteTimestampsUpdatedHandler, + DGLiteUpdatedRegistrationHandler, +} from "./handlers/index.js"; + +const STRATEGY_NAME = "allov2.DirectGrantsLiteStrategy"; + +/** + * This handler is responsible for processing events related to the + * Direct Grants Lite strategy. + * + * The following events are currently handled by this strategy: + * - Registered + * - UpdatedRegistrationWithStatus + * - TimestampsUpdated + * - AllocatedWithToken + * - RecipientStatusUpdatedWithFullRow + */ +export class DirectGrantsLiteStrategyHandler 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 "RecipientStatusUpdatedWithFullRow": + return new BaseRecipientStatusUpdatedHandler( + event as ProcessorEvent<"Strategy", "RecipientStatusUpdatedWithFullRow">, + this.chainId, + this.dependencies, + ).handle(); + case "RegisteredWithSender": + return new DGLiteRegisteredHandler( + event as ProcessorEvent<"Strategy", "RegisteredWithSender">, + this.chainId, + this.dependencies, + ).handle(); + case "UpdatedRegistrationWithStatus": + return new DGLiteUpdatedRegistrationHandler( + event as ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus">, + this.chainId, + this.dependencies, + ).handle(); + case "TimestampsUpdated": + return new DGLiteTimestampsUpdatedHandler( + event as ProcessorEvent<"Strategy", "TimestampsUpdated">, + this.chainId, + this.dependencies, + ).handle(); + case "AllocatedWithToken": + return new DGLiteAllocatedHandler( + event as ProcessorEvent<"Strategy", "AllocatedWithToken">, + this.chainId, + this.dependencies, + ).handle(); + default: + throw new UnsupportedEventException("Strategy", event.eventName, this.name); + } + } + + /** @inheritdoc */ + override async fetchStrategyTimings(strategyId: Address): Promise { + const { evmProvider } = this.dependencies; + let results: [bigint, bigint] = [0n, 0n]; + + const contractCalls = [ + { + abi: DirectGrantsLiteStrategy, + functionName: "registrationStartTime", + address: strategyId, + }, + { + abi: DirectGrantsLiteStrategy, + functionName: "registrationEndTime", + address: strategyId, + }, + ] as const; + + // TODO: refactor when evmProvider implements this natively + if (evmProvider.getMulticall3Address()) { + results = await evmProvider.multicall({ + contracts: contractCalls, + allowFailure: false, + }); + } else { + results = (await Promise.all( + contractCalls.map((call) => + evmProvider.readContract(call.address, call.abi, call.functionName), + ), + )) as [bigint, bigint]; + } + + return { + applicationsStartTime: getDateFromTimestamp(results[0]), + applicationsEndTime: getDateFromTimestamp(results[1]), + donationsStartTime: null, + donationsEndTime: null, + }; + } +} diff --git a/packages/processors/src/processors/strategy/directGrantsLite/handlers/allocated.handler.ts b/packages/processors/src/processors/strategy/directGrantsLite/handlers/allocated.handler.ts new file mode 100644 index 0000000..f035cbb --- /dev/null +++ b/packages/processors/src/processors/strategy/directGrantsLite/handlers/allocated.handler.ts @@ -0,0 +1,109 @@ +import { getAddress } from "viem"; + +import { Changeset } from "@grants-stack-indexer/repository"; +import { ChainId, getTokenOrThrow, ProcessorEvent } from "@grants-stack-indexer/shared"; + +import { getTokenAmountInUsd, getUsdInTokenAmount } from "../../../../helpers/index.js"; +import { IEventHandler, ProcessorDependencies } from "../../../../internal.js"; + +type Dependencies = Pick< + ProcessorDependencies, + "roundRepository" | "applicationRepository" | "pricingProvider" +>; + +/** + * Handler for processing AllocatedWithToken events from the DirectGrantsLite strategy. + * + * When a round operator allocates funds to a recipient, this handler: + * 1. Retrieves the round and application based on the strategy address and recipient + * 2. Converts the allocated token amount to USD value + * 3. Calculates the equivalent amount in the round's match token + * 4. Updates the application with the allocation details + */ + +export class DGLiteAllocatedHandler implements IEventHandler<"Strategy", "AllocatedWithToken"> { + constructor( + readonly event: ProcessorEvent<"Strategy", "AllocatedWithToken">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + /** + * Handles the AllocatedWithToken event for the Direct Grants Lite strategy. + * @returns The changeset with an InsertApplicationPayout operation. + * @throws RoundNotFound if the round is not found. + * @throws ApplicationNotFound if the application is not found. + * @throws TokenNotFound if the token is not found. + * @throws TokenPriceNotFound if the token price is not found. + */ + async handle(): Promise { + const { roundRepository, applicationRepository } = this.dependencies; + const { srcAddress } = this.event; + const { recipientId: _recipientId, amount: strAmount, token: _token } = this.event.params; + + const amount = BigInt(strAmount); + + const round = await roundRepository.getRoundByStrategyAddressOrThrow( + this.chainId, + getAddress(srcAddress), + ); + + const recipientId = getAddress(_recipientId); + const tokenAddress = getAddress(_token); + const application = await applicationRepository.getApplicationByAnchorAddressOrThrow( + this.chainId, + round.id, + recipientId, + ); + + const token = getTokenOrThrow(this.chainId, tokenAddress); + const matchToken = getTokenOrThrow(this.chainId, round.matchTokenAddress); + + let amountInUsd = "0"; + let amountInRoundMatchToken = 0n; + + if (amount > 0) { + const { amountInUsd: amountInUsdString } = await getTokenAmountInUsd( + this.dependencies.pricingProvider, + token, + amount, + this.event.blockTimestamp, + ); + amountInUsd = amountInUsdString; + + amountInRoundMatchToken = + matchToken.address === token.address + ? amount + : ( + await getUsdInTokenAmount( + this.dependencies.pricingProvider, + matchToken, + amountInUsd, + this.event.blockTimestamp, + ) + ).amount; + } + + const timestamp = this.event.blockTimestamp; + + return [ + { + type: "InsertApplicationPayout", + args: { + applicationPayout: { + amount, + applicationId: application.id, + roundId: round.id, + chainId: this.chainId, + tokenAddress, + amountInRoundMatchToken, + amountInUsd, + transactionHash: this.event.transactionFields.hash, + sender: getAddress(this.event.params.sender), + timestamp: new Date(timestamp), + }, + }, + }, + ]; + } +} diff --git a/packages/processors/src/processors/strategy/directGrantsLite/handlers/index.ts b/packages/processors/src/processors/strategy/directGrantsLite/handlers/index.ts new file mode 100644 index 0000000..b75c01c --- /dev/null +++ b/packages/processors/src/processors/strategy/directGrantsLite/handlers/index.ts @@ -0,0 +1,4 @@ +export * from "./registered.handler.js"; +export * from "./updatedRegistration.handler.js"; +export * from "./timestampsUpdated.handler.js"; +export * from "./allocated.handler.js"; diff --git a/packages/processors/src/processors/strategy/directGrantsLite/handlers/registered.handler.ts b/packages/processors/src/processors/strategy/directGrantsLite/handlers/registered.handler.ts new file mode 100644 index 0000000..1f05427 --- /dev/null +++ b/packages/processors/src/processors/strategy/directGrantsLite/handlers/registered.handler.ts @@ -0,0 +1,94 @@ +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 { decodeDVMDExtendedApplicationData } from "../../helpers/index.js"; + +type Dependencies = Pick< + ProcessorDependencies, + "roundRepository" | "projectRepository" | "metadataProvider" +>; + +/** + * Handles the Registered event for the Direct Grants Lite 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 DGLiteRegisteredHandler 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 Lite 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 = decodeDVMDExtendedApplicationData(encodedData); + // ID is defined as recipientsCounter - 1, which is a value emitted by the strategy + const id = (Number(values.recipientsCounter) - 1).toString(); + + const metadata = await metadataProvider.getMetadata(values.metadata.pointer); + + const application: NewApplication = { + chainId: this.chainId, + id: id, + projectId: project.id, + anchorAddress, + roundId: round.id, + status: "PENDING", + metadataCid: values.metadata.pointer, + metadata: metadata ?? null, + createdAtBlock: BigInt(blockNumber), + createdByAddress: getAddress(sender), + statusUpdatedAtBlock: BigInt(blockNumber), + statusSnapshots: [ + { + status: "PENDING", + updatedAtBlock: blockNumber.toString(), + updatedAt: new Date(blockTimestamp * 1000), // timestamp is in seconds, convert to ms + }, + ], + distributionTransaction: null, + totalAmountDonatedInUsd: 0, + totalDonationsCount: 0, + uniqueDonorsCount: 0, + tags: ["allo-v2"], + }; + + return [ + { + type: "InsertApplication", + args: application, + }, + ]; + } +} diff --git a/packages/processors/src/processors/strategy/directGrantsLite/handlers/timestampsUpdated.handler.ts b/packages/processors/src/processors/strategy/directGrantsLite/handlers/timestampsUpdated.handler.ts new file mode 100644 index 0000000..0e69546 --- /dev/null +++ b/packages/processors/src/processors/strategy/directGrantsLite/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 Lite 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 DGLiteTimestampsUpdatedHandler + 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 Lite 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/directGrantsLite/handlers/updatedRegistration.handler.ts b/packages/processors/src/processors/strategy/directGrantsLite/handlers/updatedRegistration.handler.ts new file mode 100644 index 0000000..044af71 --- /dev/null +++ b/packages/processors/src/processors/strategy/directGrantsLite/handlers/updatedRegistration.handler.ts @@ -0,0 +1,112 @@ +import { getAddress } from "viem"; + +import { Application, Changeset } from "@grants-stack-indexer/repository"; +import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; + +import { ApplicationStatus, IEventHandler, ProcessorDependencies } from "../../../../internal.js"; +import { + createStatusUpdate, + decodeDVMDApplicationData, + isValidApplicationStatus, +} from "../../helpers/index.js"; + +type Dependencies = Pick< + ProcessorDependencies, + | "logger" + | "roundRepository" + | "applicationRepository" + | "projectRepository" + | "metadataProvider" +>; + +/** + * Handles the UpdatedRegistration event for the Direct Grants Lite strategy. + * + * This handler processes updates to project registrations/applications in a round: + * - Validates the updated application status is valid (between 1-3) + * - Decodes the updated application metadata and data + * - Returns a changeset to update the application record + */ + +export class DGLiteUpdatedRegistrationHandler + implements IEventHandler<"Strategy", "UpdatedRegistrationWithStatus"> +{ + constructor( + readonly event: ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + /** + * Handles the UpdatedRegistrationWithStatus event for the Direct Grants Lite strategy. + * @returns The changeset with an UpdateApplication operation. + * @throws ProjectNotFound if the project is not found. + * @throws RoundNotFound if the round is not found. + * @throws ApplicationNotFound if the application is not found. + */ + async handle(): Promise { + const { + metadataProvider, + logger, + roundRepository, + applicationRepository, + projectRepository, + } = this.dependencies; + + const { status: strStatus } = this.event.params; + const status = Number(strStatus); + + if (!isValidApplicationStatus(status)) { + logger.warn( + `[DGLiteUpdatedRegistrationHandler] Invalid status: ${this.event.params.status}`, + ); + + return []; + } + + const project = await projectRepository.getProjectByAnchorOrThrow( + this.chainId, + getAddress(this.event.params.recipientId), + ); + const round = await roundRepository.getRoundByStrategyAddressOrThrow( + this.chainId, + getAddress(this.event.srcAddress), + ); + const application = await applicationRepository.getApplicationByAnchorAddressOrThrow( + this.chainId, + round.id, + project.anchorAddress!, + ); + + const encodedData = this.event.params.data; + const values = decodeDVMDApplicationData(encodedData); + + const metadata = await metadataProvider.getMetadata(values.metadata.pointer); + + const statusString = ApplicationStatus[status] as Application["status"]; + + const statusUpdates = createStatusUpdate({ + application, + newStatus: statusString, + blockNumber: this.event.blockNumber, + blockTimestamp: this.event.blockTimestamp, + }); + + return [ + { + type: "UpdateApplication", + args: { + chainId: this.chainId, + roundId: round.id, + applicationId: application.id, + application: { + ...application, + ...statusUpdates, + metadataCid: values.metadata.pointer, + metadata: metadata ?? null, + }, + }, + }, + ]; + } +} diff --git a/packages/processors/src/processors/strategy/directGrantsLite/index.ts b/packages/processors/src/processors/strategy/directGrantsLite/index.ts new file mode 100644 index 0000000..48ad29a --- /dev/null +++ b/packages/processors/src/processors/strategy/directGrantsLite/index.ts @@ -0,0 +1,2 @@ +export * from "./handlers/index.js"; +export * from "./directGrantsLite.handler.js"; diff --git a/packages/processors/src/processors/strategy/helpers/decoder.ts b/packages/processors/src/processors/strategy/helpers/decoder.ts index 6438187..8785f52 100644 --- a/packages/processors/src/processors/strategy/helpers/decoder.ts +++ b/packages/processors/src/processors/strategy/helpers/decoder.ts @@ -23,6 +23,30 @@ const DVMD_DATA_DECODER = [ }, ] as const; +const DG_DATA_DECODER = [ + { name: "recipientId", type: "address" }, + { name: "registryAnchor", type: "address" }, + { name: "grantAmount", type: "uint256" }, + { + name: "metadata", + type: "tuple", + components: [ + { name: "protocol", type: "uint256" }, + { name: "pointer", type: "string" }, + ], + }, +] as const; + +export type DGApplicationData = { + recipientAddress: string; + anchorAddress: string; + grantAmount: bigint; + metadata: { + protocol: number; + pointer: string; + }; +}; + export const decodeDVMDApplicationData = (encodedData: Hex): DVMDApplicationData => { const decodedData = decodeAbiParameters(DVMD_DATA_DECODER, encodedData); @@ -43,10 +67,26 @@ export const decodeDVMDExtendedApplicationData = ( ): DVMDExtendedApplicationData => { const values = decodeAbiParameters(DVMD_EVENT_DATA_DECODER, encodedData); - const encodededDVMD = decodeDVMDApplicationData(values[0]); + const decodedDVMD = decodeDVMDApplicationData(values[0]); return { - ...encodededDVMD, + ...decodedDVMD, recipientsCounter: values[1].toString(), }; }; + +export const decodeDGApplicationData = (encodedData: Hex): DGApplicationData => { + const decodedData = decodeAbiParameters(DG_DATA_DECODER, encodedData); + + const results: DGApplicationData = { + recipientAddress: decodedData[0], + anchorAddress: decodedData[1], + grantAmount: decodedData[2], + metadata: { + protocol: Number(decodedData[3].protocol), + pointer: decodedData[3].pointer, + }, + }; + + return results; +}; diff --git a/packages/processors/src/processors/strategy/mapping.ts b/packages/processors/src/processors/strategy/mapping.ts index 83c92e9..29d9b6b 100644 --- a/packages/processors/src/processors/strategy/mapping.ts +++ b/packages/processors/src/processors/strategy/mapping.ts @@ -2,6 +2,7 @@ import { Hex } from "viem"; import type { StrategyHandlerConstructor } from "../../internal.js"; import { DirectAllocationStrategyHandler } from "./directAllocation/index.js"; +import { DirectGrantsLiteStrategyHandler } from "./directGrantsLite/index.js"; import { DVMDDirectTransferStrategyHandler } from "./donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; /** @@ -21,6 +22,8 @@ const strategyIdToHandler: Readonly> DVMDDirectTransferStrategyHandler, // DonationVotingMerkleDistributionDirectTransferStrategyv2.1 "0x4cd0051913234cdd7d165b208851240d334786d6e5afbb4d0eec203515a9c6f3": DirectAllocationStrategyHandler, + "0x103732a8e473467a510d4128ee11065262bdd978f0d9dad89ba68f2c56127e27": + DirectGrantsLiteStrategyHandler, } as const; /** diff --git a/packages/processors/test/mocks/event.mock.ts b/packages/processors/test/mocks/event.mock.ts new file mode 100644 index 0000000..205afa7 --- /dev/null +++ b/packages/processors/test/mocks/event.mock.ts @@ -0,0 +1,57 @@ +import { + ChainId, + ContractToEventName, + DeepPartial, + EventParams, + Hex, + mergeDeep, + ProcessorEvent, +} from "@grants-stack-indexer/shared"; + +/** + * Creates a mock event for testing. + * + * @param eventName - The name of the event. + * @param params - The parameters of the event. + * @param strategyId - The ID of the strategy. + * @param overrides - The overrides for the event. + * @returns A mock event. + * + * @default + * srcAddress: "0x1234567890123456789012345678901234567890", + * blockNumber: 118034410, + * blockTimestamp: 1000000000, + * chainId: 10 as ChainId, + * contractName: "Strategy", + * logIndex: 1, + * transactionFields: { + * hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", + * transactionIndex: 1, + * from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + * }, + */ +export const createMockEvent = >( + eventName: T, + params: EventParams<"Strategy", T>, + strategyId: Hex, + overrides: DeepPartial> = {}, +): ProcessorEvent<"Strategy", T> => { + const defaultEvent: ProcessorEvent<"Strategy", T> = { + eventName, + params, + srcAddress: "0x1234567890123456789012345678901234567890", + blockNumber: 118034410, + blockTimestamp: 1000000000, + chainId: 10 as ChainId, + contractName: "Strategy", + logIndex: 1, + transactionFields: { + hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", + transactionIndex: 1, + from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + }, + strategyId, + }; + + return mergeDeep(defaultEvent, overrides); +}; diff --git a/packages/processors/test/mocks/index.ts b/packages/processors/test/mocks/index.ts new file mode 100644 index 0000000..743477a --- /dev/null +++ b/packages/processors/test/mocks/index.ts @@ -0,0 +1 @@ +export * from "./event.mock.js"; diff --git a/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts b/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts index 53c9410..fafa9db 100644 --- a/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts +++ b/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts @@ -4,46 +4,29 @@ import { IRoundReadRepository, Round } from "@grants-stack-indexer/repository"; import { ChainId, ILogger, ProcessorEvent } from "@grants-stack-indexer/shared"; import { BaseDistributedHandler } from "../../../src/processors/strategy/common/baseDistributed.handler.js"; - -function createMockEvent( - overrides: Partial> = {}, -): ProcessorEvent<"Strategy", "DistributedWithRecipientAddress"> { - const defaultEvent: ProcessorEvent<"Strategy", "DistributedWithRecipientAddress"> = { - params: { - amount: "1000", - recipientAddress: "0x1234567890123456789012345678901234567890", - recipientId: "0x1234567890123456789012345678901234567890", - sender: "0x1234567890123456789012345678901234567890", - }, - eventName: "DistributedWithRecipientAddress", - srcAddress: "0x1234567890123456789012345678901234567890", - blockNumber: 12345, - blockTimestamp: 1000000000, - chainId: 10 as ChainId, - contractName: "Strategy", - logIndex: 1, - transactionFields: { - hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", - transactionIndex: 6, - from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", - }, - strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", - }; - - return { ...defaultEvent, ...overrides }; -} +import { createMockEvent } from "../../mocks/index.js"; describe("BaseDistributedHandler", () => { let handler: BaseDistributedHandler; let mockRoundRepository: IRoundReadRepository; let mockEvent: ProcessorEvent<"Strategy", "DistributedWithRecipientAddress">; const chainId = 10 as ChainId; + const eventName = "DistributedWithRecipientAddress"; + const defaultParams = { + amount: "1000", + recipientAddress: "0x1234567890123456789012345678901234567890", + recipientId: "0x1234567890123456789012345678901234567890", + sender: "0x1234567890123456789012345678901234567890", + } as const; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; + const logger: ILogger = { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: vi.fn(), }; + beforeEach(() => { mockRoundRepository = { getRoundByStrategyAddress: vi.fn(), @@ -51,7 +34,7 @@ describe("BaseDistributedHandler", () => { }); it("increments round total distributed when round is found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockRound = { id: "round1" } as Round; vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); @@ -76,7 +59,7 @@ describe("BaseDistributedHandler", () => { }); it("returns an empty array when round is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(undefined); diff --git a/packages/processors/test/strategy/common/baseDistributionUpdated.handler.spec.ts b/packages/processors/test/strategy/common/baseDistributionUpdated.handler.spec.ts index 7772add..c900ca7 100644 --- a/packages/processors/test/strategy/common/baseDistributionUpdated.handler.spec.ts +++ b/packages/processors/test/strategy/common/baseDistributionUpdated.handler.spec.ts @@ -3,46 +3,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { IMetadataProvider } from "@grants-stack-indexer/metadata"; import { PartialRound } from "@grants-stack-indexer/repository"; -import { - Bytes32String, - ChainId, - DeepPartial, - Logger, - mergeDeep, - ProcessorEvent, -} from "@grants-stack-indexer/shared"; +import { Bytes32String, ChainId, Logger, ProcessorEvent } from "@grants-stack-indexer/shared"; import { BaseDistributionUpdatedHandler, MetadataNotFound, MetadataParsingFailed, } from "../../../src/internal.js"; - -function createMockEvent( - overrides: DeepPartial> = {}, -): ProcessorEvent<"Strategy", "DistributionUpdated"> { - const defaultEvent: ProcessorEvent<"Strategy", "DistributionUpdated"> = { - params: { - metadata: ["1", "ipfs://QmTestHash"], - merkleRoot: "0xroot" as Bytes32String, - }, - eventName: "DistributionUpdated", - srcAddress: "0x1234567890123456789012345678901234567890", - blockNumber: 12345, - blockTimestamp: 1000000000, - chainId: 10 as ChainId, - contractName: "Strategy", - logIndex: 1, - transactionFields: { - hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", - transactionIndex: 6, - from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", - }, - strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", - }; - - return mergeDeep(defaultEvent, overrides); -} +import { createMockEvent } from "../../mocks/index.js"; describe("BaseDistributionUpdatedHandler", () => { let handler: BaseDistributionUpdatedHandler; @@ -50,6 +18,12 @@ describe("BaseDistributionUpdatedHandler", () => { let mockLogger: Logger; let mockEvent: ProcessorEvent<"Strategy", "DistributionUpdated">; const chainId = 10 as ChainId; + const eventName = "DistributionUpdated"; + const defaultParams = { + metadata: ["1", "ipfs://QmTestHash"] as [string, string], + merkleRoot: "0xroot" as Bytes32String, + }; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; beforeEach(() => { mockMetadataProvider = { @@ -64,7 +38,7 @@ describe("BaseDistributionUpdatedHandler", () => { }); it("handles a valid distribution update event", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockDistribution = { matchingDistribution: [ { @@ -105,7 +79,7 @@ describe("BaseDistributionUpdatedHandler", () => { }); it("throws MetadataNotFound if distribution metadata is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); handler = new BaseDistributionUpdatedHandler(mockEvent, chainId, { @@ -120,7 +94,7 @@ describe("BaseDistributionUpdatedHandler", () => { }); it("throw MatchingDistributionParsingError if distribution format is invalid", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const invalidDistribution = { matchingDistribution: [ { @@ -145,7 +119,7 @@ describe("BaseDistributionUpdatedHandler", () => { }); it("handles empty matching distribution array", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const emptyDistribution = { matchingDistribution: [], }; diff --git a/packages/processors/test/strategy/common/baseFundsDistributed.handler.spec.ts b/packages/processors/test/strategy/common/baseFundsDistributed.handler.spec.ts index cc330d7..399f8c3 100644 --- a/packages/processors/test/strategy/common/baseFundsDistributed.handler.spec.ts +++ b/packages/processors/test/strategy/common/baseFundsDistributed.handler.spec.ts @@ -8,45 +8,12 @@ import { Round, RoundNotFound, } from "@grants-stack-indexer/repository"; -import { - ChainId, - DeepPartial, - Logger, - mergeDeep, - ProcessorEvent, -} from "@grants-stack-indexer/shared"; +import { ChainId, Logger, ProcessorEvent } from "@grants-stack-indexer/shared"; import "../../../src/exceptions/index.js"; import { BaseFundsDistributedHandler } from "../../../src/internal.js"; - -function createMockEvent( - overrides: DeepPartial> = {}, -): ProcessorEvent<"Strategy", "FundsDistributed"> { - const defaultEvent: ProcessorEvent<"Strategy", "FundsDistributed"> = { - params: { - recipientId: "0x1234567890123456789012345678901234567890", - amount: "1000000000000000000", - grantee: "0x1234567890123456789012345678901234567890", - token: "0x0000000000000000000000000000000000000000", - }, - eventName: "FundsDistributed", - srcAddress: "0x1234567890123456789012345678901234567890", - blockNumber: 12345, - blockTimestamp: 1000000000, - chainId: 10 as ChainId, - contractName: "Strategy", - logIndex: 1, - transactionFields: { - hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", - transactionIndex: 6, - from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", - }, - strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", - }; - - return mergeDeep(defaultEvent, overrides); -} +import { createMockEvent } from "../../mocks/index.js"; describe("BaseFundsDistributedHandler", () => { let handler: BaseFundsDistributedHandler; @@ -55,6 +22,14 @@ describe("BaseFundsDistributedHandler", () => { let mockLogger: Logger; let mockEvent: ProcessorEvent<"Strategy", "FundsDistributed">; const chainId = 10 as ChainId; + const eventName = "FundsDistributed"; + const defaultParams = { + recipientId: "0x1234567890123456789012345678901234567890", + amount: "1000000000000000000", + grantee: "0x1234567890123456789012345678901234567890", + token: "0x0000000000000000000000000000000000000000", + } as const; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; beforeEach(() => { mockRoundRepository = { @@ -72,7 +47,7 @@ describe("BaseFundsDistributedHandler", () => { }); it("handles a valid funds distributed event", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockRound = { id: "round1" } as unknown as Round; const mockApplication = { id: "app1" } as unknown as Application; @@ -116,7 +91,7 @@ describe("BaseFundsDistributedHandler", () => { }); it("throws RoundNotFound if round is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( new RoundNotFound(chainId, mockEvent.strategyId), ); @@ -131,7 +106,7 @@ describe("BaseFundsDistributedHandler", () => { }); it("throws ApplicationNotFound if application is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockRound = { id: "round1" } as unknown as Round; vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( diff --git a/packages/processors/test/strategy/common/baseRecipientStatusUpdated.handler.spec.ts b/packages/processors/test/strategy/common/baseRecipientStatusUpdated.handler.spec.ts index 972c17b..2c65ffc 100644 --- a/packages/processors/test/strategy/common/baseRecipientStatusUpdated.handler.spec.ts +++ b/packages/processors/test/strategy/common/baseRecipientStatusUpdated.handler.spec.ts @@ -8,42 +8,10 @@ import { Round, RoundNotFound, } from "@grants-stack-indexer/repository"; -import { - ChainId, - DeepPartial, - Logger, - mergeDeep, - ProcessorEvent, -} from "@grants-stack-indexer/shared"; +import { ChainId, Logger, ProcessorEvent } from "@grants-stack-indexer/shared"; import { BaseRecipientStatusUpdatedHandler } from "../../../src/internal.js"; - -function createMockEvent( - overrides: DeepPartial> = {}, -): ProcessorEvent<"Strategy", "RecipientStatusUpdatedWithFullRow"> { - const defaultEvent: ProcessorEvent<"Strategy", "RecipientStatusUpdatedWithFullRow"> = { - params: { - rowIndex: "0", - fullRow: "801", // 001100100001 (status 1 at index 0, status 2 at index 4, status 3 at index 8) - sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", - }, - eventName: "RecipientStatusUpdatedWithFullRow", - srcAddress: "0x1234567890123456789012345678901234567890", - blockNumber: 12345, - blockTimestamp: 1000000000, - chainId: 10 as ChainId, - contractName: "Strategy", - logIndex: 1, - transactionFields: { - hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", - transactionIndex: 6, - from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", - }, - strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", - }; - - return mergeDeep(defaultEvent, overrides); -} +import { createMockEvent } from "../../mocks/index.js"; describe("BaseRecipientStatusUpdatedHandler", () => { let handler: BaseRecipientStatusUpdatedHandler; @@ -52,6 +20,13 @@ describe("BaseRecipientStatusUpdatedHandler", () => { let mockLogger: Logger; let mockEvent: ProcessorEvent<"Strategy", "RecipientStatusUpdatedWithFullRow">; const chainId = 10 as ChainId; + const eventName = "RecipientStatusUpdatedWithFullRow"; + const defaultParams = { + rowIndex: "0", + fullRow: "801", // 001100100001 (status 1 at index 0, status 2 at index 4, status 3 at index 8) + sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + } as const; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; beforeEach(() => { mockRoundRepository = { @@ -69,7 +44,9 @@ describe("BaseRecipientStatusUpdatedHandler", () => { }); it("handles valid status updates for multiple applications", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + blockNumber: 12345, + }); const mockRound = { id: "round1" } as Round; const mockApplication1 = { id: "0", @@ -175,7 +152,7 @@ describe("BaseRecipientStatusUpdatedHandler", () => { }); it("throws RoundNotFound if round is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( new RoundNotFound(chainId, mockEvent.strategyId), ); @@ -190,7 +167,7 @@ describe("BaseRecipientStatusUpdatedHandler", () => { }); it("skips applications that are not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockRound = { id: "round1" } as Round; vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( @@ -209,7 +186,7 @@ describe("BaseRecipientStatusUpdatedHandler", () => { }); it("skips invalid status values", async () => { - mockEvent = createMockEvent({ + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { params: { rowIndex: "0", fullRow: "96", // Binary: 1100000 (invalid statuses 6 and 7) @@ -232,7 +209,9 @@ describe("BaseRecipientStatusUpdatedHandler", () => { }); it("doesn't create new status snapshot if status hasn't changed", async () => { - mockEvent = createMockEvent({ params: { rowIndex: "0", fullRow: "2" } }); // Binary: 10 (status 2 at index 0) + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { rowIndex: "0", fullRow: "2" }, // Binary: 10 (status 2 at index 0) + }); const mockRound = { id: "round1" } as Round; const mockApplication = { id: "0", @@ -273,7 +252,7 @@ describe("BaseRecipientStatusUpdatedHandler", () => { }); it("handles different row indexes correctly", async () => { - mockEvent = createMockEvent({ + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { params: { rowIndex: "1", // Second row fullRow: "33", // 00100001 (status 1 at index 0, status 1 at index 4) diff --git a/packages/processors/test/strategy/directAllocation/handlers/directAllocated.handler.spec.ts b/packages/processors/test/strategy/directAllocation/handlers/directAllocated.handler.spec.ts index 24b38b2..a177e51 100644 --- a/packages/processors/test/strategy/directAllocation/handlers/directAllocated.handler.spec.ts +++ b/packages/processors/test/strategy/directAllocation/handlers/directAllocated.handler.spec.ts @@ -10,47 +10,12 @@ import { Round, RoundNotFound, } from "@grants-stack-indexer/repository"; -import { - Bytes32String, - ChainId, - DeepPartial, - ILogger, - mergeDeep, - ProcessorEvent, -} from "@grants-stack-indexer/shared"; +import { Bytes32String, ChainId, ILogger, 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); -} +import { createMockEvent } from "../../../mocks/index.js"; describe("DirectAllocatedHandler", () => { let handler: DirectAllocatedHandler; @@ -60,6 +25,15 @@ describe("DirectAllocatedHandler", () => { let mockEvent: ProcessorEvent<"Strategy", "DirectAllocated">; let mockLogger: ILogger; const chainId = 10 as ChainId; + const eventName = "DirectAllocated"; + const defaultParams = { + profileId: "0x1234567890123456789012345678901234567890" as Bytes32String, + profileOwner: "0x1234567890123456789012345678901234567890", + amount: parseEther("10").toString(), + token: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + sender: "0x1234567890123456789012345678901234567890", + } as const; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; beforeEach(() => { mockRoundRepository = { @@ -81,7 +55,9 @@ describe("DirectAllocatedHandler", () => { it("handles a valid direct allocation event", async () => { const amount = parseEther("10").toString(); - mockEvent = createMockEvent({ params: { amount } }); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { amount }, + }); const mockRound = { id: "round1", } as unknown as Round; @@ -136,7 +112,7 @@ describe("DirectAllocatedHandler", () => { }); it("throws RoundNotFound if round is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( new RoundNotFound(chainId, mockEvent.srcAddress), ); @@ -152,7 +128,7 @@ describe("DirectAllocatedHandler", () => { }); it("throws ProjectNotFound if project is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockRound = { id: mockEvent.params.profileId, matchTokenAddress: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", @@ -176,7 +152,7 @@ describe("DirectAllocatedHandler", () => { }); it("throws TokenPriceNotFoundError if token price is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockRound = { id: "round1", } as unknown as Round; diff --git a/packages/processors/test/strategy/directGrantsLite/directGrantsLite.handler.spec.ts b/packages/processors/test/strategy/directGrantsLite/directGrantsLite.handler.spec.ts new file mode 100644 index 0000000..2af0acd --- /dev/null +++ b/packages/processors/test/strategy/directGrantsLite/directGrantsLite.handler.spec.ts @@ -0,0 +1,246 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import type { + IApplicationReadRepository, + IProjectReadRepository, + IRoundReadRepository, +} from "@grants-stack-indexer/repository"; +import { EvmProvider } from "@grants-stack-indexer/chain-providers"; +import { IPricingProvider } from "@grants-stack-indexer/pricing"; +import { ChainId, ILogger, ProcessorEvent, StrategyEvent } from "@grants-stack-indexer/shared"; + +import { ProcessorDependencies, UnsupportedEventException } from "../../../src/internal.js"; +import { BaseRecipientStatusUpdatedHandler } from "../../../src/processors/strategy/common/index.js"; +import { + DGLiteAllocatedHandler, + DGLiteRegisteredHandler, + DGLiteTimestampsUpdatedHandler, + DGLiteUpdatedRegistrationHandler, + DirectGrantsLiteStrategyHandler, +} from "../../../src/processors/strategy/directGrantsLite/index.js"; + +vi.mock("../../../src/processors/strategy/directGrantsLite/handlers/index.js", () => { + const DGLiteRegisteredHandler = vi.fn(); + const DGLiteAllocatedHandler = vi.fn(); + const DGLiteTimestampsUpdatedHandler = vi.fn(); + const DGLiteUpdatedRegistrationHandler = vi.fn(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + DGLiteRegisteredHandler.prototype.handle = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + DGLiteAllocatedHandler.prototype.handle = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + DGLiteTimestampsUpdatedHandler.prototype.handle = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + DGLiteUpdatedRegistrationHandler.prototype.handle = vi.fn(); + + return { + DGLiteRegisteredHandler, + DGLiteAllocatedHandler, + DGLiteTimestampsUpdatedHandler, + DGLiteUpdatedRegistrationHandler, + }; +}); +vi.mock("../../../src/processors/strategy/common/index.js", async (importOriginal) => { + const original = + await importOriginal(); + + const BaseRecipientStatusUpdatedHandler = vi.fn(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + BaseRecipientStatusUpdatedHandler.prototype.handle = vi.fn(); + return { + ...original, + BaseRecipientStatusUpdatedHandler, + }; +}); + +describe("DirectGrantsLiteStrategyHandler", () => { + const mockChainId = 10 as ChainId; + let handler: DirectGrantsLiteStrategyHandler; + let mockMetadataProvider: IMetadataProvider; + let mockRoundRepository: IRoundReadRepository; + let mockProjectRepository: IProjectReadRepository; + let mockEVMProvider: EvmProvider; + let mockPricingProvider: IPricingProvider; + let mockApplicationRepository: IApplicationReadRepository; + let dependencies: ProcessorDependencies; + + 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 = { + getMulticall3Address: vi.fn(), + multicall: vi.fn(), + readContract: vi.fn(), + } as unknown as EvmProvider; + mockPricingProvider = { + getTokenPrice: vi.fn(), + } as IPricingProvider; + mockApplicationRepository = {} as IApplicationReadRepository; + dependencies = { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, + applicationRepository: mockApplicationRepository, + logger, + }; + handler = new DirectGrantsLiteStrategyHandler(mockChainId, dependencies); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("gets correct name", () => { + expect(handler.name).toBe("allov2.DirectGrantsLiteStrategy"); + }); + + it("calls RegisteredHandler for RegisteredWithSender event", async () => { + const mockEvent = { + eventName: "RegisteredWithSender", + } as ProcessorEvent<"Strategy", "RegisteredWithSender">; + + vi.spyOn(DGLiteRegisteredHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(DGLiteRegisteredHandler).toHaveBeenCalledWith(mockEvent, mockChainId, dependencies); + expect(DGLiteRegisteredHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("calls AllocatedHandler for AllocatedWithToken event", async () => { + const mockEvent = { + eventName: "AllocatedWithToken", + } as ProcessorEvent<"Strategy", "AllocatedWithToken">; + + vi.spyOn(DGLiteAllocatedHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(DGLiteAllocatedHandler).toHaveBeenCalledWith(mockEvent, mockChainId, dependencies); + expect(DGLiteAllocatedHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("calls TimestampsUpdatedHandler for TimestampsUpdated event", async () => { + const mockEvent = { + eventName: "TimestampsUpdated", + } as ProcessorEvent<"Strategy", "TimestampsUpdated">; + + vi.spyOn(DGLiteTimestampsUpdatedHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(DGLiteTimestampsUpdatedHandler).toHaveBeenCalledWith( + mockEvent, + mockChainId, + dependencies, + ); + expect(DGLiteTimestampsUpdatedHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("calls RecipientStatusUpdatedHandler for RecipientStatusUpdatedWithFullRow event", async () => { + const mockEvent = { + eventName: "RecipientStatusUpdatedWithFullRow", + } as ProcessorEvent<"Strategy", "RecipientStatusUpdatedWithFullRow">; + + vi.spyOn(BaseRecipientStatusUpdatedHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(BaseRecipientStatusUpdatedHandler).toHaveBeenCalledWith( + mockEvent, + mockChainId, + dependencies, + ); + expect(BaseRecipientStatusUpdatedHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("calls UpdatedRegistrationHandler for UpdatedRegistrationWithStatus event", async () => { + const mockEvent = { + eventName: "UpdatedRegistrationWithStatus", + } as ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus">; + + vi.spyOn(DGLiteUpdatedRegistrationHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(DGLiteUpdatedRegistrationHandler).toHaveBeenCalledWith( + mockEvent, + mockChainId, + dependencies, + ); + expect(DGLiteUpdatedRegistrationHandler.prototype.handle).toHaveBeenCalled(); + }); + + describe("fetchStrategyTimings", () => { + it("fetches correct timings using multicall", async () => { + const strategyId = "0x1234567890123456789012345678901234567890"; + const mockTimings = [1000n, 2000n]; + + vi.spyOn(mockEVMProvider, "getMulticall3Address").mockReturnValue("0xmulticalladdress"); + vi.spyOn(mockEVMProvider, "multicall").mockResolvedValue(mockTimings); + + const result = await handler.fetchStrategyTimings(strategyId); + + expect(result).toEqual({ + applicationsStartTime: new Date(Number(mockTimings[0]) * 1000), + applicationsEndTime: new Date(Number(mockTimings[1]) * 1000), + donationsStartTime: null, + donationsEndTime: null, + }); + + expect(mockEVMProvider.multicall).toHaveBeenCalled(); + expect(mockEVMProvider.readContract).not.toHaveBeenCalled(); + }); + + it("fetches correct timings when multicall is not available", async () => { + const strategyId = "0x1234567890123456789012345678901234567890"; + const mockTimings = [1000n, 2000n]; + + vi.spyOn(mockEVMProvider, "getMulticall3Address").mockReturnValue(undefined); + vi.spyOn(mockEVMProvider, "readContract").mockImplementation((_, __, functionName) => { + switch (functionName) { + case "registrationStartTime": + return Promise.resolve(mockTimings[0]); + case "registrationEndTime": + return Promise.resolve(mockTimings[1]); + default: + return Promise.resolve(undefined); + } + }); + + const result = await handler.fetchStrategyTimings(strategyId); + + expect(result).toEqual({ + applicationsStartTime: new Date(Number(mockTimings[0]) * 1000), + applicationsEndTime: new Date(Number(mockTimings[1]) * 1000), + donationsStartTime: null, + donationsEndTime: null, + }); + + expect(mockEVMProvider.readContract).toHaveBeenCalledTimes(2); + expect(mockEVMProvider.multicall).not.toHaveBeenCalled(); + }); + }); + + it("throws UnsupportedEventException for unknown event names", async () => { + const mockEvent = { eventName: "UnknownEvent" } as unknown as ProcessorEvent< + "Strategy", + StrategyEvent + >; + await expect(() => handler.handle(mockEvent)).rejects.toThrow( + new UnsupportedEventException("Strategy", "UnknownEvent", handler.name), + ); + }); +}); diff --git a/packages/processors/test/strategy/directGrantsLite/handlers/allocated.handler.spec.ts b/packages/processors/test/strategy/directGrantsLite/handlers/allocated.handler.spec.ts new file mode 100644 index 0000000..b174bc4 --- /dev/null +++ b/packages/processors/test/strategy/directGrantsLite/handlers/allocated.handler.spec.ts @@ -0,0 +1,372 @@ +import { getAddress, pad, parseEther } from "viem"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { IPricingProvider } from "@grants-stack-indexer/pricing"; +import { + Application, + ApplicationNotFound, + IApplicationRepository, + IRoundRepository, + Round, + RoundNotFound, +} from "@grants-stack-indexer/repository"; +import { ChainId, ProcessorEvent, UnknownToken } from "@grants-stack-indexer/shared"; + +import { TokenPriceNotFoundError } from "../../../../src/exceptions/tokenPriceNotFound.exception.js"; +import { DGLiteAllocatedHandler } from "../../../../src/processors/strategy/directGrantsLite/handlers/allocated.handler.js"; +import { createMockEvent } from "../../../mocks/index.js"; + +describe("DGLiteAllocatedHandler", () => { + let handler: DGLiteAllocatedHandler; + let mockRoundRepository: IRoundRepository; + let mockApplicationRepository: IApplicationRepository; + let mockPricingProvider: IPricingProvider; + let mockEvent: ProcessorEvent<"Strategy", "AllocatedWithToken">; + const chainId = 10 as ChainId; + const eventName = "AllocatedWithToken"; + const defaultParams = { + recipientId: "0x1234567890123456789012345678901234567890", + amount: parseEther("10").toString(), + token: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + } as const; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; + + beforeEach(() => { + mockRoundRepository = { + getRoundByStrategyAddressOrThrow: vi.fn(), + } as unknown as IRoundRepository; + mockApplicationRepository = { + getApplicationByAnchorAddressOrThrow: vi.fn(), + } as unknown as IApplicationRepository; + mockPricingProvider = { + getTokenPrice: vi.fn(), + } as IPricingProvider; + }); + + it("handles a valid allocation event", async () => { + const amount = parseEther("10").toString(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { amount }, + }); + const mockRound = { + id: "round1", + matchTokenAddress: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + matchAmount: BigInt(0), + matchAmountInUsd: "0", + fundedAmount: BigInt(0), + fundedAmountInUsd: "0", + totalAmountDonatedInUsd: "0", + totalDonationsCount: 0, + uniqueDonorsCount: 0, + tags: [], + } as unknown as Round; + + const mockApplication = { + id: "app1", + projectId: "project1", + } as unknown as Application; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); + vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue({ + timestampMs: 1000000000, + priceUsd: 2000, + }); + + handler = new DGLiteAllocatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + pricingProvider: mockPricingProvider, + }); + + const result = await handler.handle(); + + expect(result[0]).toEqual({ + type: "InsertApplicationPayout", + args: { + applicationPayout: { + amount: BigInt(amount), + applicationId: "app1", + roundId: "round1", + chainId, + tokenAddress: getAddress(mockEvent.params.token), + amountInRoundMatchToken: BigInt(amount), + amountInUsd: "20000", + transactionHash: mockEvent.transactionFields.hash, + sender: getAddress(mockEvent.params.sender), + timestamp: new Date(mockEvent.blockTimestamp), + }, + }, + }); + }); + + it("doesn't fetch token price if amount is 0", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { amount: "0" }, + }); + const mockRound = { + id: "round1", + matchTokenAddress: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + matchAmount: BigInt(0), + matchAmountInUsd: "0", + fundedAmount: BigInt(0), + fundedAmountInUsd: "0", + totalAmountDonatedInUsd: "0", + totalDonationsCount: 0, + uniqueDonorsCount: 0, + tags: [], + } as unknown as Round; + + const mockApplication = { + id: "app1", + projectId: "project1", + } as unknown as Application; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); + + handler = new DGLiteAllocatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + pricingProvider: mockPricingProvider, + }); + + const result = await handler.handle(); + const changeset = result[0] as { + type: "InsertApplicationPayout"; + args: { + applicationPayout: { + amount: bigint; + amountInUsd: string; + amountInRoundMatchToken: bigint; + }; + }; + }; + + expect(mockPricingProvider.getTokenPrice).not.toHaveBeenCalled(); + expect(changeset.args.applicationPayout.amount).toBe(0n); + expect(changeset.args.applicationPayout.amountInUsd).toBe("0"); + expect(changeset.args.applicationPayout.amountInRoundMatchToken).toBe(0n); + }); + + 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 DGLiteAllocatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + pricingProvider: mockPricingProvider, + }); + + await expect(handler.handle()).rejects.toThrow(RoundNotFound); + }); + + it("throws ApplicationNotFound if application is not found", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); + const mockRound = { + id: "round1", + chainId, + matchTokenAddress: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + matchAmount: BigInt(0), + matchAmountInUsd: "0", + fundedAmount: BigInt(0), + fundedAmountInUsd: "0", + totalAmountDonatedInUsd: "0", + totalDonationsCount: 0, + uniqueDonorsCount: 0, + tags: [], + } as unknown as Round; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockRejectedValue( + new ApplicationNotFound(chainId, mockRound.id, mockEvent.params.recipientId), + ); + + handler = new DGLiteAllocatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + pricingProvider: mockPricingProvider, + }); + + await expect(handler.handle()).rejects.toThrow(ApplicationNotFound); + }); + + it("throws UnknownToken if params token is not found", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { token: pad("0x1", { size: 20 }) }, + }); + const mockRound = { + id: "round1", + matchTokenAddress: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + } as unknown as Round; + const mockApplication = { + id: "app1", + metadata: { + application: { + round: "round1", + recipient: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + }, + }, + projectId: "project1", + } as unknown as Application; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); + + handler = new DGLiteAllocatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + pricingProvider: mockPricingProvider, + }); + + await expect(handler.handle()).rejects.toThrow(UnknownToken); + }); + + it("throws UnknownToken if match token is not found", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); + const mockRound = { + id: "round1", + matchTokenAddress: pad("0x1", { size: 20 }), + } as unknown as Round; + const mockApplication = { + id: "app1", + metadata: { + application: { + round: "round1", + recipient: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + }, + }, + projectId: "project1", + } as unknown as Application; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); + + handler = new DGLiteAllocatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + pricingProvider: mockPricingProvider, + }); + + await expect(handler.handle()).rejects.toThrow(UnknownToken); + }); + + it("throws TokenPriceNotFound if token price is not found", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); + const mockRound = { + id: "round1", + matchTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + } as unknown as Round; + const mockApplication = { + id: "app1", + metadata: { + application: { + round: "round1", + recipient: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + }, + }, + projectId: "project1", + } as unknown as Application; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); + vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue(undefined); + + handler = new DGLiteAllocatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + pricingProvider: mockPricingProvider, + }); + + await expect(handler.handle()).rejects.toThrow(TokenPriceNotFoundError); + }); + + it("handles different token and match token", async () => { + const amount = parseEther("10").toString(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { amount }, + }); + const mockRound = { + id: "round1", + chainId, + matchTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + matchAmount: BigInt(0), + matchAmountInUsd: "0", + fundedAmount: BigInt(0), + fundedAmountInUsd: "0", + totalAmountDonatedInUsd: "0", + totalDonationsCount: 0, + uniqueDonorsCount: 0, + tags: [], + } as unknown as Round; + + const mockApplication = { + id: "app1", + projectId: "project1", + } as unknown as Application; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); + vi.spyOn(mockPricingProvider, "getTokenPrice") + .mockResolvedValueOnce({ + timestampMs: 1000000000, + priceUsd: 1, + }) + .mockResolvedValueOnce({ + timestampMs: 1000000000, + priceUsd: 2000, + }); + + handler = new DGLiteAllocatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + pricingProvider: mockPricingProvider, + }); + + const result = await handler.handle(); + const changeset = result[0] as { + type: "InsertApplicationPayout"; + args: { applicationPayout: { amountInRoundMatchToken: bigint } }; + }; + expect(changeset.args.applicationPayout.amountInRoundMatchToken).toBe(parseEther("0.005")); + }); +}); diff --git a/packages/processors/test/strategy/directGrantsLite/handlers/registered.handler.spec.ts b/packages/processors/test/strategy/directGrantsLite/handlers/registered.handler.spec.ts new file mode 100644 index 0000000..3ff756f --- /dev/null +++ b/packages/processors/test/strategy/directGrantsLite/handlers/registered.handler.spec.ts @@ -0,0 +1,175 @@ +import { getAddress } from "viem"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import { + IProjectRepository, + IRoundRepository, + NewApplication, + Project, + ProjectNotFound, + Round, + RoundNotFound, +} from "@grants-stack-indexer/repository"; +import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; + +import { DGLiteRegisteredHandler } from "../../../../src/processors/strategy/directGrantsLite/handlers/registered.handler.js"; +import { createMockEvent } from "../../../mocks/index.js"; + +describe("DGLiteRegisteredHandler", () => { + let handler: DGLiteRegisteredHandler; + 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: "0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000100000000000000000000000000accc041f3d1f576198ac88ede32e58c3476710a700000000000000000000000058338e95caef17861916ef10dad5fafe20421005000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656966736e77736a6c6b74746632626d6f646a6c646e76766c366677707271766a6976786b67367a6e74376a656c62786a75717a33650000000000", + 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 unknown as Project; + const mockRound = { + id: "round1", + chainId, + } as unknown 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 DGLiteRegisteredHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + }); + + const result = await handler.handle(); + + expect(result).toEqual([ + { + type: "InsertApplication", + args: { + chainId, + id: "1", + projectId: "project1", + anchorAddress: getAddress(mockEvent.params.recipientId), + roundId: "round1", + status: "PENDING", + metadataCid: "bafkreifsnwsjlkttf2bmodjldnvvl6fwprqvjivxkg6znt7jelbxjuqz3e", + 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"], + }, + }, + ]); + + expect(mockMetadataProvider.getMetadata).toHaveBeenCalledWith( + "bafkreifsnwsjlkttf2bmodjldnvvl6fwprqvjivxkg6znt7jelbxjuqz3e", + ); + }); + + it("handles null metadata", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); + const mockProject = { + id: "project1", + anchorAddress: mockEvent.params.recipientId, + } as unknown as Project; + const mockRound = { + id: "round1", + chainId, + } as unknown as Round; + + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(null); + + handler = new DGLiteRegisteredHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + }); + + const result = await handler.handle(); + expect(result).toHaveLength(1); + const changeset = result[0] as { + type: "InsertApplication"; + args: NewApplication; + }; + expect(changeset.args.metadata).toBeNull(); + }); + + 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 DGLiteRegisteredHandler(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 unknown as Project; + + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( + new RoundNotFound(chainId, mockEvent.strategyId), + ); + + handler = new DGLiteRegisteredHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + }); + + await expect(handler.handle()).rejects.toThrow(RoundNotFound); + }); +}); diff --git a/packages/processors/test/strategy/directGrantsLite/handlers/timestampsUpdated.handler.spec.ts b/packages/processors/test/strategy/directGrantsLite/handlers/timestampsUpdated.handler.spec.ts new file mode 100644 index 0000000..1032620 --- /dev/null +++ b/packages/processors/test/strategy/directGrantsLite/handlers/timestampsUpdated.handler.spec.ts @@ -0,0 +1,79 @@ +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 { DGLiteTimestampsUpdatedHandler } from "../../../../src/processors/strategy/directGrantsLite/handlers/timestampsUpdated.handler.js"; +import { createMockEvent } from "../../../mocks/index.js"; + +describe("DGLiteTimestampsUpdatedHandler", () => { + let handler: DGLiteTimestampsUpdatedHandler; + 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 unknown as Round; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + + handler = new DGLiteTimestampsUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + + const updateRound = result[0] as { + type: "UpdateRound"; + args: { + chainId: ChainId; + roundId: string; + round: { + applicationsStartTime: Date; + applicationsEndTime: Date; + }; + }; + }; + + expect(updateRound).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 DGLiteTimestampsUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + }); + + await expect(handler.handle()).rejects.toThrow(RoundNotFound); + }); +}); diff --git a/packages/processors/test/strategy/directGrantsLite/handlers/updatedRegistration.handler.spec.ts b/packages/processors/test/strategy/directGrantsLite/handlers/updatedRegistration.handler.spec.ts new file mode 100644 index 0000000..09fc0a7 --- /dev/null +++ b/packages/processors/test/strategy/directGrantsLite/handlers/updatedRegistration.handler.spec.ts @@ -0,0 +1,300 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import { + Application, + ApplicationNotFound, + IApplicationRepository, + IProjectRepository, + IRoundRepository, + PartialApplication, + Project, + ProjectNotFound, + Round, + RoundNotFound, +} from "@grants-stack-indexer/repository"; +import { ChainId, Logger, ProcessorEvent } from "@grants-stack-indexer/shared"; + +import { DGLiteUpdatedRegistrationHandler } from "../../../../src/processors/strategy/directGrantsLite/handlers/updatedRegistration.handler.js"; +import { createMockEvent } from "../../../mocks/index.js"; + +describe("DGLiteUpdatedRegistrationHandler", () => { + let handler: DGLiteUpdatedRegistrationHandler; + let mockRoundRepository: IRoundRepository; + let mockApplicationRepository: IApplicationRepository; + let mockProjectRepository: IProjectRepository; + let mockMetadataProvider: IMetadataProvider; + let mockLogger: Logger; + let mockEvent: ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus">; + const chainId = 10 as ChainId; + const eventName = "UpdatedRegistrationWithStatus"; + const defaultParams = { + recipientId: "0x1234567890123456789012345678901234567890", + status: "2", + data: "0x0000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000", + sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + } as const; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; + + beforeEach(() => { + mockRoundRepository = { + getRoundByStrategyAddressOrThrow: vi.fn(), + } as unknown as IRoundRepository; + mockApplicationRepository = { + getApplicationByAnchorAddressOrThrow: vi.fn(), + } as unknown as IApplicationRepository; + mockProjectRepository = { + getProjectByAnchorOrThrow: vi.fn(), + } as unknown as IProjectRepository; + mockMetadataProvider = { + getMetadata: vi.fn(), + } as unknown as IMetadataProvider; + mockLogger = { + warn: vi.fn(), + error: vi.fn(), + info: vi.fn(), + debug: vi.fn(), + } as unknown as Logger; + }); + + it("handles a valid registration update event", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); + const mockProject = { + id: "project1", + anchorAddress: mockEvent.params.recipientId, + } as Project; + const mockRound = { id: "round1" } as Round; + const mockApplication = { + id: "app1", + status: "PENDING", + statusSnapshots: [], + statusUpdatedAtBlock: 12344n, + } as unknown as Application; + const mockMetadata = { name: "Test Project" }; + + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(mockMetadata); + + handler = new DGLiteUpdatedRegistrationHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + const result = await handler.handle(); + + expect(result[0]).toEqual({ + type: "UpdateApplication", + args: { + chainId, + roundId: "round1", + applicationId: "app1", + application: { + ...mockApplication, + metadata: mockMetadata, + metadataCid: "bafkreigyc43cfinxlnahq5aawsghibetv6usrs7kkxf7vxmzbjyro76iwy", + status: "APPROVED", + statusUpdatedAtBlock: BigInt(mockEvent.blockNumber), + statusSnapshots: [ + { + status: "APPROVED", + updatedAtBlock: mockEvent.blockNumber.toString(), + updatedAt: new Date(mockEvent.blockTimestamp), + }, + ], + }, + }, + }); + }); + + it("returns empty array for invalid status", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { status: "4" }, + }); // Invalid status + + handler = new DGLiteUpdatedRegistrationHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + const result = await handler.handle(); + expect(result).toEqual([]); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining( + `[DGLiteUpdatedRegistrationHandler] Invalid status: ${mockEvent.params.status}`, + ), + ); + }); + + 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 DGLiteUpdatedRegistrationHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + 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.srcAddress), + ); + + handler = new DGLiteUpdatedRegistrationHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + await expect(handler.handle()).rejects.toThrow(RoundNotFound); + }); + it("throws ApplicationNotFound if application is not found", 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( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockRejectedValue( + new ApplicationNotFound(chainId, mockRound.id, mockEvent.params.recipientId), + ); + + handler = new DGLiteUpdatedRegistrationHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + await expect(handler.handle()).rejects.toThrow(ApplicationNotFound); + }); + + 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; + const mockApplication = { + id: "app1", + status: "PENDING", + statusSnapshots: [], + statusUpdatedAtBlock: 12344n, + } as unknown as Application; + + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); + + handler = new DGLiteUpdatedRegistrationHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + const result = await handler.handle(); + + expect(result).toBeDefined(); + expect(result).toHaveLength(1); + const changeset = result[0] as { + type: "UpdateApplication"; + args: { application: PartialApplication }; + }; + expect(changeset.args.application.metadata).toBeNull(); + }); + + it("doesn't add status snapshot if status hasn't changed", async () => { + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { status: "1" }, + }); // 1 is PENDING + const mockProject = { + id: "project1", + anchorAddress: mockEvent.params.recipientId, + } as Project; + const mockRound = { id: "round1" } as Round; + const mockApplication = { + id: "app1", + status: "PENDING", // Same as new status + statusSnapshots: [ + { + status: "PENDING", + updatedAtBlock: "12344", + updatedAt: new Date(1000000000), + }, + ], + statusUpdatedAtBlock: 12344n, + } as Application; + + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(null); + + handler = new DGLiteUpdatedRegistrationHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + const result = await handler.handle(); + const changeset = result[0] as { + type: "UpdateApplication"; + args: { application: { statusSnapshots: unknown[] } }; + }; + expect(changeset.args.application.statusSnapshots).toHaveLength(1); + }); +}); diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.spec.ts index b018b5f..f7b0017 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.spec.ts @@ -10,48 +10,14 @@ import { Round, RoundNotFound, } from "@grants-stack-indexer/repository"; -import { - ChainId, - DeepPartial, - mergeDeep, - ProcessorEvent, - UnknownToken, -} from "@grants-stack-indexer/shared"; +import { ChainId, ProcessorEvent, UnknownToken } from "@grants-stack-indexer/shared"; import { MetadataParsingFailed, TokenPriceNotFoundError, } from "../../../../src/exceptions/index.js"; import { DVMDAllocatedHandler } from "../../../../src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.js"; - -function createMockEvent( - overrides: DeepPartial> = {}, -): ProcessorEvent<"Strategy", "AllocatedWithOrigin"> { - const defaultEvent: ProcessorEvent<"Strategy", "AllocatedWithOrigin"> = { - params: { - recipientId: "0x1234567890123456789012345678901234567890", - amount: "10", - token: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", - origin: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", - sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", - }, - eventName: "AllocatedWithOrigin", - 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); -} +import { createMockEvent } from "../../../mocks/index.js"; describe("DVMDAllocatedHandler", () => { let handler: DVMDAllocatedHandler; @@ -60,7 +26,16 @@ describe("DVMDAllocatedHandler", () => { let mockPricingProvider: IPricingProvider; let mockEvent: ProcessorEvent<"Strategy", "AllocatedWithOrigin">; const chainId = 10 as ChainId; - const expectedDonationId = "0x60077b059a7ca75483cf0651e209a0d5c14ad2afb1fd363c728f13680d24c546"; + const eventName = "AllocatedWithOrigin"; + const defaultParams = { + recipientId: "0x1234567890123456789012345678901234567890", + amount: "10", + token: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + origin: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + } as const; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; + const expectedDonationId = "0x86ec85686b02d646ee8a45f0770e85db890679ef7e5f962a51be056f32d54e15"; beforeEach(() => { mockRoundRepository = { @@ -76,7 +51,9 @@ describe("DVMDAllocatedHandler", () => { it("handle a valid allocated event", async () => { const amount = parseEther("10").toString(); - mockEvent = createMockEvent({ params: { amount } }); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { amount }, + }); const mockRound = { id: "round1", matchTokenAddress: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", @@ -139,7 +116,9 @@ describe("DVMDAllocatedHandler", () => { it("match token is different from event token", async () => { const amount = parseEther("1500").toString(); - mockEvent = createMockEvent({ params: { amount } }); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { amount }, + }); const mockRound = { id: "round1", matchTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", @@ -198,7 +177,7 @@ describe("DVMDAllocatedHandler", () => { }); it("throws RoundNotFound if round is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( new RoundNotFound(chainId, mockEvent.strategyId), ); @@ -213,7 +192,7 @@ describe("DVMDAllocatedHandler", () => { }); it("throws ApplicationNotFound if application is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockRound = { id: "round1", matchTokenAddress: "0x0987654321098765432109876543210987654321", @@ -239,7 +218,7 @@ describe("DVMDAllocatedHandler", () => { }); it("throws UnknownToken if params token is not found", async () => { - mockEvent = createMockEvent({ + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { params: { token: pad("0x1", { size: 20 }) }, }); const mockRound = { @@ -275,7 +254,7 @@ describe("DVMDAllocatedHandler", () => { }); it("throws UnknownToken if match token is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockRound = { id: "round1", matchTokenAddress: pad("0x1", { size: 20 }), @@ -309,7 +288,7 @@ describe("DVMDAllocatedHandler", () => { }); it("throws TokenPriceNotFound if token price is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockRound = { id: "round1", matchTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", @@ -344,7 +323,7 @@ describe("DVMDAllocatedHandler", () => { }); it("throws MetadataParsingFailed if metadata is invalid", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockRound = { id: "round1", diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts index 2c9a531..608e87c 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts @@ -10,36 +10,37 @@ import { Round, RoundNotFound, } from "@grants-stack-indexer/repository"; -import { ChainId, DeepPartial, mergeDeep, ProcessorEvent } from "@grants-stack-indexer/shared"; +import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; import { DVMDRegisteredHandler } from "../../../../src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.js"; - -function createMockEvent( - overrides: DeepPartial> = {}, -): ProcessorEvent<"Strategy", "RegisteredWithSender"> { - const defaultEvent: ProcessorEvent<"Strategy", "RegisteredWithSender"> = { - params: { - recipientId: "0x1234567890123456789012345678901234567890", - sender: "0x0987654321098765432109876543210987654321", - data: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001000000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000", - }, - eventName: "RegisteredWithSender", - srcAddress: "0x1234567890123456789012345678901234567890", - blockNumber: 12345, - blockTimestamp: 1000000000, - chainId: 10 as ChainId, - contractName: "Strategy", - logIndex: 1, - transactionFields: { - hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", - transactionIndex: 6, - from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", - }, - strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", - }; - - return mergeDeep(defaultEvent, overrides) as ProcessorEvent<"Strategy", "RegisteredWithSender">; -} +import { createMockEvent } from "../../../mocks/index.js"; + +// function createMockEvent( +// overrides: DeepPartial> = {}, +// ): ProcessorEvent<"Strategy", "RegisteredWithSender"> { +// const defaultEvent: ProcessorEvent<"Strategy", "RegisteredWithSender"> = { +// params: { +// recipientId: "0x1234567890123456789012345678901234567890", +// sender: "0x0987654321098765432109876543210987654321", +// data: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001000000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000", +// }, +// eventName: "RegisteredWithSender", +// srcAddress: "0x1234567890123456789012345678901234567890", +// blockNumber: 12345, +// blockTimestamp: 1000000000, +// chainId: 10 as ChainId, +// contractName: "Strategy", +// logIndex: 1, +// transactionFields: { +// hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", +// transactionIndex: 6, +// from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", +// }, +// strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", +// }; + +// return mergeDeep(defaultEvent, overrides) as ProcessorEvent<"Strategy", "RegisteredWithSender">; +// } describe("DVMDRegisteredHandler", () => { let handler: DVMDRegisteredHandler; @@ -48,6 +49,13 @@ describe("DVMDRegisteredHandler", () => { let mockMetadataProvider: IMetadataProvider; let mockEvent: ProcessorEvent<"Strategy", "RegisteredWithSender">; const chainId = 10 as ChainId; + const eventName = "RegisteredWithSender"; + const defaultParams = { + recipientId: "0x1234567890123456789012345678901234567890", + sender: "0x0987654321098765432109876543210987654321", + data: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001000000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000", + } as const; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; beforeEach(() => { mockRoundRepository = { @@ -62,7 +70,7 @@ describe("DVMDRegisteredHandler", () => { }); it("handle a valid registration event", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockProject = { id: "project1" } as Project; const mockRound = { id: "round1" } as Round; const mockMetadata = { name: "Test Project" }; @@ -114,7 +122,7 @@ describe("DVMDRegisteredHandler", () => { }); it("throw ProjectNotFound if project is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockRejectedValue( new ProjectNotFound(chainId, mockEvent.srcAddress), ); @@ -128,7 +136,7 @@ describe("DVMDRegisteredHandler", () => { }); it("throw RoundNotFound if round is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockProject = { id: "project1" } as Project; vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( @@ -144,7 +152,7 @@ describe("DVMDRegisteredHandler", () => { }); it("handle registration with null metadata", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockProject = { id: "project1" } as Project; const mockRound = { id: "round1" } as Round; @@ -168,7 +176,7 @@ describe("DVMDRegisteredHandler", () => { it("correctly calculate application ID based on recipientsCounter", async () => { // recipientsCounter = 5 - const mockEvent = createMockEvent({ + const mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { params: { data: "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000001000000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000", }, diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/timestampsUpdated.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/timestampsUpdated.handler.spec.ts index d928878..5294868 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/timestampsUpdated.handler.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/timestampsUpdated.handler.spec.ts @@ -6,49 +6,25 @@ import { Round, RoundNotFound, } from "@grants-stack-indexer/repository"; -import { ChainId, DeepPartial, mergeDeep, ProcessorEvent } from "@grants-stack-indexer/shared"; +import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; import { DVMDTimestampsUpdatedHandler } from "../../../../src/internal.js"; - -function createMockEvent( - overrides: DeepPartial< - ProcessorEvent<"Strategy", "TimestampsUpdatedWithRegistrationAndAllocation"> - > = {}, -): ProcessorEvent<"Strategy", "TimestampsUpdatedWithRegistrationAndAllocation"> { - const defaultEvent: ProcessorEvent< - "Strategy", - "TimestampsUpdatedWithRegistrationAndAllocation" - > = { - params: { - registrationStartTime: "1000000000", - registrationEndTime: "1000086400", // +1 day - allocationStartTime: "1000172800", // +2 days - allocationEndTime: "1000259200", // +3 days - sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", - }, - eventName: "TimestampsUpdatedWithRegistrationAndAllocation", - srcAddress: "0x1234567890123456789012345678901234567890", - blockNumber: 12345, - blockTimestamp: 1000000000, - chainId: 10 as ChainId, - contractName: "Strategy", - logIndex: 1, - transactionFields: { - hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", - transactionIndex: 6, - from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", - }, - strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", - }; - - return mergeDeep(defaultEvent, overrides); -} +import { createMockEvent } from "../../../mocks/index.js"; describe("DVMDTimestampsUpdatedHandler", () => { let handler: DVMDTimestampsUpdatedHandler; let mockRoundRepository: IRoundReadRepository; let mockEvent: ProcessorEvent<"Strategy", "TimestampsUpdatedWithRegistrationAndAllocation">; const chainId = 10 as ChainId; + const eventName = "TimestampsUpdatedWithRegistrationAndAllocation"; + const defaultParams = { + registrationStartTime: "1000000000", + registrationEndTime: "1000086400", // +1 day + allocationStartTime: "1000172800", // +2 days + allocationEndTime: "1000259200", // +3 days + sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + } as const; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; beforeEach(() => { mockRoundRepository = { @@ -64,7 +40,7 @@ describe("DVMDTimestampsUpdatedHandler", () => { allocationEndTime: "1704326400", // 2024-01-04 00:00:00 }; - mockEvent = createMockEvent({ + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { params: timestamps, }); const mockRound = { id: "round1" } as Round; @@ -99,7 +75,7 @@ describe("DVMDTimestampsUpdatedHandler", () => { }); it("throws RoundNotFound if round is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( new RoundNotFound(chainId, mockEvent.strategyId), ); @@ -119,7 +95,7 @@ describe("DVMDTimestampsUpdatedHandler", () => { allocationEndTime: "1704326400", // 2024-01-04 00:00:00 }; - mockEvent = createMockEvent({ + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { params: timestamps, }); const mockRound = { id: "round1" } as Round; diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/updatedRegistration.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/updatedRegistration.handler.spec.ts index 55be75a..068c7e8 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/updatedRegistration.handler.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/updatedRegistration.handler.spec.ts @@ -13,43 +13,10 @@ import { Round, RoundNotFound, } from "@grants-stack-indexer/repository"; -import { - ChainId, - DeepPartial, - Logger, - mergeDeep, - ProcessorEvent, -} from "@grants-stack-indexer/shared"; +import { ChainId, Logger, ProcessorEvent } from "@grants-stack-indexer/shared"; import { DVMDUpdatedRegistrationHandler } from "../../../../src/internal.js"; - -function createMockEvent( - overrides: DeepPartial> = {}, -): ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus"> { - const defaultEvent: ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus"> = { - params: { - recipientId: "0x1234567890123456789012345678901234567890", - status: "1", - data: "0x0000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000", - sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", - }, - eventName: "UpdatedRegistrationWithStatus", - srcAddress: "0x1234567890123456789012345678901234567890", - blockNumber: 12345, - blockTimestamp: 1000000000, - chainId: 10 as ChainId, - contractName: "Strategy", - logIndex: 1, - transactionFields: { - hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", - transactionIndex: 6, - from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", - }, - strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", - }; - - return mergeDeep(defaultEvent, overrides); -} +import { createMockEvent } from "../../../mocks/index.js"; describe("DVMDUpdatedRegistrationHandler", () => { let handler: DVMDUpdatedRegistrationHandler; @@ -60,6 +27,14 @@ describe("DVMDUpdatedRegistrationHandler", () => { let mockLogger: Logger; let mockEvent: ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus">; const chainId = 10 as ChainId; + const eventName = "UpdatedRegistrationWithStatus"; + const defaultParams = { + recipientId: "0x1234567890123456789012345678901234567890", + status: "1", + data: "0x0000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000", + sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + } as const; + const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0"; beforeEach(() => { mockRoundRepository = { @@ -83,7 +58,9 @@ describe("DVMDUpdatedRegistrationHandler", () => { }); it("handles a valid registration update event", async () => { - mockEvent = createMockEvent({ params: { status: "2" } }); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { status: "2" }, + }); const mockProject = { id: "project1", anchorAddress: mockEvent.params.recipientId, @@ -146,7 +123,9 @@ describe("DVMDUpdatedRegistrationHandler", () => { it("returns empty array if status is invalid", async () => { const invalidStatuses = ["0", "4", "10"]; for (const status of invalidStatuses) { - mockEvent = createMockEvent({ params: { status } }); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { status }, + }); handler = new DVMDUpdatedRegistrationHandler(mockEvent, chainId, { roundRepository: mockRoundRepository, @@ -166,7 +145,7 @@ describe("DVMDUpdatedRegistrationHandler", () => { }); it("throws ProjectNotFound if project is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockRejectedValue( new ProjectNotFound(chainId, mockEvent.params.recipientId), ); @@ -183,7 +162,7 @@ describe("DVMDUpdatedRegistrationHandler", () => { }); it("throws RoundNotFound if round is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockProject = { id: "project1", anchorAddress: mockEvent.params.recipientId, @@ -206,7 +185,7 @@ describe("DVMDUpdatedRegistrationHandler", () => { }); it("throws ApplicationNotFound if application is not found", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockProject = { id: "project1", anchorAddress: mockEvent.params.recipientId, @@ -236,7 +215,7 @@ describe("DVMDUpdatedRegistrationHandler", () => { }); it("handles undefined metadata", async () => { - mockEvent = createMockEvent(); + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId); const mockProject = { id: "project1", anchorAddress: mockEvent.params.recipientId, @@ -279,7 +258,9 @@ describe("DVMDUpdatedRegistrationHandler", () => { }); it("doesn't add status snapshot if status hasn't changed", async () => { - mockEvent = createMockEvent({ params: { status: "1" } }); // 1 is PENDING + mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId, { + params: { status: "1" }, + }); // 1 is PENDING const mockProject = { id: "project1", anchorAddress: mockEvent.params.recipientId, diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.spec.ts index 5d583ed..02251a3 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.spec.ts @@ -9,7 +9,7 @@ import { decodeDVMDExtendedApplicationData, } from "../../../../src/processors/strategy/helpers/index.js"; -describe("decodeDVMDApplicationData", () => { +describe("decodeDVMDExtendedApplicationData", () => { it("correctly decodes the encoded data", () => { const encodedData = "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001000000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000"; diff --git a/packages/repository/src/db/connection.ts b/packages/repository/src/db/connection.ts index 0df58f4..8b0a352 100644 --- a/packages/repository/src/db/connection.ts +++ b/packages/repository/src/db/connection.ts @@ -1,8 +1,16 @@ -import { CamelCasePlugin, ColumnType, Kysely, PostgresDialect, WithSchemaPlugin } from "kysely"; +import { + CamelCasePlugin, + ColumnType, + Generated, + Kysely, + PostgresDialect, + WithSchemaPlugin, +} from "kysely"; import pg from "pg"; import { Application, + ApplicationPayout, Donation as DonationTable, MatchingDistribution, PendingProjectRole as PendingProjectRoleTable, @@ -37,6 +45,10 @@ type RoundTable = Omit & { >; }; +type ApplicationPayoutTable = Omit & { + id: Generated; +}; + export interface Database { rounds: RoundTable; pendingRoundRoles: PendingRoundRoleTable; @@ -46,6 +58,7 @@ export interface Database { projectRoles: ProjectRoleTable; applications: ApplicationTable; donations: DonationTable; + applicationsPayouts: ApplicationPayoutTable; } /** diff --git a/packages/repository/src/external.ts b/packages/repository/src/external.ts index 1313902..5ddb658 100644 --- a/packages/repository/src/external.ts +++ b/packages/repository/src/external.ts @@ -7,6 +7,7 @@ export type { IApplicationRepository, IApplicationReadRepository, IDonationRepository, + IApplicationPayoutRepository, DatabaseConfig, } from "./internal.js"; @@ -32,12 +33,15 @@ export type { export type { Donation, NewDonation } from "./types/index.js"; +export type { NewApplicationPayout, ApplicationPayout } from "./types/index.js"; + export type { Changeset, ProjectChangeset, RoundChangeset, ApplicationChangeset, DonationChangeset, + ApplicationPayoutChangeset, } from "./types/index.js"; export { @@ -45,6 +49,7 @@ export { KyselyProjectRepository, KyselyApplicationRepository, KyselyDonationRepository, + KyselyApplicationPayoutRepository, } from "./repositories/kysely/index.js"; export { diff --git a/packages/repository/src/interfaces/applicationPayoutRepository.interface.ts b/packages/repository/src/interfaces/applicationPayoutRepository.interface.ts new file mode 100644 index 0000000..6499187 --- /dev/null +++ b/packages/repository/src/interfaces/applicationPayoutRepository.interface.ts @@ -0,0 +1,10 @@ +import { NewApplicationPayout } from "../types/applicationPayout.types.js"; + +export interface IApplicationPayoutRepository { + /** + * Inserts a new application payout into the database. + * @param applicationPayout - The new application payout to insert. + * @returns A promise that resolves when the application payout is inserted. + */ + insertApplicationPayout(applicationPayout: NewApplicationPayout): Promise; +} diff --git a/packages/repository/src/interfaces/index.ts b/packages/repository/src/interfaces/index.ts index 73950fd..26a47f1 100644 --- a/packages/repository/src/interfaces/index.ts +++ b/packages/repository/src/interfaces/index.ts @@ -2,3 +2,4 @@ export * from "./projectRepository.interface.js"; export * from "./roundRepository.interface.js"; export * from "./applicationRepository.interface.js"; export * from "./donationRepository.interface.js"; +export * from "./applicationPayoutRepository.interface.js"; diff --git a/packages/repository/src/migrations/20241029T120000_initial.ts b/packages/repository/src/migrations/20241029T120000_initial.ts index 2c18c91..cdbeaef 100644 --- a/packages/repository/src/migrations/20241029T120000_initial.ts +++ b/packages/repository/src/migrations/20241029T120000_initial.ts @@ -225,7 +225,7 @@ export async function up(db: Kysely): Promise { .addColumn("amount", BIGINT_TYPE) .addColumn("tokenAddress", ADDRESS_TYPE) .addColumn("amountInUsd", CURRENCY_TYPE) - .addColumn("amountInRoundMatchToken", "text") + .addColumn("amountInRoundMatchToken", BIGINT_TYPE) .addColumn("transactionHash", "text") .addColumn("timestamp", "timestamptz") .addColumn("sender", ADDRESS_TYPE) diff --git a/packages/repository/src/repositories/kysely/applicationPayout.repository.ts b/packages/repository/src/repositories/kysely/applicationPayout.repository.ts new file mode 100644 index 0000000..b5d0196 --- /dev/null +++ b/packages/repository/src/repositories/kysely/applicationPayout.repository.ts @@ -0,0 +1,19 @@ +import { Kysely } from "kysely"; + +import { Database, IApplicationPayoutRepository, NewApplicationPayout } from "../../internal.js"; + +export class KyselyApplicationPayoutRepository implements IApplicationPayoutRepository { + constructor( + private readonly db: Kysely, + private readonly schemaName: string, + ) {} + + /** @inheritdoc */ + async insertApplicationPayout(applicationPayout: NewApplicationPayout): Promise { + await this.db + .withSchema(this.schemaName) + .insertInto("applicationsPayouts") + .values(applicationPayout) + .execute(); + } +} diff --git a/packages/repository/src/repositories/kysely/index.ts b/packages/repository/src/repositories/kysely/index.ts index b94e71f..6b5d977 100644 --- a/packages/repository/src/repositories/kysely/index.ts +++ b/packages/repository/src/repositories/kysely/index.ts @@ -2,3 +2,4 @@ export * from "./project.repository.js"; export * from "./round.repository.js"; export * from "./application.repository.js"; export * from "./donation.repository.js"; +export * from "./applicationPayout.repository.js"; diff --git a/packages/repository/src/types/applicationPayout.types.ts b/packages/repository/src/types/applicationPayout.types.ts new file mode 100644 index 0000000..ce2b46c --- /dev/null +++ b/packages/repository/src/types/applicationPayout.types.ts @@ -0,0 +1,17 @@ +import { Address, ChainId, Hex } from "@grants-stack-indexer/shared"; + +export type ApplicationPayout = { + id: number; + chainId: ChainId; + roundId: string; + applicationId: string; + amount: bigint; + tokenAddress: Address; + amountInUsd: string; + amountInRoundMatchToken: bigint; + transactionHash: Hex; + sender: Address; + timestamp: Date | null; +}; + +export type NewApplicationPayout = Omit; diff --git a/packages/repository/src/types/changeset.types.ts b/packages/repository/src/types/changeset.types.ts index 0ccce97..07cb2c0 100644 --- a/packages/repository/src/types/changeset.types.ts +++ b/packages/repository/src/types/changeset.types.ts @@ -1,5 +1,6 @@ import type { Address, ChainId } from "@grants-stack-indexer/shared"; +import { NewApplicationPayout } from "../internal.js"; import { NewApplication, PartialApplication } from "./application.types.js"; import { NewDonation } from "./donation.types.js"; import { @@ -159,10 +160,16 @@ export type DonationChangeset = }; }; -//TODO: add changeset for Donation and Payout tables +export type ApplicationPayoutChangeset = { + type: "InsertApplicationPayout"; + args: { + applicationPayout: NewApplicationPayout; + }; +}; export type Changeset = | ProjectChangeset | RoundChangeset | ApplicationChangeset - | DonationChangeset; + | DonationChangeset + | ApplicationPayoutChangeset; diff --git a/packages/repository/src/types/index.ts b/packages/repository/src/types/index.ts index afb0277..f573b25 100644 --- a/packages/repository/src/types/index.ts +++ b/packages/repository/src/types/index.ts @@ -3,3 +3,4 @@ export * from "./round.types.js"; export * from "./application.types.js"; export * from "./changeset.types.js"; export * from "./donation.types.js"; +export * from "./applicationPayout.types.js";