Skip to content

Commit 9ad1569

Browse files
authored
feat: base strategy processor & dvmd strategy factory (#16)
# 🤖 Linear Closes GIT-60 GIT-61 GIT-62 ## Description - `StrategyHandlerFactory` to create the proper Strategy handler based on strategyId - `BaseStrategyHandler` abstract class w/ default implementation for fetch functions (some strategies require custom logic) - `DVMDDirectTransferStrategy` handler - `Distributed` and `Registered` handlers for DVMD strategy (PR #17) - refactored TODOs from `PoolCreatedHandler` ## 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.
1 parent 28dcae6 commit 9ad1569

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1501
-269
lines changed

.husky/pre-commit

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
pnpm lint-staged && pnpm check-types
1+
pnpm lint-staged && pnpm check-types --force

packages/data-flow/test/unit/eventsFetcher.spec.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe("EventsFetcher", () => {
2727
eventName: "PoolCreated",
2828
srcAddress: "0x1234567890123456789012345678901234567890",
2929
logIndex: 0,
30-
params: { contractAddress: "0x1234" },
30+
params: { contractAddress: "0x1234", tokenAddress: "0x1234", amount: 1000 },
3131
transactionFields: { hash: "0x1234", transactionIndex: 0 },
3232
},
3333
{
@@ -38,7 +38,11 @@ describe("EventsFetcher", () => {
3838
eventName: "PoolCreated",
3939
srcAddress: "0x1234567890123456789012345678901234567890",
4040
logIndex: 0,
41-
params: { contractAddress: "0x1234" },
41+
params: {
42+
contractAddress: "0x1234",
43+
tokenAddress: "0x1234",
44+
amount: 1000,
45+
},
4246
transactionFields: { hash: "0x1234", transactionIndex: 1 },
4347
},
4448
];

packages/indexer-client/test/unit/envioIndexerClient.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe("EnvioIndexerClient", () => {
5151
eventName: "PoolCreated",
5252
srcAddress: "0x1234567890123456789012345678901234567890",
5353
logIndex: 0,
54-
params: { contractAddress: "0x1234" },
54+
params: { contractAddress: "0x1234", tokenAddress: "0x1234", amount: 1000 },
5555
transactionFields: {
5656
hash: "0x123",
5757
transactionIndex: 1,

packages/processors/src/allo/handlers/poolCreated.handler.ts

+21-29
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import { getAddress, parseUnits, zeroAddress } from "viem";
1+
import { getAddress, zeroAddress } from "viem";
22

33
import type { Changeset, NewRound, PendingRoundRole } from "@grants-stack-indexer/repository";
44
import type { ChainId, ProtocolEvent, Token } from "@grants-stack-indexer/shared";
5-
import { isAlloNativeToken } from "@grants-stack-indexer/shared";
6-
import { getToken } from "@grants-stack-indexer/shared/dist/src/internal.js";
5+
import { getToken, isAlloNativeToken } from "@grants-stack-indexer/shared";
76

87
import type { IEventHandler, ProcessorDependencies, StrategyTimings } from "../../internal.js";
9-
import { getRoundRoles } from "../../helpers/roles.js";
10-
import { extractStrategyFromId, getStrategyTimings } from "../../helpers/strategy.js";
11-
import { calculateAmountInUsd } from "../../helpers/tokenMath.js";
12-
import { TokenPriceNotFoundError } from "../../internal.js";
8+
import { calculateAmountInUsd, getRoundRoles } from "../../helpers/index.js";
9+
import { StrategyHandlerFactory, TokenPriceNotFoundError } from "../../internal.js";
1310
import { RoundMetadataSchema } from "../../schemas/index.js";
1411

1512
type Dependencies = Pick<
@@ -61,7 +58,11 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated">
6158
? zeroAddress
6259
: checksummedTokenAddress;
6360

64-
const strategy = extractStrategyFromId(strategyId);
61+
const strategyHandler = StrategyHandlerFactory.createHandler(
62+
this.chainId,
63+
this.dependencies as ProcessorDependencies,
64+
strategyId,
65+
);
6566

6667
const token = getToken(this.chainId, matchTokenAddress);
6768

@@ -72,26 +73,17 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated">
7273
donationsEndTime: null,
7374
};
7475

75-
let matchAmount = 0n;
76-
let matchAmountInUsd = "0";
77-
78-
if (strategy) {
79-
strategyTimings = await getStrategyTimings(evmProvider, strategy, strategyAddress);
80-
81-
//TODO: when creating strategy handlers, should this be moved there?
82-
if (
83-
strategy.name === "allov2.DonationVotingMerkleDistributionDirectTransferStrategy" &&
84-
parsedRoundMetadata.success &&
85-
token
86-
) {
87-
matchAmount = parseUnits(
88-
parsedRoundMetadata.data.quadraticFundingConfig.matchingFundsAvailable.toString(),
89-
token.decimals,
90-
);
76+
let matchAmountObj = {
77+
matchAmount: 0n,
78+
matchAmountInUsd: "0",
79+
};
9180

92-
matchAmountInUsd = await this.getTokenAmountInUsd(
81+
if (strategyHandler) {
82+
strategyTimings = await strategyHandler.fetchStrategyTimings(strategyAddress);
83+
if (parsedRoundMetadata.success && token) {
84+
matchAmountObj = await strategyHandler.fetchMatchAmount(
85+
Number(parsedRoundMetadata.data.quadraticFundingConfig.matchingFundsAvailable),
9386
token,
94-
matchAmount,
9587
this.event.blockTimestamp,
9688
);
9789
}
@@ -120,8 +112,8 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated">
120112
totalAmountDonatedInUsd: "0",
121113
uniqueDonorsCount: 0,
122114
matchTokenAddress,
123-
matchAmount,
124-
matchAmountInUsd,
115+
matchAmount: matchAmountObj.matchAmount,
116+
matchAmountInUsd: matchAmountObj.matchAmountInUsd,
125117
fundedAmount,
126118
fundedAmountInUsd,
127119
applicationMetadataCid: metadataPointer,
@@ -132,7 +124,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated">
132124
...roundRoles,
133125
strategyAddress,
134126
strategyId,
135-
strategyName: strategy?.name ?? "",
127+
strategyName: strategyHandler?.name ?? "",
136128
createdByAddress: getAddress(createdBy),
137129
createdAtBlock: BigInt(this.event.blockNumber),
138130
updatedAtBlock: BigInt(this.event.blockNumber),
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
export * from "./tokenPriceNotFound.exception.js";
22
export * from "./unsupportedEvent.exception.js";
33
export * from "./invalidArgument.exception.js";
4+
export * from "./unsupportedStrategy.exception.js";
5+
export * from "./projectNotFound.exception.js";
6+
export * from "./roundNotFound.exception.js";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ChainId } from "@grants-stack-indexer/shared";
2+
3+
export class ProjectNotFound extends Error {
4+
constructor(chainId: ChainId, anchorAddress: string) {
5+
super(`Project not found for chainId: ${chainId} and anchorAddress: ${anchorAddress}`);
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ChainId } from "@grants-stack-indexer/shared";
2+
3+
export class RoundNotFound extends Error {
4+
constructor(chainId: ChainId, strategyAddress: string) {
5+
super(`Round not found for chainId: ${chainId} and strategyAddress: ${strategyAddress}`);
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Hex } from "viem";
2+
3+
export class UnsupportedStrategy extends Error {
4+
constructor(strategyId: Hex) {
5+
super(`Strategy ${strategyId} unsupported`);
6+
}
7+
}

packages/processors/src/external.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
// Add your external exports here
22
export { StrategyProcessor, AlloProcessor } from "./internal.js";
33
export type { IProcessor } from "./internal.js";
4+
5+
export { existsHandler } from "./internal.js";
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./roles.js";
2+
export * from "./utils.js";
3+
export * from "./tokenMath.js";

packages/processors/src/helpers/strategy.ts

+2-222
Original file line numberDiff line numberDiff line change
@@ -1,231 +1,11 @@
11
import type { EvmProvider } from "@grants-stack-indexer/chain-providers";
2-
import type { Address, Branded } from "@grants-stack-indexer/shared";
2+
import type { Address } from "@grants-stack-indexer/shared";
33

44
import DirectGrantsLiteStrategy from "../abis/allo-v2/v1/DirectGrantsLiteStrategy.js";
5-
import DonationVotingMerkleDistributionDirectTransferStrategy from "../abis/allo-v2/v1/DonationVotingMerkleDistributionDirectTransferStrategy.js";
65
import { StrategyTimings } from "../internal.js";
76
import { getDateFromTimestamp } from "./utils.js";
87

9-
type SanitizedStrategyId = Branded<string, "SanitizedStrategyId">;
10-
type Strategy = {
11-
id: SanitizedStrategyId;
12-
name: string | null;
13-
// TODO: check if groups are required
14-
groups: string[];
15-
};
16-
17-
//TODO: refactor this into a mapping in Shared package from ID to the corresponding handler class
18-
/*
19-
* Extracts the strategy from the ID.
20-
* @param _id - The ID of the strategy.
21-
* @returns The strategy.
22-
*/
23-
export function extractStrategyFromId(_id: Address): Strategy | undefined {
24-
const id = _id.toLowerCase();
25-
/* eslint-disable no-fallthrough */
26-
switch (id) {
27-
// SQFSuperfluidv1
28-
case "0xf8a14294e80ff012e54157ec9d1b2827421f1e7f6bde38c06730b1c031b3f935":
29-
return {
30-
id: id as SanitizedStrategyId,
31-
name: "allov2.SQFSuperFluidStrategy",
32-
groups: ["allov2.SQFSuperFluidStrategy"],
33-
};
34-
35-
// MicroGrantsv1
36-
case "0x697f0592ebd05466d2d24454477e11d69c475d7a7c4134f15ddc1ea9811bb16f":
37-
return {
38-
id: id as SanitizedStrategyId,
39-
name: "allov2.MicroGrantsStrategy",
40-
groups: ["allov2.MicroGrantsStrategy", "allov2.MicroGrantsCommon"],
41-
};
42-
43-
// MicroGrantsGovv1
44-
case "0x741ac1e2f387d83f219f6b5349d35ec34902cf94019d117335e0045d2e0ed912":
45-
return {
46-
id: id as SanitizedStrategyId,
47-
name: "allov2.MicroGrantsGovStrategy",
48-
groups: ["allov2.MicroGrantsGovStrategy", "allov2.MicroGrantsCommon"],
49-
};
50-
51-
// MicroGrantsHatsv1
52-
case "0x5aa24dcfcd55a1e059a172e987b3456736b4856c71e57aaf52e9a965897318dd":
53-
return {
54-
id: id as SanitizedStrategyId,
55-
name: "allov2.MicroGrantsHatsStrategy",
56-
groups: ["allov2.MicroGrantsHatsStrategy", "allov2.MicroGrantsCommon"],
57-
};
58-
59-
// RFPSimpleStrategyv1.0
60-
case "0x0d459e12d9e91d2b2a8fa12be8c7eb2b4f1c35e74573990c34b436613bc2350f":
61-
return {
62-
id: id as SanitizedStrategyId,
63-
name: "allov2.RFPSimpleStrategy",
64-
groups: ["allov2.RFPSimpleStrategy"],
65-
};
66-
67-
// RFPCommitteeStrategyv1.0
68-
case "0x7d143166a83c6a8a303ae32a6ccd287e48d79818f5d15d89e185391199909803":
69-
return {
70-
id: id as SanitizedStrategyId,
71-
name: "allov2.RFPCommitteeStrategy",
72-
groups: ["allov2.RFPCommitteeStrategy"],
73-
};
74-
75-
// QVSimpleStrategyv1.0
76-
case "0x22d006e191d6dc5ff1a25bb0733f47f64a9c34860b6703df88dea7cb3987b4c3":
77-
return {
78-
id: id as SanitizedStrategyId,
79-
name: "allov2.QVSimpleStrategy",
80-
groups: ["allov2.QVSimpleStrategy"],
81-
};
82-
83-
// DonationVotingMerkleDistributionDirectTransferStrategyv1.0
84-
case "0x6f9291df02b2664139cec5703c124e4ebce32879c74b6297faa1468aa5ff9ebf":
85-
// DonationVotingMerkleDistributionDirectTransferStrategyv1.1
86-
case "0x2f46bf157821dc41daa51479e94783bb0c8699eac63bf75ec450508ab03867ce":
87-
// DonationVotingMerkleDistributionDirectTransferStrategyv2.0
88-
case "0x2f0250d534b2d59b8b5cfa5eb0d0848a59ccbf5de2eaf72d2ba4bfe73dce7c6b":
89-
// DonationVotingMerkleDistributionDirectTransferStrategyv2.1
90-
case "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0":
91-
return {
92-
id: id as SanitizedStrategyId,
93-
name: "allov2.DonationVotingMerkleDistributionDirectTransferStrategy",
94-
groups: ["allov2.DonationVotingMerkleDistributionDirectTransferStrategy"],
95-
};
96-
97-
// DonationVotingMerkleDistributionVaultStrategyv1.0
98-
case "0x7e75375f0a7cd9f7ea159c8b065976e4f764f9dcef1edf692f31dd1842f70c87":
99-
// DonationVotingMerkleDistributionVaultStrategyv1.1
100-
case "0x093072375737c0e8872fef36808849aeba7f865e182d495f2b98308115c9ef13":
101-
return {
102-
id: id as SanitizedStrategyId,
103-
name: "allov2.DonationVotingMerkleDistributionVaultStrategy",
104-
groups: ["allov2.DonationVotingMerkleDistributionVaultStrategy"],
105-
};
106-
107-
// DirectGrantsSimpleStrategyv1.1
108-
case "0x263cb916541b6fc1fb5543a244829ccdba75264b097726e6ecc3c3cfce824bf5":
109-
// DirectGrantsSimpleStrategyv2.1
110-
case "0x53fb9d3bce0956ca2db5bb1441f5ca23050cb1973b33789e04a5978acfd9ca93":
111-
return {
112-
id: id as SanitizedStrategyId,
113-
name: "allov2.DirectGrantsSimpleStrategy",
114-
groups: ["allov2.DirectGrantsSimpleStrategy"],
115-
};
116-
117-
// DirectGrantsLiteStrategyv1.0
118-
case "0x103732a8e473467a510d4128ee11065262bdd978f0d9dad89ba68f2c56127e27":
119-
return {
120-
id: id as SanitizedStrategyId,
121-
name: "allov2.DirectGrantsLiteStrategy",
122-
groups: ["allov2.DirectGrantsLiteStrategy"],
123-
};
124-
125-
// EasyRPGFStrategy1.0
126-
case "0x662f5a0d3ea7e9b6ed1b351a9d96ac636a3c3ed727390aeff4ec931ae760d5ae":
127-
return {
128-
id: id as SanitizedStrategyId,
129-
name: "allov2.EasyRPGFStrategy",
130-
groups: ["allov2.EasyRPGFStrategy"],
131-
};
132-
133-
// DirectAllocationStrategyv1.1
134-
case "0x4cd0051913234cdd7d165b208851240d334786d6e5afbb4d0eec203515a9c6f3":
135-
return {
136-
id: id as SanitizedStrategyId,
137-
name: "allov2.DirectAllocationStrategy",
138-
groups: ["allov2.DirectAllocationStrategy"],
139-
};
140-
}
141-
142-
return undefined;
143-
}
144-
145-
//TODO: refactor this into the StrategyHandler when implemented
146-
// see if we can use a common interface or abstract class for all strategies
147-
// so we don't have to do this switch statement
148-
// most of the strategies don't need to fetch anything and just return null for all the times
149-
export const getStrategyTimings = async (
150-
evmProvider: EvmProvider,
151-
strategy: Strategy,
152-
strategyAddress: Address,
153-
): Promise<StrategyTimings> => {
154-
switch (strategy.name) {
155-
case "allov2.DonationVotingMerkleDistributionDirectTransferStrategy":
156-
return getDonationVotingMerkleDistributionDirectTransferStrategyTimings(
157-
evmProvider,
158-
strategyAddress,
159-
);
160-
case "allov2.DirectGrantsSimpleStrategy":
161-
case "allov2.DirectGrantsLiteStrategy":
162-
return getDirectGrantsStrategyTimings(evmProvider, strategyAddress);
163-
default:
164-
return {
165-
applicationsStartTime: null,
166-
applicationsEndTime: null,
167-
donationsStartTime: null,
168-
donationsEndTime: null,
169-
};
170-
}
171-
};
172-
173-
/**
174-
* Gets the strategy data for the DonationVotingMerkleDistributionDirectTransferStrategy
175-
* @param evmProvider - The evm provider
176-
* @param strategyId - The address of the strategy
177-
* @returns The strategy data
178-
*/
179-
export const getDonationVotingMerkleDistributionDirectTransferStrategyTimings = async (
180-
evmProvider: EvmProvider,
181-
strategyId: Address,
182-
): Promise<StrategyTimings> => {
183-
let results: [bigint, bigint, bigint, bigint] = [0n, 0n, 0n, 0n];
184-
185-
const contractCalls = [
186-
{
187-
abi: DonationVotingMerkleDistributionDirectTransferStrategy,
188-
functionName: "registrationStartTime",
189-
address: strategyId,
190-
},
191-
{
192-
abi: DonationVotingMerkleDistributionDirectTransferStrategy,
193-
functionName: "registrationEndTime",
194-
address: strategyId,
195-
},
196-
{
197-
abi: DonationVotingMerkleDistributionDirectTransferStrategy,
198-
functionName: "allocationStartTime",
199-
address: strategyId,
200-
},
201-
{
202-
abi: DonationVotingMerkleDistributionDirectTransferStrategy,
203-
functionName: "allocationEndTime",
204-
address: strategyId,
205-
},
206-
] as const;
207-
208-
if (evmProvider.getMulticall3Address()) {
209-
results = await evmProvider.multicall({
210-
contracts: contractCalls,
211-
allowFailure: false,
212-
});
213-
} else {
214-
results = (await Promise.all(
215-
contractCalls.map((call) =>
216-
evmProvider.readContract(call.address, call.abi, call.functionName),
217-
),
218-
)) as [bigint, bigint, bigint, bigint];
219-
}
220-
221-
return {
222-
applicationsStartTime: getDateFromTimestamp(results[0]),
223-
applicationsEndTime: getDateFromTimestamp(results[1]),
224-
donationsStartTime: getDateFromTimestamp(results[2]),
225-
donationsEndTime: getDateFromTimestamp(results[3]),
226-
};
227-
};
228-
8+
//TODO: move this to the DirectGrantsStrategyHandler when implemented
2299
/**
23010
* Gets the strategy data for the DirectGrantsStrategy
23111
* @param evmProvider - The evm provider

0 commit comments

Comments
 (0)