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: direct grants simple event handlers #35

Merged
merged 3 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Changeset } from "@grants-stack-indexer/repository";
import { ChainId, ProcessorEvent, StrategyEvent } from "@grants-stack-indexer/shared";

import { ProcessorDependencies, UnsupportedEventException } from "../../../internal.js";
import { BaseDistributedHandler, BaseStrategyHandler } from "../index.js";
import { DGSimpleRegisteredHandler, DGSimpleTimestampsUpdatedHandler } from "./handlers/index.js";

const STRATEGY_NAME = "allov2.DirectGrantsSimpleStrategy";

/**
* This handler is responsible for processing events related to the
* Direct Grants Simple strategy.
*
* The following events are currently handled by this strategy:
* - TimestampsUpdated
* - RegisteredWithSender
* - DistributedWithRecipientAddress
*/
export class DGSimpleStrategyHandler extends BaseStrategyHandler {
constructor(
private readonly chainId: ChainId,
private readonly dependencies: ProcessorDependencies,
) {
super(STRATEGY_NAME);
}

/** @inheritdoc */
async handle(event: ProcessorEvent<"Strategy", StrategyEvent>): Promise<Changeset[]> {
switch (event.eventName) {
case "TimestampsUpdated":
return new DGSimpleTimestampsUpdatedHandler(
event as ProcessorEvent<"Strategy", "TimestampsUpdated">,
this.chainId,
this.dependencies,
).handle();
case "RegisteredWithSender":
return new DGSimpleRegisteredHandler(
event as ProcessorEvent<"Strategy", "RegisteredWithSender">,
this.chainId,
this.dependencies,
).handle();
case "DistributedWithRecipientAddress":
return new BaseDistributedHandler(
event as ProcessorEvent<"Strategy", "DistributedWithRecipientAddress">,
this.chainId,
this.dependencies,
).handle();
default:
throw new UnsupportedEventException("Strategy", event.eventName, this.name);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./timestampsUpdated.handler.js";
export * from "./registered.handler.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { getAddress } from "viem";

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

import { IEventHandler, ProcessorDependencies } from "../../../../internal.js";
import { decodeDGApplicationData } from "../../helpers/index.js";

type Dependencies = Pick<
ProcessorDependencies,
"roundRepository" | "projectRepository" | "metadataProvider"
>;

/**
* Handles the Registered event for the Donation Voting Merkle Distribution Direct Transfer strategy.
*
* This handler performs the following core actions when a project registers for a round:
* - Validates that both the project and round exist
* - Decodes the application data from the event
* - Retrieves the application metadata
* - Creates a new application record with PENDING status
* - Links the application to both the project and round
*/

export class DGSimpleRegisteredHandler
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export class DGSimpleRegisteredHandler
export class DirectGrantsSimpleRegisteredHandler

may we use complete names for strategies ? or at least use the same for strategy and event handlers :)

Look here packages/processors/src/processors/strategy/directGrantsSimple/directGrantsSimple.handler.ts

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

9d6a824

i renamed to DGSimple to follow the convention on the other strategies

implements IEventHandler<"Strategy", "RegisteredWithSender">
{
constructor(
readonly event: ProcessorEvent<"Strategy", "RegisteredWithSender">,
private readonly chainId: ChainId,
private readonly dependencies: Dependencies,
) {}

/**
* Handles the RegisteredWithSender event for the Direct Grants Simple strategy.
* @returns The changeset with an InsertApplication operation.
* @throws ProjectNotFound if the project is not found.
* @throws RoundNotFound if the round is not found.
*/
async handle(): Promise<Changeset[]> {
const { projectRepository, roundRepository, metadataProvider } = this.dependencies;
const { data: encodedData, recipientId, sender } = this.event.params;
const { blockNumber, blockTimestamp } = this.event;

const anchorAddress = getAddress(recipientId);
const project = await projectRepository.getProjectByAnchorOrThrow(
this.chainId,
anchorAddress,
);

const strategyAddress = getAddress(this.event.srcAddress);
const round = await roundRepository.getRoundByStrategyAddressOrThrow(
this.chainId,
strategyAddress,
);

const values = decodeDGApplicationData(encodedData);
const id = recipientId;

const metadata = await metadataProvider.getMetadata(values.metadata.pointer);

const application: NewApplication = {
chainId: this.chainId,
id: id,
projectId: project.id,
anchorAddress,
roundId: round.id,
status: "PENDING",
metadataCid: values.metadata.pointer,
metadata: metadata ?? null,
createdAtBlock: BigInt(blockNumber),
createdByAddress: getAddress(sender),
statusUpdatedAtBlock: BigInt(blockNumber),
statusSnapshots: [
{
status: "PENDING",
updatedAtBlock: blockNumber.toString(),
updatedAt: new Date(blockTimestamp * 1000), // timestamp is in seconds, convert to ms
},
],
distributionTransaction: null,
totalAmountDonatedInUsd: 0,
totalDonationsCount: 0,
uniqueDonorsCount: 0,
tags: ["allo-v2"],
};

return [
{
type: "InsertApplication",
args: application,
},
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { getAddress } from "viem";

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

import { getDateFromTimestamp } from "../../../../helpers/index.js";
import { IEventHandler, ProcessorDependencies } from "../../../../internal.js";

type Dependencies = Pick<ProcessorDependencies, "roundRepository">;

/**
* Handles the TimestampsUpdated event for the Direct Grants Simple strategy.
*
* This handler processes updates to the round timestamps:
* - Validates the round exists for the strategy address
* - Converts the updated registration timestamps to dates
* - Returns a changeset to update the round's application timestamps
*/
export class DGSimpleTimestampsUpdatedHandler
implements IEventHandler<"Strategy", "TimestampsUpdated">
{
constructor(
readonly event: ProcessorEvent<"Strategy", "TimestampsUpdated">,
private readonly chainId: ChainId,
private readonly dependencies: Dependencies,
) {}

/**
* Handles the TimestampsUpdated event for the Direct Grants Simple strategy.
* @returns The changeset with an UpdateRound operation.
* @throws RoundNotFound if the round is not found.
*/
async handle(): Promise<Changeset[]> {
const strategyAddress = getAddress(this.event.srcAddress);
const round = await this.dependencies.roundRepository.getRoundByStrategyAddressOrThrow(
this.chainId,
strategyAddress,
);

const { startTime: strStartTime, endTime: strEndTime } = this.event.params;

const applicationsStartTime = getDateFromTimestamp(BigInt(strStartTime));
const applicationsEndTime = getDateFromTimestamp(BigInt(strEndTime));

return [
{
type: "UpdateRound",
args: {
chainId: this.chainId,
roundId: round.id,
round: {
applicationsStartTime,
applicationsEndTime,
},
},
},
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./handlers/index.js";
export * from "./directGrantsSimple.handler.js";
3 changes: 3 additions & 0 deletions packages/processors/src/processors/strategy/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Hex } from "viem";
import type { StrategyHandlerConstructor } from "../../internal.js";
import { DirectAllocationStrategyHandler } from "./directAllocation/index.js";
import { DirectGrantsLiteStrategyHandler } from "./directGrantsLite/index.js";
import { DGSimpleStrategyHandler } from "./directGrantsSimple/index.js";
import { DVMDDirectTransferStrategyHandler } from "./donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js";

/**
Expand All @@ -22,6 +23,8 @@ const strategyIdToHandler: Readonly<Record<string, StrategyHandlerConstructor>>
DVMDDirectTransferStrategyHandler, // DonationVotingMerkleDistributionDirectTransferStrategyv2.1
"0x4cd0051913234cdd7d165b208851240d334786d6e5afbb4d0eec203515a9c6f3":
DirectAllocationStrategyHandler,
"0x263cb916541b6fc1fb5543a244829ccdba75264b097726e6ecc3c3cfce824bf5": DGSimpleStrategyHandler,
"0x53fb9d3bce0956ca2db5bb1441f5ca23050cb1973b33789e04a5978acfd9ca93": DGSimpleStrategyHandler,
"0x103732a8e473467a510d4128ee11065262bdd978f0d9dad89ba68f2c56127e27":
DirectGrantsLiteStrategyHandler,
} as const;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { EvmProvider } from "@grants-stack-indexer/chain-providers";
import { IMetadataProvider } from "@grants-stack-indexer/metadata";
import { IPricingProvider } from "@grants-stack-indexer/pricing";
import {
IApplicationReadRepository,
IProjectReadRepository,
IRoundReadRepository,
} from "@grants-stack-indexer/repository";
import { ChainId, ILogger, ProcessorEvent, StrategyEvent } from "@grants-stack-indexer/shared";

import { UnsupportedEventException } from "../../../src/exceptions/index.js";
import { BaseDistributedHandler } from "../../../src/processors/strategy/common/index.js";
import { DGSimpleStrategyHandler } from "../../../src/processors/strategy/directGrantsSimple/directGrantsSimple.handler.js";
import {
DGSimpleRegisteredHandler,
DGSimpleTimestampsUpdatedHandler,
} from "../../../src/processors/strategy/directGrantsSimple/handlers/index.js";

vi.mock("../../../src/processors/strategy/directGrantsSimple/handlers/index.js", () => {
const DGSimpleRegisteredHandler = vi.fn();
const DGSimpleTimestampsUpdatedHandler = vi.fn();

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
DGSimpleRegisteredHandler.prototype.handle = vi.fn();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
DGSimpleTimestampsUpdatedHandler.prototype.handle = vi.fn();

return {
DGSimpleRegisteredHandler,
DGSimpleTimestampsUpdatedHandler,
};
});

vi.mock("../../../src/processors/strategy/common/baseDistributed.handler.js", () => {
const BaseDistributedHandler = vi.fn();

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
BaseDistributedHandler.prototype.handle = vi.fn();

return {
BaseDistributedHandler,
};
});

describe("DirectGrantsSimpleStrategyHandler", () => {
let handler: DGSimpleStrategyHandler;
let mockMetadataProvider: IMetadataProvider;
let mockRoundRepository: IRoundReadRepository;
let mockProjectRepository: IProjectReadRepository;
let mockEVMProvider: EvmProvider;
let mockPricingProvider: IPricingProvider;
let mockApplicationRepository: IApplicationReadRepository;
const chainId = 10 as ChainId;

const logger: ILogger = {
debug: vi.fn(),
error: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
};

beforeEach(() => {
mockMetadataProvider = {} as IMetadataProvider;
mockRoundRepository = {} as IRoundReadRepository;
mockProjectRepository = {} as IProjectReadRepository;
mockEVMProvider = {} as EvmProvider;
mockPricingProvider = {} as IPricingProvider;
mockApplicationRepository = {} as IApplicationReadRepository;

handler = new DGSimpleStrategyHandler(chainId, {
metadataProvider: mockMetadataProvider,
roundRepository: mockRoundRepository,
projectRepository: mockProjectRepository,
evmProvider: mockEVMProvider,
pricingProvider: mockPricingProvider,
applicationRepository: mockApplicationRepository,
logger,
});
});

afterEach(() => {
vi.clearAllMocks();
});

it("returns correct name", () => {
expect(handler.name).toBe("allov2.DirectGrantsSimpleStrategy");
});

it("calls DGSimpleTimestampsUpdatedHandler for TimestampsUpdated event", async () => {
const mockEvent = {
eventName: "TimestampsUpdated",
} as ProcessorEvent<"Strategy", "TimestampsUpdated">;

vi.spyOn(DGSimpleTimestampsUpdatedHandler.prototype, "handle").mockResolvedValue([]);

await handler.handle(mockEvent);

expect(DGSimpleTimestampsUpdatedHandler).toHaveBeenCalledWith(mockEvent, chainId, {
metadataProvider: mockMetadataProvider,
roundRepository: mockRoundRepository,
projectRepository: mockProjectRepository,
evmProvider: mockEVMProvider,
pricingProvider: mockPricingProvider,
applicationRepository: mockApplicationRepository,
logger,
});
expect(DGSimpleTimestampsUpdatedHandler.prototype.handle).toHaveBeenCalled();
});

it("calls DGSimpleRegisteredHandler for RegisteredWithSender event", async () => {
const mockEvent = {
eventName: "RegisteredWithSender",
} as ProcessorEvent<"Strategy", "RegisteredWithSender">;

vi.spyOn(DGSimpleRegisteredHandler.prototype, "handle").mockResolvedValue([]);

await handler.handle(mockEvent);

expect(DGSimpleRegisteredHandler).toHaveBeenCalledWith(mockEvent, chainId, {
metadataProvider: mockMetadataProvider,
roundRepository: mockRoundRepository,
projectRepository: mockProjectRepository,
evmProvider: mockEVMProvider,
pricingProvider: mockPricingProvider,
applicationRepository: mockApplicationRepository,
logger,
});
expect(DGSimpleRegisteredHandler.prototype.handle).toHaveBeenCalled();
});

it("calls BaseDistributedHandler for DistributedWithRecipientAddress event", async () => {
const mockEvent = {
eventName: "DistributedWithRecipientAddress",
} as ProcessorEvent<"Strategy", "DistributedWithRecipientAddress">;

vi.spyOn(BaseDistributedHandler.prototype, "handle").mockResolvedValue([]);

await handler.handle(mockEvent);

expect(BaseDistributedHandler).toHaveBeenCalledWith(mockEvent, chainId, {
metadataProvider: mockMetadataProvider,
roundRepository: mockRoundRepository,
projectRepository: mockProjectRepository,
evmProvider: mockEVMProvider,
pricingProvider: mockPricingProvider,
applicationRepository: mockApplicationRepository,
logger,
});
expect(BaseDistributedHandler.prototype.handle).toHaveBeenCalled();
});

it("throws UnsupportedEventException for unknown events", async () => {
const mockEvent = {
eventName: "UnknownEvent",
} as unknown as ProcessorEvent<"Strategy", StrategyEvent>;

await expect(handler.handle(mockEvent)).rejects.toThrow(UnsupportedEventException);
});
});
Loading