-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from 3 commits
fabe7e3
a180bf3
8dc11aa
72a6c5a
8c25428
baf8c34
5bd2d20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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`); | ||
} | ||
} |
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"; |
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"> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
} | ||
} |
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); | ||
} | ||
} |
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": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe worth extracting these contract addresses out into a map for readability There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", () => {}); | ||
}); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤝