generated from defi-wonderland/ts-turborepo-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: direct grants simple event handlers (#35)
# 🤖 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
Showing
9 changed files
with
606 additions
and
0 deletions.
There are no files selected for viewing
52 changes: 52 additions & 0 deletions
52
packages/processors/src/processors/strategy/directGrantsSimple/directGrantsSimple.handler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
2 changes: 2 additions & 0 deletions
2
packages/processors/src/processors/strategy/directGrantsSimple/handlers/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./timestampsUpdated.handler.js"; | ||
export * from "./registered.handler.js"; |
95 changes: 95 additions & 0 deletions
95
...ages/processors/src/processors/strategy/directGrantsSimple/handlers/registered.handler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
]; | ||
} | ||
} |
59 changes: 59 additions & 0 deletions
59
...ocessors/src/processors/strategy/directGrantsSimple/handlers/timestampsUpdated.handler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}, | ||
}, | ||
]; | ||
} | ||
} |
2 changes: 2 additions & 0 deletions
2
packages/processors/src/processors/strategy/directGrantsSimple/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./handlers/index.js"; | ||
export * from "./directGrantsSimple.handler.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
161 changes: 161 additions & 0 deletions
161
packages/processors/test/strategy/directGrantsSimple/directGrantsSimple.handler.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
Oops, something went wrong.