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: base strategy processor & dvmd strategy factory #16

Merged
merged 7 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pnpm lint-staged && pnpm check-types
pnpm lint-staged && pnpm check-types --force
8 changes: 6 additions & 2 deletions packages/data-flow/test/unit/eventsFetcher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("EventsFetcher", () => {
eventName: "PoolCreated",
srcAddress: "0x1234567890123456789012345678901234567890",
logIndex: 0,
params: { contractAddress: "0x1234" },
params: { contractAddress: "0x1234", tokenAddress: "0x1234", amount: 1000 },
transactionFields: { hash: "0x1234", transactionIndex: 0 },
},
{
Expand All @@ -38,7 +38,11 @@ describe("EventsFetcher", () => {
eventName: "PoolCreated",
srcAddress: "0x1234567890123456789012345678901234567890",
logIndex: 0,
params: { contractAddress: "0x1234" },
params: {
contractAddress: "0x1234",
tokenAddress: "0x1234",
amount: 1000,
},
transactionFields: { hash: "0x1234", transactionIndex: 1 },
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe("EnvioIndexerClient", () => {
eventName: "PoolCreated",
srcAddress: "0x1234567890123456789012345678901234567890",
logIndex: 0,
params: { contractAddress: "0x1234" },
params: { contractAddress: "0x1234", tokenAddress: "0x1234", amount: 1000 },
transactionFields: {
hash: "0x123",
transactionIndex: 1,
Expand Down
1 change: 1 addition & 0 deletions packages/processors/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./tokenPriceNotFound.exception.js";
export * from "./unsupportedEvent.exception.js";
export * from "./invalidArgument.exception.js";
export * from "./unsupportedStrategy.exception.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Hex } from "viem";

export class UnsupportedStrategy extends Error {
constructor(strategyId: Hex) {
super(`Strategy ${strategyId} unsupported`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we add some more context here, like the address ? Maybe this is correct and we should enrich the log once is bubbled up. wdyt?

Copy link
Collaborator Author

@0xnigir1 0xnigir1 Oct 28, 2024

Choose a reason for hiding this comment

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

mmm we can take EBO's error handling approach and a context object for errors. i think that with bubbling the event should be enough almost every time right? is the most generic context object

Copy link
Collaborator

Choose a reason for hiding this comment

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

🤝

}
}
1 change: 1 addition & 0 deletions packages/processors/src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./processor.interface.js";
export * from "./factory.interface.js";
export * from "./eventHandler.interface.js";
export * from "./strategyHandler.interface.js";
15 changes: 15 additions & 0 deletions packages/processors/src/interfaces/strategyHandler.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Changeset } from "@grants-stack-indexer/repository";
import { ContractToEventName, ProtocolEvent } from "@grants-stack-indexer/shared";

/**
* Interface for an event handler.
* @template C - The contract name.
* @template E - The event name.
*/
export interface IStrategyHandler<E extends ContractToEventName<"Strategy">> {
/**
* Handles the event.
* @returns A promise that resolves to an array of changesets.
*/
handle(event: ProtocolEvent<"Strategy", E>): Promise<Changeset[]>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Changeset } from "@grants-stack-indexer/repository";
import { ChainId, ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared";

import type { IStrategyHandler, ProcessorDependencies } from "../../internal.js";
import { UnsupportedEventException } from "../../internal.js";
import { DVMDDistributedHandler, DVMDRegisteredHandler } from "./handlers/index.js";

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

/**
* This handler is responsible for processing events related to the
* Donation Voting Merkle Distribution Direct Transfer strategy.
*
* The following events are currently handled by this strategy:
* - Registered
* - Distributed
*/

export class DVMDDirectTransferHandler implements IStrategyHandler<StrategyEvent> {
constructor(
private readonly chainId: ChainId,
private readonly dependencies: Dependencies,
) {}
async handle(event: ProtocolEvent<"Strategy", StrategyEvent>): Promise<Changeset[]> {
switch (event.eventName) {
case "Registered":
return new DVMDRegisteredHandler(
event as ProtocolEvent<"Strategy", "Registered">,
this.chainId,
this.dependencies,
).handle();
case "Distributed":
return new DVMDDistributedHandler(
event as ProtocolEvent<"Strategy", "Distributed">,
this.chainId,
this.dependencies,
).handle();
default:
throw new UnsupportedEventException("Strategy", event.eventName);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Changeset } from "@grants-stack-indexer/repository";
import { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared";

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

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

export class DVMDDistributedHandler implements IEventHandler<"Strategy", "Distributed"> {
constructor(
readonly event: ProtocolEvent<"Strategy", "Distributed">,
private readonly chainId: ChainId,
private readonly dependencies: Dependencies,
) {}

async handle(): Promise<Changeset[]> {
//TODO: Implement
throw new Error("Method not implemented.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./distributed.handler.js";
export * from "./registered.handler.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Changeset } from "@grants-stack-indexer/repository";
import { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared";

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

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

export class DVMDRegisteredHandler implements IEventHandler<"Strategy", "Registered"> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

worth having docs for the class + inherit docs for the functions?

constructor(
readonly event: ProtocolEvent<"Strategy", "Registered">,
private readonly chainId: ChainId,
private readonly dependencies: Dependencies,
) {}

async handle(): Promise<Changeset[]> {
//TODO: Implement
throw new Error("Not implemented");
}
}
21 changes: 16 additions & 5 deletions packages/processors/src/strategy/strategy.processor.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { Changeset } from "@grants-stack-indexer/repository";
import { ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared";
import { ChainId, ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared";

import type { IProcessor } from "../internal.js";
import type { IProcessor, ProcessorDependencies } from "../internal.js";
import { StrategyHandlerFactory } from "./strategyHandler.factory.js";

export class StrategyProcessor implements IProcessor<"Strategy", StrategyEvent> {
process(_event: ProtocolEvent<"Strategy", StrategyEvent>): Promise<Changeset[]> {
//TODO: Implement
throw new Error("Method not implemented.");
constructor(
private readonly chainId: ChainId,
private readonly dependencies: ProcessorDependencies,
) {}

async process(event: ProtocolEvent<"Strategy", StrategyEvent>): Promise<Changeset[]> {
const strategyId = event.strategyId;

return StrategyHandlerFactory.createHandler(
this.chainId,
this.dependencies,
strategyId,
).handle(event);
}
}
30 changes: 30 additions & 0 deletions packages/processors/src/strategy/strategyHandler.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Hex } from "viem";

import { ChainId, StrategyEvent } from "@grants-stack-indexer/shared";

import { IStrategyHandler, ProcessorDependencies, UnsupportedStrategy } from "../internal.js";
import { DVMDDirectTransferHandler } from "./donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js";

export class StrategyHandlerFactory {
static createHandler(
chainId: ChainId,
dependencies: ProcessorDependencies,
strategyId: Hex,
): IStrategyHandler<StrategyEvent> {
const _strategyId = strategyId.toLowerCase();

switch (_strategyId) {
case "0x6f9291df02b2664139cec5703c124e4ebce32879c74b6297faa1468aa5ff9ebf":
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe worth extracting these contract addresses out into a map for readability

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sounds nice

// DonationVotingMerkleDistributionDirectTransferStrategyv1.1
case "0x2f46bf157821dc41daa51479e94783bb0c8699eac63bf75ec450508ab03867ce":
// DonationVotingMerkleDistributionDirectTransferStrategyv2.0
case "0x2f0250d534b2d59b8b5cfa5eb0d0848a59ccbf5de2eaf72d2ba4bfe73dce7c6b":
// DonationVotingMerkleDistributionDirectTransferStrategyv2.1
case "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0":
return new DVMDDirectTransferHandler(chainId, dependencies);

default:
throw new UnsupportedStrategy(strategyId);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import type { IMetadataProvider } from "@grants-stack-indexer/metadata";
import type {
IProjectReadRepository,
IRoundReadRepository,
} from "@grants-stack-indexer/repository";
import { ChainId, ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared";

import { UnsupportedEventException } from "../../../src/internal.js";
import { DVMDDirectTransferHandler } from "../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js";
import {
DVMDDistributedHandler,
DVMDRegisteredHandler,
} from "../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.js";

vi.mock(
"../../../src/strategy/donationVotingMerkleDistributionDirectTransfer/handlers/index.js",
() => {
const DVMDRegisteredHandler = vi.fn();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
DVMDRegisteredHandler.prototype.handle = vi.fn();
const DVMDDistributedHandler = vi.fn();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
DVMDDistributedHandler.prototype.handle = vi.fn();
return {
DVMDRegisteredHandler,
DVMDDistributedHandler,
};
},
);

describe("DVMDDirectTransferHandler", () => {
const mockChainId = 10 as ChainId;
let handler: DVMDDirectTransferHandler;
let mockMetadataProvider: IMetadataProvider;
let mockRoundRepository: IRoundReadRepository;
let mockProjectRepository: IProjectReadRepository;

beforeEach(() => {
mockMetadataProvider = {} as IMetadataProvider;
mockRoundRepository = {} as IRoundReadRepository;
mockProjectRepository = {} as IProjectReadRepository;

handler = new DVMDDirectTransferHandler(mockChainId, {
metadataProvider: mockMetadataProvider,
roundRepository: mockRoundRepository,
projectRepository: mockProjectRepository,
});
});

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

it("calls RegisteredHandler for Registered event", async () => {
const mockEvent = {
eventName: "Registered",
} as ProtocolEvent<"Strategy", "Registered">;

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

await handler.handle(mockEvent);

expect(DVMDRegisteredHandler).toHaveBeenCalledWith(mockEvent, mockChainId, {
metadataProvider: mockMetadataProvider,
roundRepository: mockRoundRepository,
projectRepository: mockProjectRepository,
});
expect(DVMDRegisteredHandler.prototype.handle).toHaveBeenCalled();
});

it("calls DistributedHandler for Distributed event", async () => {
const mockEvent = {
eventName: "Distributed",
} as ProtocolEvent<"Strategy", "Distributed">;

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

await handler.handle(mockEvent);

expect(DVMDDistributedHandler).toHaveBeenCalledWith(mockEvent, mockChainId, {
metadataProvider: mockMetadataProvider,
roundRepository: mockRoundRepository,
projectRepository: mockProjectRepository,
});
expect(DVMDDistributedHandler.prototype.handle).toHaveBeenCalled();
});

it.skip("calls AllocatedHandler for Allocated event");
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 ProtocolEvent<
"Strategy",
StrategyEvent
>;
await expect(() => handler.handle(mockEvent)).rejects.toThrow(UnsupportedEventException);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { describe } from "node:test";
import { it } from "vitest";

describe("DVMDDirectTransferDistributedHandler", () => {
it.skip("handle the distributed event", () => {});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { describe } from "node:test";
import { it } from "vitest";

describe("DVMDDirectTransferRegisteredHandler", () => {
it.skip("handle the registered event", () => {});
});
Loading