Skip to content

Commit 37ebd66

Browse files
authored
feat: add handler for RF distribution updated (#116)
# 🤖 Linear Closes PAR-XXX ## Description ## 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. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced the distribution update process with an improved event handling mechanism. - Introduced a simplified distribution data format for more accurate round updates. - Added a new handler for processing "DistributionUpdated" events. - Expanded type definitions to accommodate new distribution structures. - **Refactor** - Upgraded the existing event handler for more reliable processing. - Streamlined distribution type definitions to ensure consistency. - **Tests** - Added comprehensive testing to verify successful outcomes and robust error handling during distribution updates. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 1f4de53 + 59a9b37 commit 37ebd66

File tree

9 files changed

+257
-42
lines changed

9 files changed

+257
-42
lines changed

packages/data-flow/test/integration/strategy.integration.spec.ts

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -209,30 +209,14 @@ describe("Orchestrator Integration - Strategy Events Processing", () => {
209209
"fetchEventsByBlockNumberAndLogIndex",
210210
);
211211

212-
const mockDistribution = {
213-
matchingDistribution: [
214-
{
215-
contributionsCount: 44,
216-
projectPayoutAddress: "0x7340F1a1e4e38F43d2FCC85cdb2b764de36B40c0",
217-
applicationId: "3",
218-
matchPoolPercentage: 0.099999,
219-
projectId: "0x15c5e4db5530e05216abc9484025e2f1c4fb55b8525d29ef38fde237e767e324",
220-
projectName: "ReFi DAO - A Network Society to Regenerate Earth. ✨ 🌱",
221-
matchAmountInToken: "999999999999999475712",
222-
originalMatchAmountInToken: "999999999999999475712",
223-
},
224-
{
225-
contributionsCount: 69,
226-
projectPayoutAddress: "0x01d1909cA27E364904934849eab8399532dd5c8b",
227-
applicationId: "11",
228-
matchPoolPercentage: 0.099999,
229-
projectId: "0xca460772f5ba0840a589d2c19fb7e17ac259e4be884313e3712c06c9c885dc93",
230-
projectName: "Giveth",
231-
matchAmountInToken: "999999999999999475712",
232-
originalMatchAmountInToken: "999999999999999475712",
233-
},
234-
],
235-
};
212+
const mockDistribution = [
213+
{
214+
anchorAddress: "0x7340F1a1e4e38F43d2FCC85cdb2b764de36B40c0",
215+
payoutAddress: "0x7340F1a1e4e38F43d2FCC85cdb2b764de36B40c0",
216+
amount: "1000000000000000000",
217+
index: 0,
218+
},
219+
];
236220

237221
vi.spyOn(metadataProvider, "getMetadata").mockResolvedValue(mockDistribution);
238222

@@ -254,7 +238,7 @@ describe("Orchestrator Integration - Strategy Events Processing", () => {
254238
{ chainId, strategyAddress: distributionUpdatedEvent.srcAddress },
255239
{
256240
readyForPayoutTransaction: distributionUpdatedEvent.transactionFields.hash,
257-
matchingDistribution: mockDistribution.matchingDistribution,
241+
matchingDistribution: mockDistribution,
258242
},
259243
{},
260244
);

packages/processors/src/processors/strategy/easyRetroFunding/easyRetroFunding.handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ import {
2323
UnsupportedEventException,
2424
} from "../../../internal.js";
2525
import {
26-
BaseDistributionUpdatedHandler,
2726
BaseFundsDistributedHandler,
2827
BaseRecipientStatusUpdatedHandler,
2928
BaseStrategyHandler,
3029
} from "../common/index.js";
3130
import {
31+
ERFDistributionUpdatedHandler,
3232
ERFRegisteredHandler,
3333
ERFTimestampsUpdatedHandler,
3434
ERFUpdatedRegistrationHandler,
@@ -86,7 +86,7 @@ export class EasyRetroFundingStrategyHandler extends BaseStrategyHandler {
8686
this.dependencies,
8787
).handle();
8888
case "DistributionUpdated":
89-
return new BaseDistributionUpdatedHandler(
89+
return new ERFDistributionUpdatedHandler(
9090
event as ProcessorEvent<"Strategy", "DistributionUpdated">,
9191
this.chainId,
9292
this.dependencies,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { getAddress } from "viem";
2+
3+
import { Changeset } from "@grants-stack-indexer/repository";
4+
import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared";
5+
6+
import {
7+
IEventHandler,
8+
MetadataNotFound,
9+
MetadataParsingFailed,
10+
ProcessorDependencies,
11+
} from "../../../../internal.js";
12+
import {
13+
SimpleMatchingDistribution,
14+
SimpleMatchingDistributionSchema,
15+
} from "../../../../schemas/index.js";
16+
17+
type Dependencies = Pick<ProcessorDependencies, "metadataProvider" | "logger">;
18+
19+
/**
20+
* ERFDistributionUpdatedHandler: Processes 'DistributionUpdated' events
21+
*
22+
* - Decodes the updated distribution metadata
23+
* - Creates a changeset to update the round with the new distribution
24+
*
25+
* @dev:
26+
* - Strategy handlers that want to handle the DistributionUpdated event should create an instance of this class corresponding to the event.
27+
*
28+
*/
29+
30+
export class ERFDistributionUpdatedHandler
31+
implements IEventHandler<"Strategy", "DistributionUpdated">
32+
{
33+
constructor(
34+
readonly event: ProcessorEvent<"Strategy", "DistributionUpdated">,
35+
private readonly chainId: ChainId,
36+
private readonly dependencies: Dependencies,
37+
) {}
38+
39+
/* @inheritdoc */
40+
async handle(): Promise<Changeset[]> {
41+
const { logger, metadataProvider } = this.dependencies;
42+
const [_, pointer] = this.event.params.metadata;
43+
44+
const strategyAddress = getAddress(this.event.srcAddress);
45+
const rawDistribution = await metadataProvider.getMetadata<
46+
SimpleMatchingDistribution | undefined
47+
>(pointer);
48+
49+
if (!rawDistribution) {
50+
logger.warn(`No matching distribution found for pointer: ${pointer}`);
51+
52+
throw new MetadataNotFound(`No matching distribution found for pointer: ${pointer}`);
53+
}
54+
55+
const distribution = SimpleMatchingDistributionSchema.safeParse(rawDistribution);
56+
57+
if (!distribution.success) {
58+
logger.warn(`Failed to parse matching distribution: ${distribution.error.message}`);
59+
60+
throw new MetadataParsingFailed(
61+
`Failed to parse matching distribution: ${distribution.error.message}`,
62+
);
63+
}
64+
65+
return [
66+
{
67+
type: "UpdateRoundByStrategyAddress",
68+
args: {
69+
chainId: this.chainId,
70+
strategyAddress,
71+
round: {
72+
readyForPayoutTransaction: this.event.transactionFields.hash,
73+
matchingDistribution: distribution.data,
74+
},
75+
},
76+
},
77+
];
78+
}
79+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./registered.handler.js";
22
export * from "./timestampsUpdated.handler.js";
33
export * from "./updatedRegistration.handler.js";
4+
export * from "./distributionUpdated.handler.js";

packages/processors/src/schemas/matchingDistribution.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from "zod";
22

33
export type MatchingDistribution = z.infer<typeof MatchingDistributionSchema>;
4+
export type SimpleMatchingDistribution = z.infer<typeof SimpleMatchingDistributionSchema>;
45

56
const BigIntSchema = z.string().or(
67
z.object({ type: z.literal("BigNumber"), hex: z.string() }).transform((val) => {
@@ -22,3 +23,12 @@ export const MatchingDistributionSchema = z.object({
2223
}),
2324
),
2425
});
26+
27+
export const SimpleMatchingDistributionSchema = z.array(
28+
z.object({
29+
anchorAddress: z.string(),
30+
payoutAddress: z.string(),
31+
amount: BigIntSchema.default("0"),
32+
index: z.coerce.number(),
33+
}),
34+
);

packages/processors/test/strategy/easyRetroFunding/easyRetroFunding.handler.spec.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ import { ChainId, ILogger, ProcessorEvent, StrategyEvent } from "@grants-stack-i
1212

1313
import { UnsupportedEventException } from "../../../src/internal.js";
1414
import {
15-
BaseDistributionUpdatedHandler,
1615
BaseFundsDistributedHandler,
1716
BaseRecipientStatusUpdatedHandler,
1817
} from "../../../src/processors/strategy/common/index.js";
1918
import { EasyRetroFundingStrategyHandler } from "../../../src/processors/strategy/easyRetroFunding/easyRetroFunding.handler.js";
2019
import {
20+
ERFDistributionUpdatedHandler,
2121
ERFRegisteredHandler,
2222
ERFTimestampsUpdatedHandler,
2323
ERFUpdatedRegistrationHandler,
@@ -27,38 +27,37 @@ vi.mock("../../../src/processors/strategy/easyRetroFunding/handlers/index.js", a
2727
const ERFRegisteredHandler = vi.fn();
2828
const ERFTimestampsUpdatedHandler = vi.fn();
2929
const ERFUpdatedRegistrationHandler = vi.fn();
30-
30+
const ERFDistributionUpdatedHandler = vi.fn();
3131
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
3232
ERFRegisteredHandler.prototype.handle = vi.fn();
3333
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
3434
ERFTimestampsUpdatedHandler.prototype.handle = vi.fn();
3535
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
3636
ERFUpdatedRegistrationHandler.prototype.handle = vi.fn();
3737
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
38+
ERFDistributionUpdatedHandler.prototype.handle = vi.fn();
39+
3840
return {
3941
ERFRegisteredHandler,
4042
ERFTimestampsUpdatedHandler,
4143
ERFUpdatedRegistrationHandler,
44+
ERFDistributionUpdatedHandler,
4245
};
4346
});
4447

4548
vi.mock("../../../src/processors/strategy/common/index.js", async (importOriginal) => {
4649
const original =
4750
await importOriginal<typeof import("../../../src/processors/strategy/common/index.js")>();
4851
const BaseFundsDistributedHandler = vi.fn();
49-
const BaseDistributionUpdatedHandler = vi.fn();
5052
const BaseRecipientStatusUpdatedHandler = vi.fn();
5153

5254
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
5355
BaseFundsDistributedHandler.prototype.handle = vi.fn();
5456
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
55-
BaseDistributionUpdatedHandler.prototype.handle = vi.fn();
56-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
5757
BaseRecipientStatusUpdatedHandler.prototype.handle = vi.fn();
5858
return {
5959
...original,
6060
BaseFundsDistributedHandler,
61-
BaseDistributionUpdatedHandler,
6261
BaseRecipientStatusUpdatedHandler,
6362
};
6463
});
@@ -197,16 +196,16 @@ describe("EasyRetroFundingStrategyHandler", () => {
197196
expect(ERFTimestampsUpdatedHandler.prototype.handle).toHaveBeenCalled();
198197
});
199198

200-
it("calls BaseDistributionUpdatedHandler for DistributionUpdated event", async () => {
199+
it("calls ERFDistributionUpdatedHandler for DistributionUpdated event", async () => {
201200
const mockEvent = {
202201
eventName: "DistributionUpdated",
203202
} as ProcessorEvent<"Strategy", "DistributionUpdated">;
204203

205-
vi.spyOn(BaseDistributionUpdatedHandler.prototype, "handle").mockResolvedValue([]);
204+
vi.spyOn(ERFDistributionUpdatedHandler.prototype, "handle").mockResolvedValue([]);
206205

207206
await handler.handle(mockEvent);
208207

209-
expect(BaseDistributionUpdatedHandler).toHaveBeenCalledWith(mockEvent, chainId, {
208+
expect(ERFDistributionUpdatedHandler).toHaveBeenCalledWith(mockEvent, chainId, {
210209
metadataProvider: mockMetadataProvider,
211210
roundRepository: mockRoundRepository,
212211
projectRepository: mockProjectRepository,
@@ -215,7 +214,7 @@ describe("EasyRetroFundingStrategyHandler", () => {
215214
applicationRepository: mockApplicationRepository,
216215
logger,
217216
});
218-
expect(BaseDistributionUpdatedHandler.prototype.handle).toHaveBeenCalled();
217+
expect(ERFDistributionUpdatedHandler.prototype.handle).toHaveBeenCalled();
219218
});
220219

221220
it("calls FundsDistributedHandler for FundsDistributed event", async () => {
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { getAddress } from "viem";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
4+
import { IMetadataProvider } from "@grants-stack-indexer/metadata";
5+
import { PartialRound } from "@grants-stack-indexer/repository";
6+
import { Bytes32String, ChainId, Logger, ProcessorEvent } from "@grants-stack-indexer/shared";
7+
8+
import { MetadataNotFound, MetadataParsingFailed } from "../../../../src/internal.js";
9+
import { ERFDistributionUpdatedHandler } from "../../../../src/processors/strategy/easyRetroFunding/index.js";
10+
import { createMockEvent } from "../../../mocks/index.js";
11+
12+
describe("ERFDistributionUpdatedHandler", () => {
13+
let handler: ERFDistributionUpdatedHandler;
14+
let mockMetadataProvider: IMetadataProvider;
15+
let mockLogger: Logger;
16+
let mockEvent: ProcessorEvent<"Strategy", "DistributionUpdated">;
17+
const chainId = 10 as ChainId;
18+
const eventName = "DistributionUpdated";
19+
const defaultParams = {
20+
metadata: ["1", "ipfs://QmTestHash"] as [string, string],
21+
merkleRoot: "0xroot" as Bytes32String,
22+
};
23+
const defaultStrategyId = "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0";
24+
25+
beforeEach(() => {
26+
mockMetadataProvider = {
27+
getMetadata: vi.fn(),
28+
} as unknown as IMetadataProvider;
29+
mockLogger = {
30+
warn: vi.fn(),
31+
error: vi.fn(),
32+
info: vi.fn(),
33+
debug: vi.fn(),
34+
} as unknown as Logger;
35+
});
36+
37+
it("handles a valid distribution update event", async () => {
38+
mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId);
39+
const mockDistribution = [
40+
{
41+
anchorAddress: "anchorAddress",
42+
payoutAddress: "payoutAddress",
43+
amount: "10",
44+
index: 0,
45+
},
46+
];
47+
48+
vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(mockDistribution);
49+
50+
handler = new ERFDistributionUpdatedHandler(mockEvent, chainId, {
51+
metadataProvider: mockMetadataProvider,
52+
logger: mockLogger,
53+
});
54+
55+
const result = await handler.handle();
56+
57+
expect(result).toEqual([
58+
{
59+
type: "UpdateRoundByStrategyAddress",
60+
args: {
61+
chainId,
62+
strategyAddress: getAddress(mockEvent.srcAddress),
63+
round: {
64+
readyForPayoutTransaction: mockEvent.transactionFields.hash,
65+
matchingDistribution: mockDistribution,
66+
},
67+
},
68+
},
69+
]);
70+
});
71+
72+
it("throws MetadataNotFound if distribution metadata is not found", async () => {
73+
mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId);
74+
vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined);
75+
76+
handler = new ERFDistributionUpdatedHandler(mockEvent, chainId, {
77+
metadataProvider: mockMetadataProvider,
78+
logger: mockLogger,
79+
});
80+
81+
await expect(handler.handle()).rejects.toThrow(MetadataNotFound);
82+
expect(mockLogger.warn).toHaveBeenCalledWith(
83+
expect.stringContaining("No matching distribution found for pointer:"),
84+
);
85+
});
86+
87+
it("throw MatchingDistributionParsingError if distribution format is invalid", async () => {
88+
mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId);
89+
const invalidDistribution = {
90+
matchingDistribution: [
91+
{
92+
amount: "not_a_number", // Invalid amount format
93+
applicationId: "app1",
94+
recipientAddress: "0x1234567890123456789012345678901234567890",
95+
},
96+
],
97+
};
98+
99+
vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(invalidDistribution);
100+
101+
handler = new ERFDistributionUpdatedHandler(mockEvent, chainId, {
102+
metadataProvider: mockMetadataProvider,
103+
logger: mockLogger,
104+
});
105+
106+
await expect(handler.handle()).rejects.toThrow(MetadataParsingFailed);
107+
expect(mockLogger.warn).toHaveBeenCalledWith(
108+
expect.stringContaining("Failed to parse matching distribution:"),
109+
);
110+
});
111+
112+
it("handles empty matching distribution array", async () => {
113+
mockEvent = createMockEvent(eventName, defaultParams, defaultStrategyId);
114+
115+
vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue([]);
116+
117+
handler = new ERFDistributionUpdatedHandler(mockEvent, chainId, {
118+
metadataProvider: mockMetadataProvider,
119+
logger: mockLogger,
120+
});
121+
122+
const result = await handler.handle();
123+
expect(result).toHaveLength(1);
124+
125+
const changeset = result[0] as {
126+
type: "UpdateRoundByStrategyAddress";
127+
args: {
128+
round: PartialRound;
129+
};
130+
};
131+
expect(changeset.args.round.matchingDistribution).toEqual([]);
132+
});
133+
});

0 commit comments

Comments
 (0)