From bd5be45a8aa1d5c1849689755ec8f583122f473d Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Wed, 13 Nov 2024 19:18:33 -0300 Subject: [PATCH] feat: all remaining event handlers for DVMD strategy (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 🤖 Linear Closes GIT-170 GIT-155 GIT-156 GIT-160 GIT-167 ## Description - adds remaining Event Handlers for DVMD Direct Transfer Strategy - `TimestampsUpdated` - `DistributionUpdated` (common to all strategies) - `FundsDistributed` (common to all strategies) - `RecipientStatusUpdated` (common to all strategies) - `UpdatedRegistration` ## Checklist before requesting a review - [x] I have conducted a self-review of my code. - [x] I have conducted a QA. - [x] If it is a core feature, I have included comprehensive tests. --- apps/indexer/config.yaml | 8 + apps/indexer/src/handlers/Strategy.ts | 5 + .../test/unit/eventsProcessor.spec.ts | 2 +- .../data-flow/test/unit/orchestrator.spec.ts | 9 + packages/processors/package.json | 1 + packages/processors/src/constants/enums.ts | 6 + packages/processors/src/constants/index.ts | 1 + packages/processors/src/exceptions/index.ts | 5 +- .../exceptions/metadataNotFound.exception.ts | 6 + .../src/exceptions/unknownToken.exception.ts | 7 - packages/processors/src/internal.ts | 1 + .../registry/handlers/roleRevoked.handler.ts | 4 +- .../common/baseDistributed.handler.ts | 2 +- .../common/baseDistributionUpdated.handler.ts | 78 +++++ .../common/baseFundsDistributed.handler.ts | 81 +++++ .../baseRecipientStatusUpdated.handler.ts | 112 ++++++ .../src/processors/strategy/common/index.ts | 3 + .../dvmdDirectTransfer.handler.ts | 48 ++- .../handlers/allocated.handler.ts | 87 +---- .../handlers/index.ts | 2 + .../handlers/registered.handler.ts | 26 +- .../handlers/timestampsUpdated.handler.ts | 71 ++++ .../handlers/updatedRegistration.handler.ts | 106 ++++++ .../helpers/index.ts | 1 - .../types/index.ts | 5 +- .../strategy/helpers/applicationStatus.ts | 47 +++ .../helpers/decoder.ts | 29 +- .../src/processors/strategy/helpers/index.ts | 2 + .../src/processors/strategy/index.ts | 1 + packages/processors/src/schemas/index.ts | 1 + .../src/schemas/matchingDistribution.ts | 24 ++ .../handlers/roleRevoked.handler.spec.ts | 7 +- .../common/baseDistributed.handler.spec.ts | 2 +- .../baseDistributionUpdated.handler.spec.ts | 171 +++++++++ .../baseFundsDistributed.handler.spec.ts | 155 ++++++++ ...baseRecipientStatusUpdated.handler.spec.ts | 316 +++++++++++++++++ .../dvmdDirectTransfer.handler.spec.ts | 150 +++++++- .../handlers/allocated.handler.spec.ts | 98 ++++-- .../handlers/registered.handler.spec.ts | 35 +- .../timestampsUpdated.handler.spec.ts | 155 ++++++++ .../updatedRegistration.handler.spec.ts | 330 ++++++++++++++++++ .../helpers/decoder.spec.ts | 40 ++- packages/repository/src/db/connection.ts | 11 +- .../applicationNotFound.exception.ts | 0 packages/repository/src/exceptions/index.ts | 3 + .../exceptions/projectNotFound.exception.ts | 0 .../src/exceptions/roundNotFound.exception.ts | 0 packages/repository/src/external.ts | 7 + .../applicationRepository.interface.ts | 14 + .../interfaces/projectRepository.interface.ts | 9 + .../interfaces/roundRepository.interface.ts | 9 + packages/repository/src/internal.ts | 1 + .../kysely/application.repository.ts | 20 ++ .../repositories/kysely/project.repository.ts | 8 + .../repositories/kysely/round.repository.ts | 34 +- .../repository/src/types/application.types.ts | 2 +- packages/repository/src/types/round.types.ts | 2 +- packages/shared/src/external.ts | 2 +- packages/shared/src/tokens/tokens.ts | 14 +- packages/shared/src/types/events/strategy.ts | 124 ++++++- pnpm-lock.yaml | 252 +++++++++++++ 61 files changed, 2553 insertions(+), 199 deletions(-) create mode 100644 packages/processors/src/constants/enums.ts create mode 100644 packages/processors/src/constants/index.ts create mode 100644 packages/processors/src/exceptions/metadataNotFound.exception.ts delete mode 100644 packages/processors/src/exceptions/unknownToken.exception.ts create mode 100644 packages/processors/src/processors/strategy/common/baseDistributionUpdated.handler.ts create mode 100644 packages/processors/src/processors/strategy/common/baseFundsDistributed.handler.ts create mode 100644 packages/processors/src/processors/strategy/common/baseRecipientStatusUpdated.handler.ts create mode 100644 packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/timestampsUpdated.handler.ts create mode 100644 packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/updatedRegistration.handler.ts delete mode 100644 packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/index.ts create mode 100644 packages/processors/src/processors/strategy/helpers/applicationStatus.ts rename packages/processors/src/processors/strategy/{donationVotingMerkleDistributionDirectTransfer => }/helpers/decoder.ts (64%) create mode 100644 packages/processors/src/processors/strategy/helpers/index.ts create mode 100644 packages/processors/src/schemas/matchingDistribution.ts create mode 100644 packages/processors/test/strategy/common/baseDistributionUpdated.handler.spec.ts create mode 100644 packages/processors/test/strategy/common/baseFundsDistributed.handler.spec.ts create mode 100644 packages/processors/test/strategy/common/baseRecipientStatusUpdated.handler.spec.ts create mode 100644 packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/timestampsUpdated.handler.spec.ts create mode 100644 packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/updatedRegistration.handler.spec.ts rename packages/{processors => repository}/src/exceptions/applicationNotFound.exception.ts (100%) create mode 100644 packages/repository/src/exceptions/index.ts rename packages/{processors => repository}/src/exceptions/projectNotFound.exception.ts (100%) rename packages/{processors => repository}/src/exceptions/roundNotFound.exception.ts (100%) diff --git a/apps/indexer/config.yaml b/apps/indexer/config.yaml index f825a01..7e16494 100644 --- a/apps/indexer/config.yaml +++ b/apps/indexer/config.yaml @@ -86,6 +86,14 @@ contracts: - event: RecipientStatusUpdated(uint256 indexed rowIndex, uint256 fullRow, address sender) name: RecipientStatusUpdatedWithFullRow + # UpdatedRegistration + - event: UpdatedRegistration(address indexed recipientId, bytes data, address sender, uint8 status) + name: UpdatedRegistrationWithStatus + - event: UpdatedRegistration(address indexed recipientId, bytes data, address sender) + name: UpdatedRegistration + - event: UpdatedRegistration(address indexed recipientId, uint256 applicationId, bytes data, address sender, uint8 status) + name: UpdatedRegistrationWithApplicationId + # ########################## ALLO v2.1 ########################## # # AllocationExtension diff --git a/apps/indexer/src/handlers/Strategy.ts b/apps/indexer/src/handlers/Strategy.ts index 8367cc8..2c4d009 100644 --- a/apps/indexer/src/handlers/Strategy.ts +++ b/apps/indexer/src/handlers/Strategy.ts @@ -37,3 +37,8 @@ Strategy.DirectAllocated.handler(async ({}) => {}); Strategy.RecipientStatusUpdatedWithApplicationId.handler(async ({}) => {}); Strategy.RecipientStatusUpdatedWithRecipientStatus.handler(async ({}) => {}); Strategy.RecipientStatusUpdatedWithFullRow.handler(async ({}) => {}); + +// UpdatedRegistration Handlers +Strategy.UpdatedRegistrationWithStatus.handler(async ({}) => {}); +Strategy.UpdatedRegistration.handler(async ({}) => {}); +Strategy.UpdatedRegistrationWithApplicationId.handler(async ({}) => {}); diff --git a/packages/data-flow/test/unit/eventsProcessor.spec.ts b/packages/data-flow/test/unit/eventsProcessor.spec.ts index 51c4692..8d3ace1 100644 --- a/packages/data-flow/test/unit/eventsProcessor.spec.ts +++ b/packages/data-flow/test/unit/eventsProcessor.spec.ts @@ -105,7 +105,7 @@ describe("EventsProcessor", () => { sender: "0x0", recipientAddress: "0x0", recipientId: "0x0", - amount: 1n, + amount: "1", }, transactionFields: { hash: "0x0", diff --git a/packages/data-flow/test/unit/orchestrator.spec.ts b/packages/data-flow/test/unit/orchestrator.spec.ts index d625834..830b9fe 100644 --- a/packages/data-flow/test/unit/orchestrator.spec.ts +++ b/packages/data-flow/test/unit/orchestrator.spec.ts @@ -275,6 +275,15 @@ describe("Orchestrator", { sequential: true }, () => { AllocatedWithData: "", AllocatedWithVotes: "", AllocatedWithStatus: "", + TimestampsUpdatedWithRegistrationAndAllocation: "", + DistributionUpdated: "", + FundsDistributed: "", + RecipientStatusUpdatedWithApplicationId: "", + RecipientStatusUpdatedWithRecipientStatus: "", + RecipientStatusUpdatedWithFullRow: "", + UpdatedRegistrationWithStatus: "", + UpdatedRegistration: "", + UpdatedRegistrationWithApplicationId: "", }; for (const event of Object.keys(strategyEvents) as StrategyEvent[]) { diff --git a/packages/processors/package.json b/packages/processors/package.json index 97af619..3de9c99 100644 --- a/packages/processors/package.json +++ b/packages/processors/package.json @@ -33,6 +33,7 @@ "@grants-stack-indexer/pricing": "workspace:*", "@grants-stack-indexer/repository": "workspace:*", "@grants-stack-indexer/shared": "workspace:*", + "statuses-bitmap": "github:gitcoinco/statuses-bitmap#f123d7778e42e16adb98fff2b2ba18c0fee57227", "viem": "2.21.19", "zod": "3.23.8" } diff --git a/packages/processors/src/constants/enums.ts b/packages/processors/src/constants/enums.ts new file mode 100644 index 0000000..66dd328 --- /dev/null +++ b/packages/processors/src/constants/enums.ts @@ -0,0 +1,6 @@ +export enum ApplicationStatus { + NONE = 0, + PENDING, + APPROVED, + REJECTED, +} diff --git a/packages/processors/src/constants/index.ts b/packages/processors/src/constants/index.ts new file mode 100644 index 0000000..b741264 --- /dev/null +++ b/packages/processors/src/constants/index.ts @@ -0,0 +1 @@ +export * from "./enums.js"; diff --git a/packages/processors/src/exceptions/index.ts b/packages/processors/src/exceptions/index.ts index 8bd458f..9916a5b 100644 --- a/packages/processors/src/exceptions/index.ts +++ b/packages/processors/src/exceptions/index.ts @@ -2,8 +2,5 @@ export * from "./tokenPriceNotFound.exception.js"; export * from "./unsupportedEvent.exception.js"; export * from "./invalidArgument.exception.js"; export * from "./unsupportedStrategy.exception.js"; -export * from "./projectNotFound.exception.js"; -export * from "./roundNotFound.exception.js"; -export * from "./applicationNotFound.exception.js"; -export * from "./unknownToken.exception.js"; export * from "./metadataParsingFailed.exception.js"; +export * from "./metadataNotFound.exception.js"; diff --git a/packages/processors/src/exceptions/metadataNotFound.exception.ts b/packages/processors/src/exceptions/metadataNotFound.exception.ts new file mode 100644 index 0000000..04b4fd5 --- /dev/null +++ b/packages/processors/src/exceptions/metadataNotFound.exception.ts @@ -0,0 +1,6 @@ +export class MetadataNotFound extends Error { + constructor(message: string) { + super(message); + this.name = "MetadataNotFoundError"; + } +} diff --git a/packages/processors/src/exceptions/unknownToken.exception.ts b/packages/processors/src/exceptions/unknownToken.exception.ts deleted file mode 100644 index 3e29931..0000000 --- a/packages/processors/src/exceptions/unknownToken.exception.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ChainId } from "@grants-stack-indexer/shared"; - -export class UnknownToken extends Error { - constructor(tokenAddress: string, chainId?: ChainId) { - super(`Unknown token: ${tokenAddress} ${chainId ? `on chain ${chainId}` : ""}`); - } -} diff --git a/packages/processors/src/internal.ts b/packages/processors/src/internal.ts index 265ae03..71b7a6e 100644 --- a/packages/processors/src/internal.ts +++ b/packages/processors/src/internal.ts @@ -1,6 +1,7 @@ // Types and interfaces export * from "./types/index.js"; export * from "./interfaces/index.js"; +export * from "./constants/index.js"; // Exceptions export * from "./exceptions/index.js"; diff --git a/packages/processors/src/processors/registry/handlers/roleRevoked.handler.ts b/packages/processors/src/processors/registry/handlers/roleRevoked.handler.ts index a6154bc..c2d5d7b 100644 --- a/packages/processors/src/processors/registry/handlers/roleRevoked.handler.ts +++ b/packages/processors/src/processors/registry/handlers/roleRevoked.handler.ts @@ -1,9 +1,9 @@ import { getAddress } from "viem"; -import { Changeset } from "@grants-stack-indexer/repository"; +import { Changeset, ProjectByRoleNotFound } from "@grants-stack-indexer/repository"; import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; -import { IEventHandler, ProcessorDependencies, ProjectByRoleNotFound } from "../../../internal.js"; +import { IEventHandler, ProcessorDependencies } from "../../../internal.js"; type Dependencies = Pick; /** diff --git a/packages/processors/src/processors/strategy/common/baseDistributed.handler.ts b/packages/processors/src/processors/strategy/common/baseDistributed.handler.ts index d938130..ff82087 100644 --- a/packages/processors/src/processors/strategy/common/baseDistributed.handler.ts +++ b/packages/processors/src/processors/strategy/common/baseDistributed.handler.ts @@ -48,7 +48,7 @@ export class BaseDistributedHandler args: { chainId: this.chainId, roundId: round.id, - amount: this.event.params.amount, + amount: BigInt(this.event.params.amount), }, }, ]; diff --git a/packages/processors/src/processors/strategy/common/baseDistributionUpdated.handler.ts b/packages/processors/src/processors/strategy/common/baseDistributionUpdated.handler.ts new file mode 100644 index 0000000..e8b4c64 --- /dev/null +++ b/packages/processors/src/processors/strategy/common/baseDistributionUpdated.handler.ts @@ -0,0 +1,78 @@ +import { getAddress } from "viem"; + +import { Changeset } from "@grants-stack-indexer/repository"; +import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; + +import { + IEventHandler, + MetadataNotFound, + MetadataParsingFailed, + ProcessorDependencies, +} from "../../../internal.js"; +import { MatchingDistribution, MatchingDistributionSchema } from "../../../schemas/index.js"; + +type Dependencies = Pick; + +/** + * BaseDistributionUpdatedHandler: Processes 'DistributionUpdated' events + * + * - Decodes the updated distribution metadata + * - Creates a changeset to update the round with the new distribution + * - Serves as a base class as all strategies share the same logic for this event. + * + * @dev: + * - Strategy handlers that want to handle the DistributionUpdated event should create an instance of this class corresponding to the event. + * + */ + +export class BaseDistributionUpdatedHandler + implements IEventHandler<"Strategy", "DistributionUpdated"> +{ + constructor( + readonly event: ProcessorEvent<"Strategy", "DistributionUpdated">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + /* @inheritdoc */ + async handle(): Promise { + const { logger, metadataProvider } = this.dependencies; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, pointer] = this.event.params.metadata; + + const strategyAddress = getAddress(this.event.srcAddress); + const rawDistribution = await metadataProvider.getMetadata< + MatchingDistribution | undefined + >(pointer); + + if (!rawDistribution) { + logger.warn(`No matching distribution found for pointer: ${pointer}`); + + throw new MetadataNotFound(`No matching distribution found for pointer: ${pointer}`); + } + + const distribution = MatchingDistributionSchema.safeParse(rawDistribution); + + if (!distribution.success) { + logger.warn(`Failed to parse matching distribution: ${distribution.error.message}`); + + throw new MetadataParsingFailed( + `Failed to parse matching distribution: ${distribution.error.message}`, + ); + } + + return [ + { + type: "UpdateRoundByStrategyAddress", + args: { + chainId: this.chainId, + strategyAddress, + round: { + readyForPayoutTransaction: this.event.transactionFields.hash, + matchingDistribution: distribution.data.matchingDistribution, + }, + }, + }, + ]; + } +} diff --git a/packages/processors/src/processors/strategy/common/baseFundsDistributed.handler.ts b/packages/processors/src/processors/strategy/common/baseFundsDistributed.handler.ts new file mode 100644 index 0000000..ce5893a --- /dev/null +++ b/packages/processors/src/processors/strategy/common/baseFundsDistributed.handler.ts @@ -0,0 +1,81 @@ +import { getAddress } from "viem"; + +import { Changeset } from "@grants-stack-indexer/repository"; +import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; + +import { IEventHandler, ProcessorDependencies } from "../../../internal.js"; + +type Dependencies = Pick< + ProcessorDependencies, + "roundRepository" | "applicationRepository" | "logger" +>; + +/** + * BaseFundsDistributedHandler: Processes 'FundsDistributed' events + * + * - Handles funds distributed events across all strategies. + * - Creates two changesets: + * 1. UpdateApplication: Updates the application with the transaction hash. + * 2. IncrementRoundTotalDistributed: Increments the total distributed amount for a round. + * - Serves as a base class as all strategies share the same logic for this event. + * + * @dev: + * - Strategy handlers that want to handle the FundsDistributed event should create an instance of this class corresponding to the event. + * + */ + +export class BaseFundsDistributedHandler implements IEventHandler<"Strategy", "FundsDistributed"> { + constructor( + readonly event: ProcessorEvent<"Strategy", "FundsDistributed">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + /** + * Handles the FundsDistributed event. + * @throws {RoundNotFound} if the round is not found. + * @throws {ApplicationNotFound} if the application is not found. + * @returns An array of changesets with the following: + * 1. UpdateApplication: Updates the application with the transaction hash. + * 2. IncrementRoundTotalDistributed: Increments the total distributed amount for a round. + */ + async handle(): Promise { + const { roundRepository, applicationRepository } = this.dependencies; + + const strategyAddress = getAddress(this.event.srcAddress); + const round = await roundRepository.getRoundByStrategyAddressOrThrow( + this.chainId, + strategyAddress, + ); + + const roundId = round.id; + const anchorAddress = getAddress(this.event.params.recipientId); + const application = await applicationRepository.getApplicationByAnchorAddressOrThrow( + this.chainId, + roundId, + anchorAddress, + ); + + return [ + { + type: "UpdateApplication", + args: { + chainId: this.chainId, + roundId, + applicationId: application.id, + application: { + distributionTransaction: this.event.transactionFields.hash, + }, + }, + }, + { + type: "IncrementRoundTotalDistributed", + args: { + chainId: this.chainId, + roundId: round.id, + amount: BigInt(this.event.params.amount), + }, + }, + ]; + } +} diff --git a/packages/processors/src/processors/strategy/common/baseRecipientStatusUpdated.handler.ts b/packages/processors/src/processors/strategy/common/baseRecipientStatusUpdated.handler.ts new file mode 100644 index 0000000..0f8482c --- /dev/null +++ b/packages/processors/src/processors/strategy/common/baseRecipientStatusUpdated.handler.ts @@ -0,0 +1,112 @@ +import StatusesBitmap from "statuses-bitmap"; +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, isValidApplicationStatus } from "../helpers/index.js"; + +type Dependencies = Pick< + ProcessorDependencies, + "logger" | "roundRepository" | "applicationRepository" +>; + +type ApplicationUpdate = { + application: Application; + status: number; +}; + +/** + * BaseRecipientStatusUpdatedHandler: Processes 'RecipientStatusUpdated' events + * + * - Decodes a bitmap containing status updates for multiple applications + * - Validates each status is valid (between 1-3) + * - Creates changesets to update application statuses in bulk + * - Serves as a base class as all strategies share the same logic for this event + * + * @dev: + * - Strategy handlers that want to handle the RecipientStatusUpdated event should create an instance of this class corresponding to the event. + * + */ +export class BaseRecipientStatusUpdatedHandler + implements IEventHandler<"Strategy", "RecipientStatusUpdatedWithFullRow"> +{ + private readonly bitmap: StatusesBitmap; + + constructor( + readonly event: ProcessorEvent<"Strategy", "RecipientStatusUpdatedWithFullRow">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) { + this.bitmap = new StatusesBitmap(256n, 4n); + } + + /** + * Handles the RecipientStatusUpdated event by processing status updates for multiple applications. + * @returns An array of changesets to update application statuses. + */ + async handle(): Promise { + const { roundRepository } = this.dependencies; + + const strategyAddress = getAddress(this.event.srcAddress); + const round = await roundRepository.getRoundByStrategyAddressOrThrow( + this.chainId, + strategyAddress, + ); + + const applicationsToUpdate = await this.getApplicationsToUpdate(round.id); + + return applicationsToUpdate.map(({ application, status }) => { + const statusString = ApplicationStatus[status] as Application["status"]; + return { + type: "UpdateApplication", + args: { + chainId: this.chainId, + roundId: round.id, + applicationId: application.id, + application: createStatusUpdate({ + application, + newStatus: statusString, + blockNumber: this.event.blockNumber, + blockTimestamp: this.event.blockTimestamp, + }), + }, + }; + }); + } + + /** + * Gets the list of applications that need to be updated based on the bitmap row + * @param roundId - The ID of the round. + * @returns An array of application updates. + */ + private async getApplicationsToUpdate(roundId: string): Promise { + const { rowIndex, fullRow } = this.event.params; + this.bitmap.setRow(BigInt(rowIndex), BigInt(fullRow)); + + const startIndex = BigInt(rowIndex) * BigInt(this.bitmap.itemsPerRow); + const applications: { application: Application; status: number }[] = []; + + for (let i = startIndex; i < startIndex + this.bitmap.itemsPerRow; i++) { + const status = this.bitmap.getStatus(i); + if (isValidApplicationStatus(status)) { + const application = + await this.dependencies.applicationRepository.getApplicationById( + i.toString(), + this.chainId, + roundId, + ); + + if (application) { + applications.push({ + application, + status, + }); + } + } + } + + return applications; + } +} diff --git a/packages/processors/src/processors/strategy/common/index.ts b/packages/processors/src/processors/strategy/common/index.ts index 428bb60..a0cc165 100644 --- a/packages/processors/src/processors/strategy/common/index.ts +++ b/packages/processors/src/processors/strategy/common/index.ts @@ -1,2 +1,5 @@ export * from "./baseDistributed.handler.js"; export * from "./base.strategy.js"; +export * from "./baseDistributionUpdated.handler.js"; +export * from "./baseFundsDistributed.handler.js"; +export * from "./baseRecipientStatusUpdated.handler.js"; diff --git a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts index 81302eb..c39bc09 100644 --- a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts +++ b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.ts @@ -13,8 +13,19 @@ import type { ProcessorDependencies, StrategyTimings } from "../../../internal.j import DonationVotingMerkleDistributionDirectTransferStrategy from "../../../abis/allo-v2/v1/DonationVotingMerkleDistributionDirectTransferStrategy.js"; import { calculateAmountInUsd, getDateFromTimestamp } from "../../../helpers/index.js"; import { TokenPriceNotFoundError, UnsupportedEventException } from "../../../internal.js"; -import { BaseDistributedHandler, BaseStrategyHandler } from "../common/index.js"; -import { DVMDAllocatedHandler, DVMDRegisteredHandler } from "./handlers/index.js"; +import { + BaseDistributedHandler, + BaseDistributionUpdatedHandler, + BaseFundsDistributedHandler, + BaseRecipientStatusUpdatedHandler, + BaseStrategyHandler, +} from "../common/index.js"; +import { + DVMDAllocatedHandler, + DVMDRegisteredHandler, + DVMDTimestampsUpdatedHandler, + DVMDUpdatedRegistrationHandler, +} from "./handlers/index.js"; type Dependencies = Pick< ProcessorDependencies, @@ -70,6 +81,39 @@ export class DVMDDirectTransferStrategyHandler extends BaseStrategyHandler { this.chainId, this.dependencies, ).handle(); + case "TimestampsUpdatedWithRegistrationAndAllocation": + return new DVMDTimestampsUpdatedHandler( + event as ProcessorEvent< + "Strategy", + "TimestampsUpdatedWithRegistrationAndAllocation" + >, + this.chainId, + this.dependencies, + ).handle(); + case "DistributionUpdated": + return new BaseDistributionUpdatedHandler( + event as ProcessorEvent<"Strategy", "DistributionUpdated">, + this.chainId, + this.dependencies, + ).handle(); + case "FundsDistributed": + return new BaseFundsDistributedHandler( + event as ProcessorEvent<"Strategy", "FundsDistributed">, + this.chainId, + this.dependencies, + ).handle(); + case "UpdatedRegistrationWithStatus": + return new DVMDUpdatedRegistrationHandler( + event as ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus">, + this.chainId, + this.dependencies, + ).handle(); + case "RecipientStatusUpdatedWithFullRow": + return new BaseRecipientStatusUpdatedHandler( + event as ProcessorEvent<"Strategy", "RecipientStatusUpdatedWithFullRow">, + this.chainId, + this.dependencies, + ).handle(); default: throw new UnsupportedEventException("Strategy", event.eventName); } diff --git a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.ts b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.ts index 5960914..eeba5e4 100644 --- a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.ts +++ b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.ts @@ -1,16 +1,13 @@ -import { Address, encodePacked, getAddress, keccak256 } from "viem"; +import { encodePacked, getAddress, keccak256 } from "viem"; -import { Application, Changeset, Donation, Round } from "@grants-stack-indexer/repository"; -import { ChainId, getToken, ProcessorEvent, Token } from "@grants-stack-indexer/shared"; +import { Changeset, Donation } from "@grants-stack-indexer/repository"; +import { ChainId, getTokenOrThrow, ProcessorEvent } from "@grants-stack-indexer/shared"; import { getTokenAmountInUsd, getUsdInTokenAmount } from "../../../../helpers/index.js"; import { - ApplicationNotFound, IEventHandler, MetadataParsingFailed, ProcessorDependencies, - RoundNotFound, - UnknownToken, } from "../../../../internal.js"; import { ApplicationMetadata, ApplicationMetadataSchema } from "../../../../schemas/index.js"; @@ -46,16 +43,26 @@ export class DVMDAllocatedHandler implements IEventHandler<"Strategy", "Allocate * @throws {MetadataParsingFailed} if the metadata is invalid */ async handle(): Promise { + const { roundRepository, applicationRepository } = this.dependencies; const { srcAddress } = this.event; - const { recipientId: _recipientId, amount, token: _token } = this.event.params; + const { recipientId: _recipientId, amount: strAmount, token: _token } = this.event.params; - const round = await this.getRoundOrThrow(srcAddress); - const application = await this.getApplicationOrThrow(round.id, _recipientId); + const amount = BigInt(strAmount); + + const round = await roundRepository.getRoundByStrategyAddressOrThrow( + this.chainId, + getAddress(srcAddress), + ); + const application = await applicationRepository.getApplicationByAnchorAddressOrThrow( + this.chainId, + round.id, + getAddress(_recipientId), + ); const donationId = this.getDonationId(this.event.blockNumber, this.event.logIndex); - const token = this.getTokenOrThrow(_token); - const matchToken = this.getTokenOrThrow(round.matchTokenAddress); + const token = getTokenOrThrow(this.chainId, _token); + const matchToken = getTokenOrThrow(this.chainId, round.matchTokenAddress); const { amountInUsd, timestamp: priceTimestamp } = await getTokenAmountInUsd( this.dependencies.pricingProvider, @@ -103,52 +110,6 @@ export class DVMDAllocatedHandler implements IEventHandler<"Strategy", "Allocate ]; } - /** - * Retrieves a round by its strategy address. - * @param {Address} strategyAddress - The address of the strategy. - * @returns {Promise} The round found. - * @throws {RoundNotFound} if the round does not exist. - */ - private async getRoundOrThrow(strategyAddress: Address): Promise { - const normalizedStrategyAddress = getAddress(strategyAddress); - const round = await this.dependencies.roundRepository.getRoundByStrategyAddress( - this.chainId, - normalizedStrategyAddress, - ); - - if (!round) { - throw new RoundNotFound(this.chainId, normalizedStrategyAddress); - } - - return round; - } - - /** - * Retrieves an application by its round ID and recipient address. - * @param {string} roundId - The ID of the round. - * @param {Address} recipientId - The address of the recipient. - * @returns {Promise} The application found. - * @throws {ApplicationNotFound} if the application does not exist. - */ - private async getApplicationOrThrow( - roundId: string, - recipientId: Address, - ): Promise { - const normalizedRecipientId = getAddress(recipientId); - const application = - await this.dependencies.applicationRepository.getApplicationByAnchorAddress( - this.chainId, - roundId, - normalizedRecipientId, - ); - - if (!application) { - throw new ApplicationNotFound(this.chainId, roundId, normalizedRecipientId); - } - - return application; - } - /** * DONATION_ID = keccak256(abi.encodePacked(blockNumber, "-", logIndex)); */ @@ -156,18 +117,6 @@ export class DVMDAllocatedHandler implements IEventHandler<"Strategy", "Allocate return keccak256(encodePacked(["string"], [`${blockNumber}-${logIndex}`])); } - /** - * Retrieves a token by its address and chain ID. - * @param {Address} tokenAddress - The address of the token. - * @returns {Token} The token found. - * @throws {UnknownToken} if the token does not exist. - */ - private getTokenOrThrow(tokenAddress: Address): Token { - const token = getToken(this.chainId, getAddress(tokenAddress)); - if (!token) throw new UnknownToken(tokenAddress, this.chainId); - return token; - } - /** * Parses the application metadata. * @param {unknown} metadata - The metadata to parse. diff --git a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.ts b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.ts index 3072f31..565cf14 100644 --- a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.ts +++ b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.ts @@ -1,2 +1,4 @@ export * from "./allocated.handler.js"; export * from "./registered.handler.js"; +export * from "./timestampsUpdated.handler.js"; +export * from "./updatedRegistration.handler.js"; diff --git a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts index 9250ca6..902ba83 100644 --- a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts +++ b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.ts @@ -3,13 +3,8 @@ import { getAddress } from "viem"; import { Changeset, NewApplication } from "@grants-stack-indexer/repository"; import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; -import { - IEventHandler, - ProcessorDependencies, - ProjectNotFound, - RoundNotFound, -} from "../../../../internal.js"; -import { decodeDVMDApplicationData } from "../helpers/index.js"; +import { IEventHandler, ProcessorDependencies } from "../../../../internal.js"; +import { decodeDVMDExtendedApplicationData } from "../../helpers/index.js"; type Dependencies = Pick< ProcessorDependencies, @@ -41,23 +36,18 @@ export class DVMDRegisteredHandler implements IEventHandler<"Strategy", "Registe const { blockNumber, blockTimestamp } = this.event; const anchorAddress = getAddress(recipientId); - const project = await projectRepository.getProjectByAnchor(this.chainId, anchorAddress); - - if (!project) { - throw new ProjectNotFound(this.chainId, anchorAddress); - } + const project = await projectRepository.getProjectByAnchorOrThrow( + this.chainId, + anchorAddress, + ); const strategyAddress = getAddress(this.event.srcAddress); - const round = await roundRepository.getRoundByStrategyAddress( + const round = await roundRepository.getRoundByStrategyAddressOrThrow( this.chainId, strategyAddress, ); - if (!round) { - throw new RoundNotFound(this.chainId, strategyAddress); - } - - const values = decodeDVMDApplicationData(encodedData); + 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(); diff --git a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/timestampsUpdated.handler.ts b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/timestampsUpdated.handler.ts new file mode 100644 index 0000000..765339b --- /dev/null +++ b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/timestampsUpdated.handler.ts @@ -0,0 +1,71 @@ +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 Donation Voting Merkle Distribution Direct Transfer strategy. + * + * This handler processes updates to the round timestamps: + * - Validates the round exists for the strategy address + * - Converts the updated registration and allocation timestamps to dates + * - Returns a changeset to update the round's application and donation period timestamps + */ +export class DVMDTimestampsUpdatedHandler + implements IEventHandler<"Strategy", "TimestampsUpdatedWithRegistrationAndAllocation"> +{ + constructor( + readonly event: ProcessorEvent< + "Strategy", + "TimestampsUpdatedWithRegistrationAndAllocation" + >, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + /** + * Handles the TimestampsUpdated event for the Donation Voting Merkle Distribution Direct Transfer 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 { + registrationStartTime: strRegistrationStartTime, + registrationEndTime: strRegistrationEndTime, + allocationStartTime: strAllocationStartTime, + allocationEndTime: strAllocationEndTime, + } = this.event.params; + + const applicationsStartTime = getDateFromTimestamp(BigInt(strRegistrationStartTime)); + const applicationsEndTime = getDateFromTimestamp(BigInt(strRegistrationEndTime)); + const donationsStartTime = getDateFromTimestamp(BigInt(strAllocationStartTime)); + const donationsEndTime = getDateFromTimestamp(BigInt(strAllocationEndTime)); + + return [ + { + type: "UpdateRound", + args: { + chainId: this.chainId, + roundId: round.id, + round: { + applicationsStartTime, + applicationsEndTime, + donationsStartTime, + donationsEndTime, + }, + }, + }, + ]; + } +} diff --git a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/updatedRegistration.handler.ts b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/updatedRegistration.handler.ts new file mode 100644 index 0000000..e254c5d --- /dev/null +++ b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/updatedRegistration.handler.ts @@ -0,0 +1,106 @@ +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 Donation Voting Merkle Distribution Direct Transfer 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 DVMDUpdatedRegistrationHandler + implements IEventHandler<"Strategy", "UpdatedRegistrationWithStatus"> +{ + constructor( + readonly event: ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + /* @inheritdoc */ + 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( + `[DVMDUpdatedRegistrationHandler] 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/donationVotingMerkleDistributionDirectTransfer/helpers/index.ts b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/index.ts deleted file mode 100644 index 1616d7f..0000000 --- a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./decoder.js"; diff --git a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/types/index.ts b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/types/index.ts index 10f42ff..d24b04d 100644 --- a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/types/index.ts +++ b/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/types/index.ts @@ -1,7 +1,6 @@ import { Address } from "@grants-stack-indexer/shared"; export type DVMDApplicationData = { - recipientsCounter: string; anchorAddress: Address; recipientAddress: Address; metadata: { @@ -9,3 +8,7 @@ export type DVMDApplicationData = { pointer: string; }; }; + +export type DVMDExtendedApplicationData = DVMDApplicationData & { + recipientsCounter: string; +}; diff --git a/packages/processors/src/processors/strategy/helpers/applicationStatus.ts b/packages/processors/src/processors/strategy/helpers/applicationStatus.ts new file mode 100644 index 0000000..dd97127 --- /dev/null +++ b/packages/processors/src/processors/strategy/helpers/applicationStatus.ts @@ -0,0 +1,47 @@ +import { Application } from "@grants-stack-indexer/repository"; + +/** + * Checks if an application status index is valid (between 1 and 3) + * @see ApplicationStatus + */ +export function isValidApplicationStatus(status: number): boolean { + return status >= 1 && status <= 3; +} + +type StatusUpdateParams = { + application: Application; + newStatus: Application["status"]; + blockNumber: number; + blockTimestamp: number; +}; + +/** + * Creates a status update object for an application + * @param application - The application. + * @param newStatus - The new status. + * @param blockNumber - The block number. + * @param blockTimestamp - The block timestamp. + * @returns a Partial + */ +export function createStatusUpdate({ + application, + newStatus, + blockNumber, + blockTimestamp, +}: StatusUpdateParams): Pick { + const statusSnapshots = [...application.statusSnapshots]; + + if (application.status !== newStatus) { + statusSnapshots.push({ + status: newStatus, + updatedAtBlock: blockNumber.toString(), + updatedAt: new Date(blockTimestamp), + }); + } + + return { + status: newStatus, + statusUpdatedAtBlock: BigInt(blockNumber), + statusSnapshots, + }; +} diff --git a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.ts b/packages/processors/src/processors/strategy/helpers/decoder.ts similarity index 64% rename from packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.ts rename to packages/processors/src/processors/strategy/helpers/decoder.ts index f9378ac..6438187 100644 --- a/packages/processors/src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.ts +++ b/packages/processors/src/processors/strategy/helpers/decoder.ts @@ -1,8 +1,9 @@ import { decodeAbiParameters, Hex } from "viem"; -import { Address } from "@grants-stack-indexer/shared"; - -import { DVMDApplicationData } from "../types/index.js"; +import { + DVMDApplicationData, + DVMDExtendedApplicationData, +} from "../donationVotingMerkleDistributionDirectTransfer/types/index.js"; const DVMD_EVENT_DATA_DECODER = [ { name: "data", type: "bytes" }, @@ -23,14 +24,11 @@ const DVMD_DATA_DECODER = [ ] as const; export const decodeDVMDApplicationData = (encodedData: Hex): DVMDApplicationData => { - const values = decodeAbiParameters(DVMD_EVENT_DATA_DECODER, encodedData); - - const decodedData = decodeAbiParameters(DVMD_DATA_DECODER, values[0]); + const decodedData = decodeAbiParameters(DVMD_DATA_DECODER, encodedData); const results: DVMDApplicationData = { - recipientsCounter: values[1].toString(), - anchorAddress: decodedData[0] as Address, - recipientAddress: decodedData[1] as Address, + anchorAddress: decodedData[0], + recipientAddress: decodedData[1], metadata: { protocol: Number(decodedData[2].protocol), pointer: decodedData[2].pointer, @@ -39,3 +37,16 @@ export const decodeDVMDApplicationData = (encodedData: Hex): DVMDApplicationData return results; }; + +export const decodeDVMDExtendedApplicationData = ( + encodedData: Hex, +): DVMDExtendedApplicationData => { + const values = decodeAbiParameters(DVMD_EVENT_DATA_DECODER, encodedData); + + const encodededDVMD = decodeDVMDApplicationData(values[0]); + + return { + ...encodededDVMD, + recipientsCounter: values[1].toString(), + }; +}; diff --git a/packages/processors/src/processors/strategy/helpers/index.ts b/packages/processors/src/processors/strategy/helpers/index.ts new file mode 100644 index 0000000..1f38b97 --- /dev/null +++ b/packages/processors/src/processors/strategy/helpers/index.ts @@ -0,0 +1,2 @@ +export * from "./decoder.js"; +export * from "./applicationStatus.js"; diff --git a/packages/processors/src/processors/strategy/index.ts b/packages/processors/src/processors/strategy/index.ts index a98b82d..a0fecc1 100644 --- a/packages/processors/src/processors/strategy/index.ts +++ b/packages/processors/src/processors/strategy/index.ts @@ -1,4 +1,5 @@ export * from "./common/index.js"; +export * from "./donationVotingMerkleDistributionDirectTransfer/handlers/index.js"; export * from "./strategyHandler.factory.js"; export * from "./strategy.processor.js"; // Export mapping separately to avoid circular dependencies diff --git a/packages/processors/src/schemas/index.ts b/packages/processors/src/schemas/index.ts index f6e9268..92e489d 100644 --- a/packages/processors/src/schemas/index.ts +++ b/packages/processors/src/schemas/index.ts @@ -1,3 +1,4 @@ export * from "./projectMetadata.js"; export * from "./roundMetadata.js"; export * from "./applicationMetadata.js"; +export * from "./matchingDistribution.js"; diff --git a/packages/processors/src/schemas/matchingDistribution.ts b/packages/processors/src/schemas/matchingDistribution.ts new file mode 100644 index 0000000..a70bce7 --- /dev/null +++ b/packages/processors/src/schemas/matchingDistribution.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +export type MatchingDistribution = z.infer; + +const BigIntSchema = z.string().or( + z.object({ type: z.literal("BigNumber"), hex: z.string() }).transform((val) => { + return BigInt(val.hex).toString(); + }), +); + +export const MatchingDistributionSchema = z.object({ + matchingDistribution: z.array( + z.object({ + applicationId: z.string(), + projectPayoutAddress: z.string(), + projectId: z.string(), + projectName: z.string(), + matchPoolPercentage: z.number().or(z.string().min(1)).pipe(z.coerce.number()), + contributionsCount: z.number().or(z.string().min(1)).pipe(z.coerce.number()), + originalMatchAmountInToken: BigIntSchema.default("0"), + matchAmountInToken: BigIntSchema.default("0"), + }), + ), +}); diff --git a/packages/processors/test/registry/handlers/roleRevoked.handler.spec.ts b/packages/processors/test/registry/handlers/roleRevoked.handler.spec.ts index a602ad2..2acd684 100644 --- a/packages/processors/test/registry/handlers/roleRevoked.handler.spec.ts +++ b/packages/processors/test/registry/handlers/roleRevoked.handler.spec.ts @@ -1,9 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { IProjectRepository, Project } from "@grants-stack-indexer/repository"; +import { + IProjectRepository, + Project, + ProjectByRoleNotFound, +} from "@grants-stack-indexer/repository"; import { ChainId, ILogger, ProcessorEvent } from "@grants-stack-indexer/shared"; -import { ProjectByRoleNotFound } from "../../../src/internal.js"; import { RoleRevokedHandler } from "../../../src/processors/registry/handlers/roleRevoked.handler.js"; describe("RoleRevokedHandler", () => { diff --git a/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts b/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts index a774933..53c9410 100644 --- a/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts +++ b/packages/processors/test/strategy/common/baseDistributed.handler.spec.ts @@ -10,7 +10,7 @@ function createMockEvent( ): ProcessorEvent<"Strategy", "DistributedWithRecipientAddress"> { const defaultEvent: ProcessorEvent<"Strategy", "DistributedWithRecipientAddress"> = { params: { - amount: 1000n, + amount: "1000", recipientAddress: "0x1234567890123456789012345678901234567890", recipientId: "0x1234567890123456789012345678901234567890", sender: "0x1234567890123456789012345678901234567890", diff --git a/packages/processors/test/strategy/common/baseDistributionUpdated.handler.spec.ts b/packages/processors/test/strategy/common/baseDistributionUpdated.handler.spec.ts new file mode 100644 index 0000000..7772add --- /dev/null +++ b/packages/processors/test/strategy/common/baseDistributionUpdated.handler.spec.ts @@ -0,0 +1,171 @@ +import { getAddress } from "viem"; +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 { + 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); +} + +describe("BaseDistributionUpdatedHandler", () => { + let handler: BaseDistributionUpdatedHandler; + let mockMetadataProvider: IMetadataProvider; + let mockLogger: Logger; + let mockEvent: ProcessorEvent<"Strategy", "DistributionUpdated">; + const chainId = 10 as ChainId; + + beforeEach(() => { + 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 distribution update event", async () => { + mockEvent = createMockEvent(); + const mockDistribution = { + matchingDistribution: [ + { + applicationId: "app1", + projectPayoutAddress: "projectPayoutAddress", + projectId: "projectId", + projectName: "projectName", + matchPoolPercentage: 100, + contributionsCount: 100, + originalMatchAmountInToken: "9", + matchAmountInToken: "10", + }, + ], + }; + + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(mockDistribution); + + handler = new BaseDistributionUpdatedHandler(mockEvent, chainId, { + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + const result = await handler.handle(); + + expect(result).toEqual([ + { + type: "UpdateRoundByStrategyAddress", + args: { + chainId, + strategyAddress: getAddress(mockEvent.srcAddress), + round: { + readyForPayoutTransaction: mockEvent.transactionFields.hash, + matchingDistribution: mockDistribution.matchingDistribution, + }, + }, + }, + ]); + }); + + it("throws MetadataNotFound if distribution metadata is not found", async () => { + mockEvent = createMockEvent(); + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); + + handler = new BaseDistributionUpdatedHandler(mockEvent, chainId, { + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + await expect(handler.handle()).rejects.toThrow(MetadataNotFound); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("No matching distribution found for pointer:"), + ); + }); + + it("throw MatchingDistributionParsingError if distribution format is invalid", async () => { + mockEvent = createMockEvent(); + const invalidDistribution = { + matchingDistribution: [ + { + amount: "not_a_number", // Invalid amount format + applicationId: "app1", + recipientAddress: "0x1234567890123456789012345678901234567890", + }, + ], + }; + + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(invalidDistribution); + + handler = new BaseDistributionUpdatedHandler(mockEvent, chainId, { + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + await expect(handler.handle()).rejects.toThrow(MetadataParsingFailed); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("Failed to parse matching distribution:"), + ); + }); + + it("handles empty matching distribution array", async () => { + mockEvent = createMockEvent(); + const emptyDistribution = { + matchingDistribution: [], + }; + + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(emptyDistribution); + + handler = new BaseDistributionUpdatedHandler(mockEvent, chainId, { + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + const result = await handler.handle(); + expect(result).toHaveLength(1); + + const changeset = result[0] as { + type: "UpdateRoundByStrategyAddress"; + args: { + round: PartialRound; + }; + }; + expect(changeset.args.round.matchingDistribution).toEqual([]); + }); +}); diff --git a/packages/processors/test/strategy/common/baseFundsDistributed.handler.spec.ts b/packages/processors/test/strategy/common/baseFundsDistributed.handler.spec.ts new file mode 100644 index 0000000..cc330d7 --- /dev/null +++ b/packages/processors/test/strategy/common/baseFundsDistributed.handler.spec.ts @@ -0,0 +1,155 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + Application, + ApplicationNotFound, + IApplicationRepository, + IRoundReadRepository, + Round, + RoundNotFound, +} from "@grants-stack-indexer/repository"; +import { + ChainId, + DeepPartial, + Logger, + mergeDeep, + 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); +} + +describe("BaseFundsDistributedHandler", () => { + let handler: BaseFundsDistributedHandler; + let mockRoundRepository: IRoundReadRepository; + let mockApplicationRepository: IApplicationRepository; + let mockLogger: Logger; + let mockEvent: ProcessorEvent<"Strategy", "FundsDistributed">; + const chainId = 10 as ChainId; + + beforeEach(() => { + mockRoundRepository = { + getRoundByStrategyAddressOrThrow: vi.fn(), + } as unknown as IRoundReadRepository; + mockApplicationRepository = { + getApplicationByAnchorAddressOrThrow: vi.fn(), + } as unknown as IApplicationRepository; + mockLogger = { + warn: vi.fn(), + error: vi.fn(), + info: vi.fn(), + debug: vi.fn(), + } as unknown as Logger; + }); + + it("handles a valid funds distributed event", async () => { + mockEvent = createMockEvent(); + const mockRound = { id: "round1" } as unknown as Round; + const mockApplication = { id: "app1" } as unknown as Application; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); + + handler = new BaseFundsDistributedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + logger: mockLogger, + }); + + const result = await handler.handle(); + + expect(result).toEqual([ + { + type: "UpdateApplication", + args: { + chainId, + roundId: "round1", + applicationId: "app1", + application: { + distributionTransaction: mockEvent.transactionFields.hash, + }, + }, + }, + { + type: "IncrementRoundTotalDistributed", + args: { + chainId, + roundId: "round1", + amount: BigInt(mockEvent.params.amount), + }, + }, + ]); + }); + + it("throws RoundNotFound if round is not found", async () => { + mockEvent = createMockEvent(); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( + new RoundNotFound(chainId, mockEvent.strategyId), + ); + + handler = new BaseFundsDistributedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + logger: mockLogger, + }); + + await expect(handler.handle()).rejects.toThrow(RoundNotFound); + }); + + it("throws ApplicationNotFound if application is not found", async () => { + mockEvent = createMockEvent(); + const mockRound = { id: "round1" } 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 BaseFundsDistributedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + logger: mockLogger, + }); + + await expect(handler.handle()).rejects.toThrow(ApplicationNotFound); + }); +}); diff --git a/packages/processors/test/strategy/common/baseRecipientStatusUpdated.handler.spec.ts b/packages/processors/test/strategy/common/baseRecipientStatusUpdated.handler.spec.ts new file mode 100644 index 0000000..972c17b --- /dev/null +++ b/packages/processors/test/strategy/common/baseRecipientStatusUpdated.handler.spec.ts @@ -0,0 +1,316 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + Application, + IApplicationRepository, + IRoundReadRepository, + PartialApplication, + Round, + RoundNotFound, +} from "@grants-stack-indexer/repository"; +import { + ChainId, + DeepPartial, + Logger, + mergeDeep, + 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); +} + +describe("BaseRecipientStatusUpdatedHandler", () => { + let handler: BaseRecipientStatusUpdatedHandler; + let mockRoundRepository: IRoundReadRepository; + let mockApplicationRepository: IApplicationRepository; + let mockLogger: Logger; + let mockEvent: ProcessorEvent<"Strategy", "RecipientStatusUpdatedWithFullRow">; + const chainId = 10 as ChainId; + + beforeEach(() => { + mockRoundRepository = { + getRoundByStrategyAddressOrThrow: vi.fn(), + } as unknown as IRoundReadRepository; + mockApplicationRepository = { + getApplicationById: vi.fn(), + } as unknown as IApplicationRepository; + mockLogger = { + warn: vi.fn(), + error: vi.fn(), + info: vi.fn(), + debug: vi.fn(), + } as unknown as Logger; + }); + + it("handles valid status updates for multiple applications", async () => { + mockEvent = createMockEvent(); + const mockRound = { id: "round1" } as Round; + const mockApplication1 = { + id: "0", + status: "PENDING", + statusSnapshots: [], + statusUpdatedAtBlock: 12344n, + } as unknown as Application; + const mockApplication2 = { + id: "4", + status: "PENDING", + statusSnapshots: [], + statusUpdatedAtBlock: 12344n, + } as unknown as Application; + const mockApplication3 = { + id: "8", + status: "PENDING", + statusSnapshots: [], + statusUpdatedAtBlock: 12344n, + } as unknown as Application; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn(mockApplicationRepository, "getApplicationById") + .mockResolvedValueOnce(mockApplication1) + .mockResolvedValueOnce(mockApplication2) + .mockResolvedValueOnce(mockApplication3); + + handler = new BaseRecipientStatusUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + logger: mockLogger, + }); + + const result = await handler.handle(); + + expect(result).toHaveLength(3); + const changeset0 = result[0] as { + type: "UpdateApplication"; + args: { application: PartialApplication }; + }; + expect(changeset0).toEqual({ + type: "UpdateApplication", + args: { + chainId, + roundId: "round1", + applicationId: "0", + application: { + status: "PENDING", + statusUpdatedAtBlock: 12345n, + statusSnapshots: [], + }, + }, + }); + + const changeset1 = result[1] as { + type: "UpdateApplication"; + args: { application: PartialApplication }; + }; + expect(changeset1).toEqual({ + type: "UpdateApplication", + args: { + chainId, + roundId: "round1", + applicationId: "4", + application: { + status: "APPROVED", + statusUpdatedAtBlock: 12345n, + statusSnapshots: [ + { + status: "APPROVED", + updatedAtBlock: "12345", + updatedAt: new Date(mockEvent.blockTimestamp), + }, + ], + }, + }, + }); + + const changeset2 = result[2] as { + type: "UpdateApplication"; + args: { application: PartialApplication }; + }; + expect(changeset2).toEqual({ + type: "UpdateApplication", + args: { + chainId, + roundId: "round1", + applicationId: "8", + application: { + status: "REJECTED", + statusUpdatedAtBlock: 12345n, + statusSnapshots: [ + { + status: "REJECTED", + updatedAtBlock: "12345", + updatedAt: new Date(mockEvent.blockTimestamp), + }, + ], + }, + }, + }); + }); + + it("throws RoundNotFound if round is not found", async () => { + mockEvent = createMockEvent(); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( + new RoundNotFound(chainId, mockEvent.strategyId), + ); + + handler = new BaseRecipientStatusUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + logger: mockLogger, + }); + + await expect(handler.handle()).rejects.toThrow(RoundNotFound); + }); + + it("skips applications that are not found", async () => { + mockEvent = createMockEvent(); + const mockRound = { id: "round1" } as Round; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn(mockApplicationRepository, "getApplicationById").mockResolvedValue(undefined); + + handler = new BaseRecipientStatusUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + logger: mockLogger, + }); + + const result = await handler.handle(); + expect(result).toHaveLength(0); + }); + + it("skips invalid status values", async () => { + mockEvent = createMockEvent({ + params: { + rowIndex: "0", + fullRow: "96", // Binary: 1100000 (invalid statuses 6 and 7) + }, + }); + const mockRound = { id: "round1" } as Round; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + + handler = new BaseRecipientStatusUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + logger: mockLogger, + }); + + const result = await handler.handle(); + expect(result).toHaveLength(0); + }); + + 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) + const mockRound = { id: "round1" } as Round; + const mockApplication = { + id: "0", + status: "APPROVED", // Same as the new status + statusSnapshots: [ + { + status: "APPROVED", + updatedAtBlock: "12344", + updatedAt: new Date(1000000000), + }, + ], + statusUpdatedAtBlock: 12344n, + } as Application; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn(mockApplicationRepository, "getApplicationById").mockResolvedValue( + mockApplication, + ); + + handler = new BaseRecipientStatusUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + logger: mockLogger, + }); + + const result = await handler.handle(); + expect(result).toBeDefined(); + expect(result.length).toBe(1); + + const changeset = result[0] as { + type: "UpdateApplication"; + args: { application: PartialApplication }; + }; + expect(changeset.args.application.statusSnapshots).toHaveLength(1); + expect(changeset.args.application.statusSnapshots).toEqual(mockApplication.statusSnapshots); + }); + + it("handles different row indexes correctly", async () => { + mockEvent = createMockEvent({ + params: { + rowIndex: "1", // Second row + fullRow: "33", // 00100001 (status 1 at index 0, status 1 at index 4) + }, + }); + const mockRound = { id: "round1" } as Round; + const mockApplication = { + id: "64", // Index 0 in second row (64 = 1 * 64 + 0) + status: "PENDING", + statusSnapshots: [], + statusUpdatedAtBlock: 12344n, + } as unknown as Application; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn(mockApplicationRepository, "getApplicationById").mockResolvedValue( + mockApplication, + ); + + handler = new BaseRecipientStatusUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + logger: mockLogger, + }); + + const result = await handler.handle(); + expect(result).toHaveLength(2); + expect(mockApplicationRepository.getApplicationById).toHaveBeenCalledWith( + "64", + chainId, + "round1", + ); + const changeset1 = result[1] as { + type: "UpdateApplication"; + args: { application: PartialApplication }; + }; + expect(changeset1.args.application.status).toBe("APPROVED"); + }); +}); diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts index 540420f..ded3141 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.spec.ts @@ -19,11 +19,18 @@ import { } from "@grants-stack-indexer/shared"; import { TokenPriceNotFoundError, UnsupportedEventException } from "../../../src/internal.js"; -import { BaseDistributedHandler } from "../../../src/processors/strategy/common/index.js"; +import { + BaseDistributedHandler, + BaseDistributionUpdatedHandler, + BaseFundsDistributedHandler, + BaseRecipientStatusUpdatedHandler, +} from "../../../src/processors/strategy/common/index.js"; import { DVMDDirectTransferStrategyHandler } from "../../../src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js"; import { DVMDAllocatedHandler, DVMDRegisteredHandler, + DVMDTimestampsUpdatedHandler, + DVMDUpdatedRegistrationHandler, } from "../../../src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.js"; vi.mock( @@ -31,22 +38,50 @@ vi.mock( () => { const DVMDRegisteredHandler = vi.fn(); const DVMDAllocatedHandler = vi.fn(); + const DVMDTimestampsUpdatedHandler = vi.fn(); + const DVMDUpdatedRegistrationHandler = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access DVMDRegisteredHandler.prototype.handle = vi.fn(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access DVMDAllocatedHandler.prototype.handle = vi.fn(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + DVMDTimestampsUpdatedHandler.prototype.handle = vi.fn(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + DVMDUpdatedRegistrationHandler.prototype.handle = vi.fn(); + return { DVMDRegisteredHandler, DVMDAllocatedHandler, + DVMDTimestampsUpdatedHandler, + DVMDUpdatedRegistrationHandler, }; }, ); -vi.mock("../../../src/processors/strategy/common/baseDistributed.handler.js", () => { +vi.mock("../../../src/processors/strategy/common/index.js", async (importOriginal) => { + const original = + await importOriginal(); const BaseDistributedHandler = vi.fn(); + const BaseFundsDistributedHandler = vi.fn(); + const BaseDistributionUpdatedHandler = vi.fn(); + const BaseRecipientStatusUpdatedHandler = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access BaseDistributedHandler.prototype.handle = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + BaseFundsDistributedHandler.prototype.handle = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + BaseDistributionUpdatedHandler.prototype.handle = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + BaseRecipientStatusUpdatedHandler.prototype.handle = vi.fn(); return { + ...original, BaseDistributedHandler, + BaseFundsDistributedHandler, + BaseDistributionUpdatedHandler, + BaseRecipientStatusUpdatedHandler, }; }); @@ -161,6 +196,111 @@ describe("DVMDDirectTransferHandler", () => { expect(DVMDAllocatedHandler.prototype.handle).toHaveBeenCalled(); }); + it("calls TimestampsUpdatedHandler for TimestampsUpdated event", async () => { + const mockEvent = { + eventName: "TimestampsUpdatedWithRegistrationAndAllocation", + } as ProcessorEvent<"Strategy", "TimestampsUpdatedWithRegistrationAndAllocation">; + + vi.spyOn(DVMDTimestampsUpdatedHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(DVMDTimestampsUpdatedHandler).toHaveBeenCalledWith(mockEvent, mockChainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, + applicationRepository: mockApplicationRepository, + logger, + }); + expect(DVMDTimestampsUpdatedHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("calls FundsDistributedHandler for FundsDistributed event", async () => { + const mockEvent = { + eventName: "FundsDistributed", + } as ProcessorEvent<"Strategy", "FundsDistributed">; + + vi.spyOn(BaseFundsDistributedHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(BaseFundsDistributedHandler).toHaveBeenCalledWith(mockEvent, mockChainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, + applicationRepository: mockApplicationRepository, + logger, + }); + expect(BaseFundsDistributedHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("calls DistributionUpdatedHandler for DistributionUpdated event", async () => { + const mockEvent = { + eventName: "DistributionUpdated", + } as ProcessorEvent<"Strategy", "DistributionUpdated">; + + vi.spyOn(BaseDistributionUpdatedHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(BaseDistributionUpdatedHandler).toHaveBeenCalledWith(mockEvent, mockChainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, + applicationRepository: mockApplicationRepository, + logger, + }); + expect(BaseDistributionUpdatedHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("calls RecipientStatusUpdatedHandler for RecipientStatusUpdated 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, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, + applicationRepository: mockApplicationRepository, + logger, + }); + expect(BaseRecipientStatusUpdatedHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("calls UpdatedRegistrationHandler for UpdatedRegistration event", async () => { + const mockEvent = { + eventName: "UpdatedRegistrationWithStatus", + } as ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus">; + + vi.spyOn(DVMDUpdatedRegistrationHandler.prototype, "handle").mockResolvedValue([]); + + await handler.handle(mockEvent); + + expect(DVMDUpdatedRegistrationHandler).toHaveBeenCalledWith(mockEvent, mockChainId, { + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: mockProjectRepository, + evmProvider: mockEVMProvider, + pricingProvider: mockPricingProvider, + applicationRepository: mockApplicationRepository, + logger, + }); + expect(DVMDUpdatedRegistrationHandler.prototype.handle).toHaveBeenCalled(); + }); + describe("fetchMatchAmount", () => { it("fetches the correct match amount and USD value", async () => { const matchingFundsAvailable = 1000; @@ -262,12 +402,6 @@ describe("DVMDDirectTransferHandler", () => { }); }); - it.skip("calls TimestampsUpdatedHandler for TimestampsUpdated event"); - it.skip("calls RecipientStatusUpdatedHandler for RecipientStatusUpdated event"); - it.skip("calls DistributionUpdatedHandler for DistributionUpdated event"); - it.skip("calls UpdatedRegistrationHandler for UpdatedRegistration event"); - it.skip("calls FundsDistributedHandler for FundsDistributed event"); - it("throws UnsupportedEventException for unknown event names", async () => { const mockEvent = { eventName: "UnknownEvent" } as unknown as ProcessorEvent< "Strategy", 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 04360fd..b018b5f 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.spec.ts @@ -4,18 +4,23 @@ 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, DeepPartial, mergeDeep, ProcessorEvent } from "@grants-stack-indexer/shared"; +import { + ChainId, + DeepPartial, + mergeDeep, + ProcessorEvent, + UnknownToken, +} from "@grants-stack-indexer/shared"; import { - ApplicationNotFound, MetadataParsingFailed, - RoundNotFound, TokenPriceNotFoundError, - UnknownToken, } from "../../../../src/exceptions/index.js"; import { DVMDAllocatedHandler } from "../../../../src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/allocated.handler.js"; @@ -25,7 +30,7 @@ function createMockEvent( const defaultEvent: ProcessorEvent<"Strategy", "AllocatedWithOrigin"> = { params: { recipientId: "0x1234567890123456789012345678901234567890", - amount: 10n, + amount: "10", token: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", origin: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", sender: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", @@ -59,10 +64,10 @@ describe("DVMDAllocatedHandler", () => { beforeEach(() => { mockRoundRepository = { - getRoundByStrategyAddress: vi.fn(), + getRoundByStrategyAddressOrThrow: vi.fn(), } as unknown as IRoundRepository; mockApplicationRepository = { - getApplicationByAnchorAddress: vi.fn(), + getApplicationByAnchorAddressOrThrow: vi.fn(), } as unknown as IApplicationRepository; mockPricingProvider = { getTokenPrice: vi.fn(), @@ -70,7 +75,7 @@ describe("DVMDAllocatedHandler", () => { }); it("handle a valid allocated event", async () => { - const amount = parseEther("10"); + const amount = parseEther("10").toString(); mockEvent = createMockEvent({ params: { amount } }); const mockRound = { id: "round1", @@ -87,10 +92,13 @@ describe("DVMDAllocatedHandler", () => { projectId: "project1", } as unknown as Application; - vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); - vi.spyOn(mockApplicationRepository, "getApplicationByAnchorAddress").mockResolvedValue( - mockApplication, + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue({ timestampMs: 1000000000, priceUsd: 2000, @@ -119,9 +127,9 @@ describe("DVMDAllocatedHandler", () => { transactionHash: mockEvent.transactionFields.hash, blockNumber: BigInt(mockEvent.blockNumber), tokenAddress: getAddress("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"), - amount: amount, + amount: BigInt(amount), amountInUsd: "20000", - amountInRoundMatchToken: amount, + amountInRoundMatchToken: BigInt(amount), timestamp: new Date(1000000000), }, }, @@ -130,7 +138,7 @@ describe("DVMDAllocatedHandler", () => { }); it("match token is different from event token", async () => { - const amount = parseEther("1500"); + const amount = parseEther("1500").toString(); mockEvent = createMockEvent({ params: { amount } }); const mockRound = { id: "round1", @@ -147,10 +155,13 @@ describe("DVMDAllocatedHandler", () => { projectId: "project1", } as unknown as Application; - vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); - vi.spyOn(mockApplicationRepository, "getApplicationByAnchorAddress").mockResolvedValue( - mockApplication, + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); vi.spyOn(mockPricingProvider, "getTokenPrice") .mockResolvedValueOnce({ timestampMs: 1000000000, @@ -176,7 +187,7 @@ describe("DVMDAllocatedHandler", () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment donation: expect.objectContaining({ tokenAddress: getAddress("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"), - amount: amount, + amount: BigInt(amount), amountInUsd: "1500", amountInRoundMatchToken: parseEther("0.75"), timestamp: new Date(1000000000), @@ -188,7 +199,9 @@ describe("DVMDAllocatedHandler", () => { it("throws RoundNotFound if round is not found", async () => { mockEvent = createMockEvent(); - vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(undefined); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( + new RoundNotFound(chainId, mockEvent.strategyId), + ); handler = new DVMDAllocatedHandler(mockEvent, chainId, { roundRepository: mockRoundRepository, @@ -206,9 +219,14 @@ describe("DVMDAllocatedHandler", () => { matchTokenAddress: "0x0987654321098765432109876543210987654321", } as unknown as Round; - vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); - vi.spyOn(mockApplicationRepository, "getApplicationByAnchorAddress").mockResolvedValue( - undefined, + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockRejectedValue( + new ApplicationNotFound(chainId, mockRound.id, mockEvent.params.recipientId), ); handler = new DVMDAllocatedHandler(mockEvent, chainId, { @@ -239,10 +257,13 @@ describe("DVMDAllocatedHandler", () => { projectId: "project1", } as unknown as Application; - vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); - vi.spyOn(mockApplicationRepository, "getApplicationByAnchorAddress").mockResolvedValue( - mockApplication, + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); handler = new DVMDAllocatedHandler(mockEvent, chainId, { roundRepository: mockRoundRepository, @@ -270,10 +291,13 @@ describe("DVMDAllocatedHandler", () => { projectId: "project1", } as unknown as Application; - vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); - vi.spyOn(mockApplicationRepository, "getApplicationByAnchorAddress").mockResolvedValue( - mockApplication, + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); handler = new DVMDAllocatedHandler(mockEvent, chainId, { roundRepository: mockRoundRepository, @@ -301,10 +325,13 @@ describe("DVMDAllocatedHandler", () => { projectId: "project1", } as unknown as Application; - vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); - vi.spyOn(mockApplicationRepository, "getApplicationByAnchorAddress").mockResolvedValue( - mockApplication, + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue(undefined); handler = new DVMDAllocatedHandler(mockEvent, chainId, { @@ -333,10 +360,13 @@ describe("DVMDAllocatedHandler", () => { projectId: "project1", } as unknown as Application; - vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); - vi.spyOn(mockApplicationRepository, "getApplicationByAnchorAddress").mockResolvedValue( - mockApplication, + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, ); + vi.spyOn( + mockApplicationRepository, + "getApplicationByAnchorAddressOrThrow", + ).mockResolvedValue(mockApplication); vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue({ timestampMs: 1000000000, priceUsd: 2000, 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 74bf6e0..2c9a531 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/registered.handler.spec.ts @@ -6,11 +6,12 @@ import { IRoundReadRepository, NewApplication, Project, + ProjectNotFound, Round, + RoundNotFound, } from "@grants-stack-indexer/repository"; import { ChainId, DeepPartial, mergeDeep, ProcessorEvent } from "@grants-stack-indexer/shared"; -import { ProjectNotFound, RoundNotFound } from "../../../../src/exceptions/index.js"; import { DVMDRegisteredHandler } from "../../../../src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.js"; function createMockEvent( @@ -50,10 +51,10 @@ describe("DVMDRegisteredHandler", () => { beforeEach(() => { mockRoundRepository = { - getRoundByStrategyAddress: vi.fn(), + getRoundByStrategyAddressOrThrow: vi.fn(), } as unknown as IRoundReadRepository; mockProjectRepository = { - getProjectByAnchor: vi.fn(), + getProjectByAnchorOrThrow: vi.fn(), } as unknown as IProjectReadRepository; mockMetadataProvider = { getMetadata: vi.fn(), @@ -66,8 +67,10 @@ describe("DVMDRegisteredHandler", () => { const mockRound = { id: "round1" } as Round; const mockMetadata = { name: "Test Project" }; - vi.spyOn(mockProjectRepository, "getProjectByAnchor").mockResolvedValue(mockProject); - vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(mockMetadata); handler = new DVMDRegisteredHandler(mockEvent, chainId, { @@ -112,7 +115,9 @@ describe("DVMDRegisteredHandler", () => { it("throw ProjectNotFound if project is not found", async () => { mockEvent = createMockEvent(); - vi.spyOn(mockProjectRepository, "getProjectByAnchor").mockResolvedValue(undefined); + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockRejectedValue( + new ProjectNotFound(chainId, mockEvent.srcAddress), + ); handler = new DVMDRegisteredHandler(mockEvent, chainId, { projectRepository: mockProjectRepository, @@ -125,8 +130,10 @@ describe("DVMDRegisteredHandler", () => { it("throw RoundNotFound if round is not found", async () => { mockEvent = createMockEvent(); const mockProject = { id: "project1" } as Project; - vi.spyOn(mockProjectRepository, "getProjectByAnchor").mockResolvedValue(mockProject); - vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(undefined); + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( + new RoundNotFound(chainId, mockEvent.strategyId), + ); handler = new DVMDRegisteredHandler(mockEvent, chainId, { projectRepository: mockProjectRepository, @@ -141,8 +148,10 @@ describe("DVMDRegisteredHandler", () => { const mockProject = { id: "project1" } as Project; const mockRound = { id: "round1" } as Round; - vi.spyOn(mockProjectRepository, "getProjectByAnchor").mockResolvedValue(mockProject); - vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); handler = new DVMDRegisteredHandler(mockEvent, chainId, { @@ -167,8 +176,10 @@ describe("DVMDRegisteredHandler", () => { const mockProject = { id: "project1" } as Project; const mockRound = { id: "round1" } as Round; - vi.spyOn(mockProjectRepository, "getProjectByAnchor").mockResolvedValue(mockProject); - vi.spyOn(mockRoundRepository, "getRoundByStrategyAddress").mockResolvedValue(mockRound); + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); handler = new DVMDRegisteredHandler(mockEvent, chainId, { projectRepository: mockProjectRepository, diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/timestampsUpdated.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/timestampsUpdated.handler.spec.ts new file mode 100644 index 0000000..d928878 --- /dev/null +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/timestampsUpdated.handler.spec.ts @@ -0,0 +1,155 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + IRoundReadRepository, + PartialRound, + Round, + RoundNotFound, +} from "@grants-stack-indexer/repository"; +import { ChainId, DeepPartial, mergeDeep, 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); +} + +describe("DVMDTimestampsUpdatedHandler", () => { + let handler: DVMDTimestampsUpdatedHandler; + let mockRoundRepository: IRoundReadRepository; + let mockEvent: ProcessorEvent<"Strategy", "TimestampsUpdatedWithRegistrationAndAllocation">; + const chainId = 10 as ChainId; + + beforeEach(() => { + mockRoundRepository = { + getRoundByStrategyAddressOrThrow: vi.fn(), + } as unknown as IRoundReadRepository; + }); + + it("handle a valid timestamps update event", async () => { + const timestamps = { + registrationStartTime: "1704067200", // 2024-01-01 00:00:00 + registrationEndTime: "1704153600", // 2024-01-02 00:00:00 + allocationStartTime: "1704240000", // 2024-01-03 00:00:00 + allocationEndTime: "1704326400", // 2024-01-04 00:00:00 + }; + + mockEvent = createMockEvent({ + params: timestamps, + }); + const mockRound = { id: "round1" } as Round; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + + handler = new DVMDTimestampsUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + + expect(result.length).toBe(1); + const changeset = result[0] as { + type: "UpdateRound"; + args: { chainId: ChainId; roundId: string; round: PartialRound }; + }; + + expect(changeset.type).toBe("UpdateRound"); + expect(changeset.args.chainId).toBe(chainId); + expect(changeset.args.roundId).toBe("round1"); + expect(changeset.args.round).toBeDefined(); + + const partialRound = changeset.args.round; + + expect(partialRound.applicationsStartTime).toEqual(new Date("2024-01-01T00:00:00.000Z")); + expect(partialRound.applicationsEndTime).toEqual(new Date("2024-01-02T00:00:00.000Z")); + expect(partialRound.donationsStartTime).toEqual(new Date("2024-01-03T00:00:00.000Z")); + expect(partialRound.donationsEndTime).toEqual(new Date("2024-01-04T00:00:00.000Z")); + }); + + it("throws RoundNotFound if round is not found", async () => { + mockEvent = createMockEvent(); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( + new RoundNotFound(chainId, mockEvent.strategyId), + ); + + handler = new DVMDTimestampsUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + }); + + await expect(handler.handle()).rejects.toThrow(RoundNotFound); + }); + + it("correctly convert timestamps to Date objects", async () => { + const timestamps = { + registrationStartTime: "1704067200", // 2024-01-01 00:00:00 + registrationEndTime: "1704153600", // 2024-01-02 00:00:00 + allocationStartTime: "1704240000", // 2024-01-03 00:00:00 + allocationEndTime: "1704326400", // 2024-01-04 00:00:00 + }; + + mockEvent = createMockEvent({ + params: timestamps, + }); + const mockRound = { id: "round1" } as Round; + + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockResolvedValue( + mockRound, + ); + + handler = new DVMDTimestampsUpdatedHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + + expect(result.length).toBe(1); + const changeset = result[0] as { + type: "UpdateRound"; + args: { chainId: ChainId; roundId: string; round: PartialRound }; + }; + + expect(changeset.type).toBe("UpdateRound"); + expect(changeset.args.chainId).toBe(chainId); + expect(changeset.args.roundId).toBe("round1"); + expect(changeset.args.round).toBeDefined(); + + const partialRound = changeset.args.round; + + expect(partialRound.applicationsStartTime).toEqual(new Date("2024-01-01T00:00:00.000Z")); + expect(partialRound.applicationsEndTime).toEqual(new Date("2024-01-02T00:00:00.000Z")); + expect(partialRound.donationsStartTime).toEqual(new Date("2024-01-03T00:00:00.000Z")); + expect(partialRound.donationsEndTime).toEqual(new Date("2024-01-04T00:00:00.000Z")); + }); +}); diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/updatedRegistration.handler.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/updatedRegistration.handler.spec.ts new file mode 100644 index 0000000..55be75a --- /dev/null +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/updatedRegistration.handler.spec.ts @@ -0,0 +1,330 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import { + Application, + ApplicationNotFound, + IApplicationRepository, + IProjectRepository, + IRoundReadRepository, + PartialApplication, + Project, + ProjectNotFound, + Round, + RoundNotFound, +} from "@grants-stack-indexer/repository"; +import { + ChainId, + DeepPartial, + Logger, + mergeDeep, + 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); +} + +describe("DVMDUpdatedRegistrationHandler", () => { + let handler: DVMDUpdatedRegistrationHandler; + let mockRoundRepository: IRoundReadRepository; + let mockApplicationRepository: IApplicationRepository; + let mockProjectRepository: IProjectRepository; + let mockMetadataProvider: IMetadataProvider; + let mockLogger: Logger; + let mockEvent: ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus">; + const chainId = 10 as ChainId; + + beforeEach(() => { + mockRoundRepository = { + getRoundByStrategyAddressOrThrow: vi.fn(), + } as unknown as IRoundReadRepository; + 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({ params: { status: "2" } }); + 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 DVMDUpdatedRegistrationHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + const result = await handler.handle(); + + expect(result).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 if status is invalid", async () => { + const invalidStatuses = ["0", "4", "10"]; + for (const status of invalidStatuses) { + mockEvent = createMockEvent({ params: { status } }); + + handler = new DVMDUpdatedRegistrationHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + const result = await handler.handle(); + + expect(result).toEqual([]); + expect(mockLogger.warn).toHaveBeenCalledWith( + `[DVMDUpdatedRegistrationHandler] Invalid status: ${mockEvent.params.status}`, + ); + } + }); + + it("throws ProjectNotFound if project is not found", async () => { + mockEvent = createMockEvent(); + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockRejectedValue( + new ProjectNotFound(chainId, mockEvent.params.recipientId), + ); + + handler = new DVMDUpdatedRegistrationHandler(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(); + const mockProject = { + id: "project1", + anchorAddress: mockEvent.params.recipientId, + } as Project; + + vi.spyOn(mockProjectRepository, "getProjectByAnchorOrThrow").mockResolvedValue(mockProject); + vi.spyOn(mockRoundRepository, "getRoundByStrategyAddressOrThrow").mockRejectedValue( + new RoundNotFound(chainId, mockEvent.strategyId), + ); + + handler = new DVMDUpdatedRegistrationHandler(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(); + 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 DVMDUpdatedRegistrationHandler(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(); + 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 DVMDUpdatedRegistrationHandler(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({ 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 status as in the event + 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 DVMDUpdatedRegistrationHandler(mockEvent, chainId, { + roundRepository: mockRoundRepository, + applicationRepository: mockApplicationRepository, + projectRepository: mockProjectRepository, + metadataProvider: mockMetadataProvider, + logger: mockLogger, + }); + + const result = await handler.handle(); + + expect(result).toHaveLength(1); + const changeset = result[0] as { + type: "UpdateApplication"; + args: { application: PartialApplication }; + }; + expect(changeset.args.application.statusSnapshots).toHaveLength(1); + expect(changeset.args.application.status).toBe("PENDING"); + expect(changeset.args.application.statusSnapshots).toEqual(mockApplication.statusSnapshots); + }); +}); diff --git a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.spec.ts b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.spec.ts index 7bfdfc7..5d583ed 100644 --- a/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.spec.ts +++ b/packages/processors/test/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.spec.ts @@ -1,14 +1,20 @@ import { describe, expect, it } from "vitest"; -import { decodeDVMDApplicationData } from "../../../../src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/helpers/decoder.js"; -import { DVMDApplicationData } from "../../../../src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/types/index.js"; +import { + DVMDApplicationData, + DVMDExtendedApplicationData, +} from "../../../../src/processors/strategy/donationVotingMerkleDistributionDirectTransfer/types/index.js"; +import { + decodeDVMDApplicationData, + decodeDVMDExtendedApplicationData, +} from "../../../../src/processors/strategy/helpers/index.js"; describe("decodeDVMDApplicationData", () => { it("correctly decodes the encoded data", () => { const encodedData = "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001000000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000"; - const expectedResult: DVMDApplicationData = { + const expectedResult: DVMDExtendedApplicationData = { recipientsCounter: "1", anchorAddress: "0x2c7296a5eC0539f0A018C7176c97c92A9C44E2B4", recipientAddress: "0xE7eB5D2b5b188777df902e89c54570E7Ef4F59CE", @@ -18,6 +24,32 @@ describe("decodeDVMDApplicationData", () => { }, }; + const result = decodeDVMDExtendedApplicationData(encodedData); + + expect(result).toEqual(expectedResult); + }); + + it("throw an error for invalid encoded data", () => { + const invalidEncodedData = "0x1234"; + + expect(() => decodeDVMDExtendedApplicationData(invalidEncodedData)).toThrow(); + }); +}); + +describe("decodeDVMDApplicationData", () => { + it("correctly decodes the encoded data", () => { + const encodedData = + "0x0000000000000000000000002c7296a5ec0539f0a018c7176c97c92a9c44e2b4000000000000000000000000e7eb5d2b5b188777df902e89c54570e7ef4f59ce000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003b6261666b72656967796334336366696e786c6e6168713561617773676869626574763675737273376b6b78663776786d7a626a79726f37366977790000000000"; + + const expectedResult: DVMDApplicationData = { + anchorAddress: "0x2c7296a5eC0539f0A018C7176c97c92A9C44E2B4", + recipientAddress: "0xE7eB5D2b5b188777df902e89c54570E7Ef4F59CE", + metadata: { + protocol: 1, + pointer: "bafkreigyc43cfinxlnahq5aawsghibetv6usrs7kkxf7vxmzbjyro76iwy", + }, + }; + const result = decodeDVMDApplicationData(encodedData); expect(result).toEqual(expectedResult); @@ -26,6 +58,6 @@ describe("decodeDVMDApplicationData", () => { it("throw an error for invalid encoded data", () => { const invalidEncodedData = "0x1234"; - expect(() => decodeDVMDApplicationData(invalidEncodedData)).toThrow(); + expect(() => decodeDVMDExtendedApplicationData(invalidEncodedData)).toThrow(); }); }); diff --git a/packages/repository/src/db/connection.ts b/packages/repository/src/db/connection.ts index 2da393a..0df58f4 100644 --- a/packages/repository/src/db/connection.ts +++ b/packages/repository/src/db/connection.ts @@ -4,12 +4,13 @@ import pg from "pg"; import { Application, Donation as DonationTable, + MatchingDistribution, PendingProjectRole as PendingProjectRoleTable, PendingRoundRole as PendingRoundRoleTable, ProjectRole as ProjectRoleTable, Project as ProjectTable, + Round, RoundRole as RoundRoleTable, - Round as RoundTable, StatusSnapshot, } from "../internal.js"; @@ -28,6 +29,14 @@ type ApplicationTable = Omit & { >; }; +type RoundTable = Omit & { + matchingDistribution: ColumnType< + MatchingDistribution[] | null, + MatchingDistribution[] | string | null, + MatchingDistribution[] | string | null + >; +}; + export interface Database { rounds: RoundTable; pendingRoundRoles: PendingRoundRoleTable; diff --git a/packages/processors/src/exceptions/applicationNotFound.exception.ts b/packages/repository/src/exceptions/applicationNotFound.exception.ts similarity index 100% rename from packages/processors/src/exceptions/applicationNotFound.exception.ts rename to packages/repository/src/exceptions/applicationNotFound.exception.ts diff --git a/packages/repository/src/exceptions/index.ts b/packages/repository/src/exceptions/index.ts new file mode 100644 index 0000000..710e180 --- /dev/null +++ b/packages/repository/src/exceptions/index.ts @@ -0,0 +1,3 @@ +export * from "./roundNotFound.exception.js"; +export * from "./applicationNotFound.exception.js"; +export * from "./projectNotFound.exception.js"; diff --git a/packages/processors/src/exceptions/projectNotFound.exception.ts b/packages/repository/src/exceptions/projectNotFound.exception.ts similarity index 100% rename from packages/processors/src/exceptions/projectNotFound.exception.ts rename to packages/repository/src/exceptions/projectNotFound.exception.ts diff --git a/packages/processors/src/exceptions/roundNotFound.exception.ts b/packages/repository/src/exceptions/roundNotFound.exception.ts similarity index 100% rename from packages/processors/src/exceptions/roundNotFound.exception.ts rename to packages/repository/src/exceptions/roundNotFound.exception.ts diff --git a/packages/repository/src/external.ts b/packages/repository/src/external.ts index 3568aaa..1313902 100644 --- a/packages/repository/src/external.ts +++ b/packages/repository/src/external.ts @@ -47,6 +47,13 @@ export { KyselyDonationRepository, } from "./repositories/kysely/index.js"; +export { + RoundNotFound, + ApplicationNotFound, + ProjectNotFound, + ProjectByRoleNotFound, +} from "./internal.js"; + export { createKyselyPostgresDb as createKyselyDatabase } from "./internal.js"; export { migrateToLatest, resetDatabase } from "./db/index.js"; diff --git a/packages/repository/src/interfaces/applicationRepository.interface.ts b/packages/repository/src/interfaces/applicationRepository.interface.ts index 627fe8c..2eb2f60 100644 --- a/packages/repository/src/interfaces/applicationRepository.interface.ts +++ b/packages/repository/src/interfaces/applicationRepository.interface.ts @@ -42,6 +42,20 @@ export interface IApplicationReadRepository { anchorAddress: Address, ): Promise; + /** + * Retrieves a specific application by its chain ID, round ID, and anchor address. + * @param chainId The chain ID of the application. + * @param roundId The round ID of the application. + * @param anchorAddress The anchor address of the application. + * @returns A promise that resolves to an Application object + * @throws {ApplicationNotFound} if the application does not exist + */ + getApplicationByAnchorAddressOrThrow( + chainId: ChainId, + roundId: string, + anchorAddress: Address, + ): Promise; + /** * Retrieves all applications for a given chain ID and round ID. * @param chainId The chain ID of the applications. diff --git a/packages/repository/src/interfaces/projectRepository.interface.ts b/packages/repository/src/interfaces/projectRepository.interface.ts index 717bfd3..a3dce7d 100644 --- a/packages/repository/src/interfaces/projectRepository.interface.ts +++ b/packages/repository/src/interfaces/projectRepository.interface.ts @@ -47,6 +47,15 @@ export interface IProjectReadRepository { * @returns A promise that resolves to a Project object if found, or undefined if not found. */ getProjectByAnchor(chainId: ChainId, anchorAddress: Address): Promise; + + /** + * Retrieves a project by its anchor address and chain ID. + * @param chainId The chain ID of the project. + * @param anchorAddress The anchor address of the project. + * @returns A promise that resolves to a Project object + * @throws {ProjectNotFound} if the project does not exist + */ + getProjectByAnchorOrThrow(chainId: ChainId, anchorAddress: Address): Promise; } export interface IProjectRepository extends IProjectReadRepository { diff --git a/packages/repository/src/interfaces/roundRepository.interface.ts b/packages/repository/src/interfaces/roundRepository.interface.ts index f937ee7..12ac0ba 100644 --- a/packages/repository/src/interfaces/roundRepository.interface.ts +++ b/packages/repository/src/interfaces/roundRepository.interface.ts @@ -38,6 +38,15 @@ export interface IRoundReadRepository { strategyAddress: Address, ): Promise; + /** + * Retrieves a round by its strategy address and chain ID. + * @param chainId The chain ID of the round. + * @param strategyAddress The strategy address of the round. + * @returns A promise that resolves to a Round object + * @throws {RoundNotFound} if the round does not exist + */ + getRoundByStrategyAddressOrThrow(chainId: ChainId, strategyAddress: Address): Promise; + /** * Retrieves a round by a specific role and role value. * @param chainId The chain ID of the round. diff --git a/packages/repository/src/internal.ts b/packages/repository/src/internal.ts index c27844e..df70aa9 100644 --- a/packages/repository/src/internal.ts +++ b/packages/repository/src/internal.ts @@ -3,3 +3,4 @@ export * from "./interfaces/index.js"; export * from "./db/connection.js"; export * from "./repositories/kysely/index.js"; export * from "./db/helpers.js"; +export * from "./exceptions/index.js"; diff --git a/packages/repository/src/repositories/kysely/application.repository.ts b/packages/repository/src/repositories/kysely/application.repository.ts index 05a3c21..48d6f8f 100644 --- a/packages/repository/src/repositories/kysely/application.repository.ts +++ b/packages/repository/src/repositories/kysely/application.repository.ts @@ -4,6 +4,7 @@ import { Address, ChainId, stringify } from "@grants-stack-indexer/shared"; import { Application, + ApplicationNotFound, Database, IApplicationRepository, NewApplication, @@ -64,6 +65,25 @@ export class KyselyApplicationRepository implements IApplicationRepository { .executeTakeFirst(); } + /* @inheritdoc */ + async getApplicationByAnchorAddressOrThrow( + chainId: ChainId, + roundId: string, + anchorAddress: Address, + ): Promise { + const application = await this.getApplicationByAnchorAddress( + chainId, + roundId, + anchorAddress, + ); + + if (!application) { + throw new ApplicationNotFound(chainId, roundId, anchorAddress); + } + + return application; + } + /* @inheritdoc */ async getApplicationsByRoundId(chainId: ChainId, roundId: string): Promise { return this.db diff --git a/packages/repository/src/repositories/kysely/project.repository.ts b/packages/repository/src/repositories/kysely/project.repository.ts index a058331..70e48d0 100644 --- a/packages/repository/src/repositories/kysely/project.repository.ts +++ b/packages/repository/src/repositories/kysely/project.repository.ts @@ -11,6 +11,7 @@ import { PartialProject, PendingProjectRole, Project, + ProjectNotFound, ProjectRoleNames, } from "../../internal.js"; @@ -57,6 +58,13 @@ export class KyselyProjectRepository implements IProjectRepository { .executeTakeFirst(); } + /* @inheritdoc */ + async getProjectByAnchorOrThrow(chainId: ChainId, anchorAddress: Address): Promise { + const project = await this.getProjectByAnchor(chainId, anchorAddress); + if (!project) throw new ProjectNotFound(chainId, anchorAddress); + return project; + } + /* @inheritdoc */ async insertProject(project: NewProject): Promise { await this.db.withSchema(this.schemaName).insertInto("projects").values(project).execute(); diff --git a/packages/repository/src/repositories/kysely/round.repository.ts b/packages/repository/src/repositories/kysely/round.repository.ts index d3103c2..dbed809 100644 --- a/packages/repository/src/repositories/kysely/round.repository.ts +++ b/packages/repository/src/repositories/kysely/round.repository.ts @@ -1,6 +1,6 @@ import { Kysely } from "kysely"; -import { Address, ChainId } from "@grants-stack-indexer/shared"; +import { Address, ChainId, stringify } from "@grants-stack-indexer/shared"; import { Database, @@ -11,6 +11,7 @@ import { PartialRound, PendingRoundRole, Round, + RoundNotFound, RoundRole, RoundRoleNames, } from "../../internal.js"; @@ -58,6 +59,18 @@ export class KyselyRoundRepository implements IRoundRepository { .executeTakeFirst(); } + /* @inheritdoc */ + async getRoundByStrategyAddressOrThrow( + chainId: ChainId, + strategyAddress: Address, + ): Promise { + const round = await this.getRoundByStrategyAddress(chainId, strategyAddress); + if (!round) { + throw new RoundNotFound(chainId, strategyAddress); + } + return round; + } + /* @inheritdoc */ async getRoundByRole( chainId: ChainId, @@ -99,10 +112,12 @@ export class KyselyRoundRepository implements IRoundRepository { where: { id: string; chainId: ChainId } | { chainId: ChainId; strategyAddress: Address }, round: PartialRound, ): Promise { + const _round = this.formatRound(round); + const query = this.db .withSchema(this.schemaName) .updateTable("rounds") - .set(round) + .set(_round) .where("chainId", "=", where.chainId); if ("id" in where) { @@ -218,4 +233,19 @@ export class KyselyRoundRepository implements IRoundRepository { .where("id", "in", ids) .execute(); } + + /** + * Formats the round to ensure that the matchingDistribution is stored as a JSONB string. + * @param round - The round to format. + * @returns The formatted round. + */ + private formatRound(round: T): T { + if (round?.matchingDistribution) { + round = { + ...round, + matchingDistribution: stringify(round.matchingDistribution), + }; + } + return round; + } } diff --git a/packages/repository/src/types/application.types.ts b/packages/repository/src/types/application.types.ts index 02df074..671890c 100644 --- a/packages/repository/src/types/application.types.ts +++ b/packages/repository/src/types/application.types.ts @@ -15,7 +15,7 @@ export type Application = { projectId: string; anchorAddress: Address | null; status: ApplicationStatus; - statusSnapshots: StatusSnapshot[] | string; + statusSnapshots: StatusSnapshot[]; distributionTransaction: string | null; metadataCid: string | null; metadata: unknown | null; diff --git a/packages/repository/src/types/round.types.ts b/packages/repository/src/types/round.types.ts index a458421..bc5bb84 100644 --- a/packages/repository/src/types/round.types.ts +++ b/packages/repository/src/types/round.types.ts @@ -40,7 +40,7 @@ export type Round = { strategyId: string; strategyName: string; readyForPayoutTransaction: string | null; - matchingDistribution: MatchingDistribution | null; + matchingDistribution: MatchingDistribution[] | null; projectId: string; tags: string[]; }; diff --git a/packages/shared/src/external.ts b/packages/shared/src/external.ts index 7d7bcd8..01ba50e 100644 --- a/packages/shared/src/external.ts +++ b/packages/shared/src/external.ts @@ -17,7 +17,7 @@ export { BigNumber } from "./internal.js"; export type { BigNumberType } from "./internal.js"; export type { TokenCode, Token } from "./internal.js"; -export { TOKENS, getToken } from "./tokens/tokens.js"; +export { TOKENS, getToken, getTokenOrThrow, UnknownToken } from "./internal.js"; export { isAlloEvent, isRegistryEvent, isStrategyEvent } from "./internal.js"; export { stringify } from "./internal.js"; diff --git a/packages/shared/src/tokens/tokens.ts b/packages/shared/src/tokens/tokens.ts index 3167001..dcb3961 100644 --- a/packages/shared/src/tokens/tokens.ts +++ b/packages/shared/src/tokens/tokens.ts @@ -1,6 +1,6 @@ import { Branded } from "viem"; -import { Address } from "../internal.js"; +import { Address, ChainId } from "../internal.js"; export type TokenCode = Branded; @@ -606,3 +606,15 @@ export const TOKENS: { export const getToken = (chainId: number, tokenAddress: Address): Token | undefined => { return TOKENS[chainId]?.[tokenAddress]; }; + +export class UnknownToken extends Error { + constructor(tokenAddress: string, chainId?: ChainId) { + super(`Unknown token: ${tokenAddress} ${chainId ? `on chain ${chainId}` : ""}`); + } +} + +export const getTokenOrThrow = (chainId: ChainId, tokenAddress: Address): Token => { + const token = getToken(chainId, tokenAddress); + if (!token) throw new UnknownToken(tokenAddress, chainId); + return token; +}; diff --git a/packages/shared/src/types/events/strategy.ts b/packages/shared/src/types/events/strategy.ts index d9ac6c1..a752339 100644 --- a/packages/shared/src/types/events/strategy.ts +++ b/packages/shared/src/types/events/strategy.ts @@ -1,6 +1,6 @@ import { Hex } from "viem"; -import { Address, AnyEvent, ContractName, ProcessorEvent } from "../../internal.js"; +import { Address, AnyEvent, Bytes32String, ContractName, ProcessorEvent } from "../../internal.js"; /** * This array is used to represent all Strategy events. @@ -12,11 +12,20 @@ const StrategyEventArray = [ "DistributedWithData", "DistributedWithFlowRate", "TimestampsUpdated", + "TimestampsUpdatedWithRegistrationAndAllocation", "AllocatedWithOrigin", "AllocatedWithToken", "AllocatedWithData", "AllocatedWithVotes", "AllocatedWithStatus", + "DistributionUpdated", + "FundsDistributed", + "RecipientStatusUpdatedWithApplicationId", + "RecipientStatusUpdatedWithRecipientStatus", + "RecipientStatusUpdatedWithFullRow", + "UpdatedRegistrationWithStatus", + "UpdatedRegistration", + "UpdatedRegistrationWithApplicationId", ] as const; /** @@ -39,13 +48,31 @@ export type StrategyEventParams = T extends "Registered ? DistributedWithFlowRateParams : T extends "TimestampsUpdated" ? TimestampsUpdatedParams - : T extends "AllocatedWithToken" - ? AllocatedWithTokenParams - : T extends "AllocatedWithOrigin" - ? AllocatedWithOriginParams - : T extends "AllocatedWithVotes" - ? AllocatedWithVotesParams - : never; + : T extends "TimestampsUpdatedWithRegistrationAndAllocation" + ? TimestampsUpdatedWithRegistrationAndAllocationParams + : T extends "AllocatedWithToken" + ? AllocatedWithTokenParams + : T extends "AllocatedWithOrigin" + ? AllocatedWithOriginParams + : T extends "AllocatedWithVotes" + ? AllocatedWithVotesParams + : T extends "DistributionUpdated" + ? DistributionUpdatedParams + : T extends "FundsDistributed" + ? FundsDistributedParams + : T extends "RecipientStatusUpdatedWithApplicationId" + ? RecipientStatusUpdatedWithApplicationIdParams + : T extends "RecipientStatusUpdatedWithRecipientStatus" + ? RecipientStatusUpdatedWithRecipientStatusParams + : T extends "RecipientStatusUpdatedWithFullRow" + ? RecipientStatusUpdatedWithFullRowParams + : T extends "UpdatedRegistrationWithStatus" + ? UpdatedRegistrationWithStatusParams + : T extends "UpdatedRegistration" + ? UpdatedRegistrationParams + : T extends "UpdatedRegistrationWithApplicationId" + ? UpdatedRegistrationWithApplicationIdParams + : never; // ============================================================================= // =============================== Event Parameters ============================ @@ -68,7 +95,7 @@ export type DistributedWithRecipientAddressParams = { recipientAddress: Address; recipientId: Address; sender: Address; - amount: bigint; + amount: string; //uint256 }; export type DistributedWithDataParams = { @@ -77,28 +104,45 @@ export type DistributedWithDataParams = { }; export type DistributedWithFlowRateParams = { - flowRate: bigint; + flowRate: string; //int96 sender: Address; }; // ======================= TimestampsUpdated ======================= export type TimestampsUpdatedParams = { - contractAddress: Address; - timestamp: number; + startTime: string; //uint64 + endTime: string; //uint64 + sender: Address; +}; + +export type TimestampsUpdatedWithRegistrationAndAllocationParams = { + registrationStartTime: string; //uint64 + registrationEndTime: string; //uint64 + allocationStartTime: string; //uint64 + allocationEndTime: string; //uint64 + sender: Address; +}; + +// ======================= FundsDistributed ======================= +export type FundsDistributedParams = { + amount: string; //uint256 + grantee: Address; + token: Address; + recipientId: Address; }; // ======================= Allocated ======================= export type AllocatedWithTokenParams = { recipientId: Address; - amount: bigint; + amount: string; //uint256 token: Address; sender: Address; }; export type AllocatedWithOriginParams = { recipientId: Address; - amount: bigint; + amount: string; //uint256 token: Address; sender: Address; origin: Address; @@ -106,14 +150,62 @@ export type AllocatedWithOriginParams = { export type AllocatedWithVotesParams = { recipientId: Address; - votes: bigint; + votes: string; //uint256 allocator: Address; }; export type AllocatedWithStatusParams = { recipientId: Address; - status: number; + status: string; //uint8 + sender: Address; +}; + +// ======================= DistributionUpdated ======================= +export type DistributionUpdatedParams = { + merkleRoot: Bytes32String; + metadata: [protocol: string, pointer: string]; //uint256,bytes32 +}; + +// ======================= RecipientStatusUpdated ======================= +export type RecipientStatusUpdatedWithApplicationIdParams = { + recipientId: Address; + applicationId: string; //uint256 + status: string; //uint8 + sender: Address; +}; + +export type RecipientStatusUpdatedWithRecipientStatusParams = { + recipientId: Address; + status: string; //uint8 + sender: Address; +}; + +export type RecipientStatusUpdatedWithFullRowParams = { + rowIndex: string; //uint256 + fullRow: string; //uint256 + sender: Address; +}; + +// ======================= UpdatedRegistration ======================= +export type UpdatedRegistrationWithStatusParams = { + recipientId: Address; + data: Hex; + sender: Address; + status: string; //uint8 +}; + +export type UpdatedRegistrationParams = { + recipientId: Address; + data: Hex; + sender: Address; +}; + +export type UpdatedRegistrationWithApplicationIdParams = { + recipientId: Address; + applicationId: bigint; + data: Hex; sender: Address; + status: string; //uint8 }; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f7bfa2..0bd975f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -246,6 +246,9 @@ importers: "@grants-stack-indexer/shared": specifier: workspace:* version: link:../shared + statuses-bitmap: + specifier: github:gitcoinco/statuses-bitmap#f123d7778e42e16adb98fff2b2ba18c0fee57227 + version: https://codeload.github.com/gitcoinco/statuses-bitmap/tar.gz/f123d7778e42e16adb98fff2b2ba18c0fee57227(typescript@5.5.4) viem: specifier: 2.21.19 version: 2.21.19(typescript@5.5.4)(zod@3.23.8) @@ -1404,6 +1407,12 @@ packages: integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==, } + "@types/json-schema@7.0.15": + resolution: + { + integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, + } + "@types/json5@0.0.29": resolution: { @@ -1440,12 +1449,32 @@ packages: integrity: sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==, } + "@types/semver@7.5.8": + resolution: + { + integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==, + } + "@types/triple-beam@1.3.5": resolution: { integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==, } + "@typescript-eslint/eslint-plugin@5.62.0": + resolution: + { + integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + peerDependencies: + "@typescript-eslint/parser": ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: "*" + peerDependenciesMeta: + typescript: + optional: true + "@typescript-eslint/eslint-plugin@7.18.0": resolution: { @@ -1460,6 +1489,19 @@ packages: typescript: optional: true + "@typescript-eslint/parser@5.62.0": + resolution: + { + integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: "*" + peerDependenciesMeta: + typescript: + optional: true + "@typescript-eslint/parser@7.18.0": resolution: { @@ -1473,6 +1515,13 @@ packages: typescript: optional: true + "@typescript-eslint/scope-manager@5.62.0": + resolution: + { + integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + "@typescript-eslint/scope-manager@7.18.0": resolution: { @@ -1480,6 +1529,19 @@ packages: } engines: { node: ^18.18.0 || >=20.0.0 } + "@typescript-eslint/type-utils@5.62.0": + resolution: + { + integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + peerDependencies: + eslint: "*" + typescript: "*" + peerDependenciesMeta: + typescript: + optional: true + "@typescript-eslint/type-utils@7.18.0": resolution: { @@ -1493,6 +1555,13 @@ packages: typescript: optional: true + "@typescript-eslint/types@5.62.0": + resolution: + { + integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + "@typescript-eslint/types@7.18.0": resolution: { @@ -1500,6 +1569,18 @@ packages: } engines: { node: ^18.18.0 || >=20.0.0 } + "@typescript-eslint/typescript-estree@5.62.0": + resolution: + { + integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + peerDependencies: + typescript: "*" + peerDependenciesMeta: + typescript: + optional: true + "@typescript-eslint/typescript-estree@7.18.0": resolution: { @@ -1512,6 +1593,15 @@ packages: typescript: optional: true + "@typescript-eslint/utils@5.62.0": + resolution: + { + integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + "@typescript-eslint/utils@7.18.0": resolution: { @@ -1521,6 +1611,13 @@ packages: peerDependencies: eslint: ^8.56.0 + "@typescript-eslint/visitor-keys@5.62.0": + resolution: + { + integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + "@typescript-eslint/visitor-keys@7.18.0": resolution: { @@ -2414,6 +2511,13 @@ packages: eslint-config-prettier: optional: true + eslint-scope@5.1.1: + resolution: + { + integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==, + } + engines: { node: ">=8.0.0" } + eslint-scope@7.2.2: resolution: { @@ -2457,6 +2561,13 @@ packages: } engines: { node: ">=4.0" } + estraverse@4.3.0: + resolution: + { + integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==, + } + engines: { node: ">=4.0" } + estraverse@5.3.0: resolution: { @@ -3501,6 +3612,12 @@ packages: engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true + natural-compare-lite@1.4.0: + resolution: + { + integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==, + } + natural-compare@1.4.0: resolution: { @@ -4171,6 +4288,13 @@ packages: integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, } + statuses-bitmap@https://codeload.github.com/gitcoinco/statuses-bitmap/tar.gz/f123d7778e42e16adb98fff2b2ba18c0fee57227: + resolution: + { + tarball: https://codeload.github.com/gitcoinco/statuses-bitmap/tar.gz/f123d7778e42e16adb98fff2b2ba18c0fee57227, + } + version: 0.1.0 + std-env@3.7.0: resolution: { @@ -4417,6 +4541,12 @@ packages: integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==, } + tslib@1.14.1: + resolution: + { + integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==, + } + tslib@2.4.0: resolution: { @@ -4429,6 +4559,15 @@ packages: integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==, } + tsutils@3.21.0: + resolution: + { + integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==, + } + engines: { node: ">= 6" } + peerDependencies: + typescript: ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + tsx@4.19.2: resolution: { @@ -5505,6 +5644,8 @@ snapshots: "@types/estree@1.0.5": {} + "@types/json-schema@7.0.15": {} + "@types/json5@0.0.29": optional: true @@ -5524,8 +5665,29 @@ snapshots: pg-protocol: 1.7.0 pg-types: 4.0.2 + "@types/semver@7.5.8": {} + "@types/triple-beam@1.3.5": {} + "@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.56.0)(typescript@5.5.4))(eslint@8.56.0)(typescript@5.5.4)": + dependencies: + "@eslint-community/regexpp": 4.11.0 + "@typescript-eslint/parser": 5.62.0(eslint@8.56.0)(typescript@5.5.4) + "@typescript-eslint/scope-manager": 5.62.0 + "@typescript-eslint/type-utils": 5.62.0(eslint@8.56.0)(typescript@5.5.4) + "@typescript-eslint/utils": 5.62.0(eslint@8.56.0)(typescript@5.5.4) + debug: 4.3.7 + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare-lite: 1.4.0 + semver: 7.6.3 + tsutils: 3.21.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + "@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.56.0)(typescript@5.5.4))(eslint@8.56.0)(typescript@5.5.4)": dependencies: "@eslint-community/regexpp": 4.11.0 @@ -5544,6 +5706,18 @@ snapshots: transitivePeerDependencies: - supports-color + "@typescript-eslint/parser@5.62.0(eslint@8.56.0)(typescript@5.5.4)": + dependencies: + "@typescript-eslint/scope-manager": 5.62.0 + "@typescript-eslint/types": 5.62.0 + "@typescript-eslint/typescript-estree": 5.62.0(typescript@5.5.4) + debug: 4.3.7 + eslint: 8.56.0 + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + "@typescript-eslint/parser@7.18.0(eslint@8.56.0)(typescript@5.5.4)": dependencies: "@typescript-eslint/scope-manager": 7.18.0 @@ -5557,11 +5731,28 @@ snapshots: transitivePeerDependencies: - supports-color + "@typescript-eslint/scope-manager@5.62.0": + dependencies: + "@typescript-eslint/types": 5.62.0 + "@typescript-eslint/visitor-keys": 5.62.0 + "@typescript-eslint/scope-manager@7.18.0": dependencies: "@typescript-eslint/types": 7.18.0 "@typescript-eslint/visitor-keys": 7.18.0 + "@typescript-eslint/type-utils@5.62.0(eslint@8.56.0)(typescript@5.5.4)": + dependencies: + "@typescript-eslint/typescript-estree": 5.62.0(typescript@5.5.4) + "@typescript-eslint/utils": 5.62.0(eslint@8.56.0)(typescript@5.5.4) + debug: 4.3.7 + eslint: 8.56.0 + tsutils: 3.21.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + "@typescript-eslint/type-utils@7.18.0(eslint@8.56.0)(typescript@5.5.4)": dependencies: "@typescript-eslint/typescript-estree": 7.18.0(typescript@5.5.4) @@ -5574,8 +5765,24 @@ snapshots: transitivePeerDependencies: - supports-color + "@typescript-eslint/types@5.62.0": {} + "@typescript-eslint/types@7.18.0": {} + "@typescript-eslint/typescript-estree@5.62.0(typescript@5.5.4)": + dependencies: + "@typescript-eslint/types": 5.62.0 + "@typescript-eslint/visitor-keys": 5.62.0 + debug: 4.3.7 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.6.3 + tsutils: 3.21.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + "@typescript-eslint/typescript-estree@7.18.0(typescript@5.5.4)": dependencies: "@typescript-eslint/types": 7.18.0 @@ -5591,6 +5798,21 @@ snapshots: transitivePeerDependencies: - supports-color + "@typescript-eslint/utils@5.62.0(eslint@8.56.0)(typescript@5.5.4)": + dependencies: + "@eslint-community/eslint-utils": 4.4.0(eslint@8.56.0) + "@types/json-schema": 7.0.15 + "@types/semver": 7.5.8 + "@typescript-eslint/scope-manager": 5.62.0 + "@typescript-eslint/types": 5.62.0 + "@typescript-eslint/typescript-estree": 5.62.0(typescript@5.5.4) + eslint: 8.56.0 + eslint-scope: 5.1.1 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + - typescript + "@typescript-eslint/utils@7.18.0(eslint@8.56.0)(typescript@5.5.4)": dependencies: "@eslint-community/eslint-utils": 4.4.0(eslint@8.56.0) @@ -5602,6 +5824,11 @@ snapshots: - supports-color - typescript + "@typescript-eslint/visitor-keys@5.62.0": + dependencies: + "@typescript-eslint/types": 5.62.0 + eslint-visitor-keys: 3.4.3 + "@typescript-eslint/visitor-keys@7.18.0": dependencies: "@typescript-eslint/types": 7.18.0 @@ -6143,6 +6370,11 @@ snapshots: optionalDependencies: eslint-config-prettier: 9.1.0(eslint@8.56.0) + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 @@ -6207,6 +6439,8 @@ snapshots: dependencies: estraverse: 5.3.0 + estraverse@4.3.0: {} + estraverse@5.3.0: {} estree-walker@3.0.3: @@ -6754,6 +6988,8 @@ snapshots: nanoid@3.3.7: {} + natural-compare-lite@1.4.0: {} + natural-compare@1.4.0: {} node-releases@2.0.18: {} @@ -7089,6 +7325,15 @@ snapshots: stackback@0.0.2: {} + statuses-bitmap@https://codeload.github.com/gitcoinco/statuses-bitmap/tar.gz/f123d7778e42e16adb98fff2b2ba18c0fee57227(typescript@5.5.4): + dependencies: + "@typescript-eslint/eslint-plugin": 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.56.0)(typescript@5.5.4))(eslint@8.56.0)(typescript@5.5.4) + "@typescript-eslint/parser": 5.62.0(eslint@8.56.0)(typescript@5.5.4) + eslint: 8.56.0 + transitivePeerDependencies: + - supports-color + - typescript + std-env@3.7.0: {} string-argv@0.3.2: {} @@ -7231,10 +7476,17 @@ snapshots: strip-bom: 3.0.0 optional: true + tslib@1.14.1: {} + tslib@2.4.0: {} tslib@2.7.0: {} + tsutils@3.21.0(typescript@5.5.4): + dependencies: + tslib: 1.14.1 + typescript: 5.5.4 + tsx@4.19.2: dependencies: esbuild: 0.23.1