Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: all remaining event handlers for DVMD strategy #30

Merged
merged 5 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/indexer/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions apps/indexer/src/handlers/Strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ({}) => {});
9 changes: 9 additions & 0 deletions packages/data-flow/test/unit/orchestrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]) {
Expand Down
1 change: 1 addition & 0 deletions packages/processors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
6 changes: 6 additions & 0 deletions packages/processors/src/constants/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum ApplicationStatus {
NONE = 0,
PENDING,
APPROVED,
REJECTED,
}
1 change: 1 addition & 0 deletions packages/processors/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./enums.js";
1 change: 1 addition & 0 deletions packages/processors/src/exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ 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";
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class MetadataNotFound extends Error {
constructor(message: string) {
super(message);
this.name = "MetadataNotFoundError";
}
}
1 change: 1 addition & 0 deletions packages/processors/src/internal.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
1 change: 1 addition & 0 deletions packages/processors/src/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./projectMetadata.js";
export * from "./roundMetadata.js";
export * from "./applicationMetadata.js";
export * from "./matchingDistribution.js";
25 changes: 25 additions & 0 deletions packages/processors/src/schemas/matchingDistribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { z } from "zod";

export type MatchingDistribution = z.infer<typeof MatchingDistributionSchema>;

// handle ethers bigint serialization
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean viem?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just copy-pasted the comment, will delete it

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.coerce.number(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will coerce empty strings to 0 rather than fail validation--just checking if you're ok with this. If you'd prefer to fail validation you can check out this: colinhacks/zod#2461 (comment)

Copy link
Collaborator Author

@0xnigir1 0xnigir1 Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting 👀 , have to say that '' being 0 is kinda unexpected

contributionsCount: z.coerce.number(),
originalMatchAmountInToken: BigIntSchema.default("0"),
matchAmountInToken: BigIntSchema.default("0"),
}),
),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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<ProcessorDependencies, "metadataProvider" | "logger">;

/**
* 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,
) {}

async handle(): Promise<Changeset[]> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inheritdoc?

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}`);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should MetadataProvider be responsible for throwing a "not found" error instead of having the handler doing it?


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,
},
},
},
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Address, getAddress } from "viem";

import { Application, Changeset, Round } from "@grants-stack-indexer/repository";
import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared";

import {
ApplicationNotFound,
IEventHandler,
ProcessorDependencies,
RoundNotFound,
} 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<Changeset[]> {
const strategyAddress = getAddress(this.event.srcAddress);
const round = await this.getRoundOrThrow(strategyAddress);

const roundId = round.id;
const anchorAddress = getAddress(this.event.params.recipientId);
const application = await this.getApplicationOrThrow(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: this.event.params.amount,
},
},
];
}

/**
* Retrieves a round by its strategy address.
* @param {Address} strategyAddress - The address of the strategy.
* @returns {Promise<Round>} The round found.
* @throws {RoundNotFound} if the round does not exist.
*/
private async getRoundOrThrow(strategyAddress: Address): Promise<Round> {
const { roundRepository } = this.dependencies;
const round = await roundRepository.getRoundByStrategyAddress(
this.chainId,
strategyAddress,
);

if (!round) {
throw new RoundNotFound(this.chainId, strategyAddress);
}

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<Application>} The application found.
* @throws {ApplicationNotFound} if the application does not exist.
*/
private async getApplicationOrThrow(
roundId: string,
anchorAddress: Address,
): Promise<Application> {
const { applicationRepository } = this.dependencies;
const application = await applicationRepository.getApplicationByAnchorAddress(
this.chainId,
roundId,
anchorAddress,
);

if (!application) {
throw new ApplicationNotFound(this.chainId, roundId, anchorAddress);
}

return application;
}
}
Loading