Skip to content

Commit

Permalink
feat: direct grants simple event handlers (#35)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes GIT-161 GIT-163 

## Description
- add `DirectGrantsSimpleStrategy` handler and events handlers:
  - `Registered`
  - `TimestampsUpdated`
  - `Distributed`


## 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.
  • Loading branch information
0xnigir1 authored Nov 20, 2024
1 parent 0623cda commit 5d2d01b
Show file tree
Hide file tree
Showing 9 changed files with 606 additions and 0 deletions.
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
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

0 comments on commit 5d2d01b

Please sign in to comment.