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 all 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
50 changes: 21 additions & 29 deletions packages/processors/src/allo/handlers/poolCreated.handler.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { getAddress, parseUnits, zeroAddress } from "viem";
import { getAddress, zeroAddress } from "viem";

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

import type { IEventHandler, ProcessorDependencies, StrategyTimings } from "../../internal.js";
import { getRoundRoles } from "../../helpers/roles.js";
import { extractStrategyFromId, getStrategyTimings } from "../../helpers/strategy.js";
import { calculateAmountInUsd } from "../../helpers/tokenMath.js";
import { TokenPriceNotFoundError } from "../../internal.js";
import { calculateAmountInUsd, getRoundRoles } from "../../helpers/index.js";
import { StrategyHandlerFactory, TokenPriceNotFoundError } from "../../internal.js";
import { RoundMetadataSchema } from "../../schemas/index.js";

type Dependencies = Pick<
Expand Down Expand Up @@ -61,7 +58,11 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated">
? zeroAddress
: checksummedTokenAddress;

const strategy = extractStrategyFromId(strategyId);
const strategyHandler = StrategyHandlerFactory.createHandler(
this.chainId,
this.dependencies as ProcessorDependencies,
strategyId,
);

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

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

let matchAmount = 0n;
let matchAmountInUsd = "0";

if (strategy) {
strategyTimings = await getStrategyTimings(evmProvider, strategy, strategyAddress);

//TODO: when creating strategy handlers, should this be moved there?
if (
strategy.name === "allov2.DonationVotingMerkleDistributionDirectTransferStrategy" &&
parsedRoundMetadata.success &&
token
) {
matchAmount = parseUnits(
parsedRoundMetadata.data.quadraticFundingConfig.matchingFundsAvailable.toString(),
token.decimals,
);
let matchAmountObj = {
matchAmount: 0n,
Copy link
Collaborator

Choose a reason for hiding this comment

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

hmm maybe worth naming the internal matchAmount something like matchAmountBigInt or maybe the outer matchAmount could be matchAmountObj?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i like that

matchAmountInUsd: "0",
};

matchAmountInUsd = await this.getTokenAmountInUsd(
if (strategyHandler) {
strategyTimings = await strategyHandler.fetchStrategyTimings(strategyAddress);
if (parsedRoundMetadata.success && token) {
matchAmountObj = await strategyHandler.fetchMatchAmount(
Number(parsedRoundMetadata.data.quadraticFundingConfig.matchingFundsAvailable),
token,
matchAmount,
this.event.blockTimestamp,
);
}
Expand Down Expand Up @@ -120,8 +112,8 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated">
totalAmountDonatedInUsd: "0",
uniqueDonorsCount: 0,
matchTokenAddress,
matchAmount,
matchAmountInUsd,
matchAmount: matchAmountObj.matchAmount,
matchAmountInUsd: matchAmountObj.matchAmountInUsd,
fundedAmount,
fundedAmountInUsd,
applicationMetadataCid: metadataPointer,
Expand All @@ -132,7 +124,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated">
...roundRoles,
strategyAddress,
strategyId,
strategyName: strategy?.name ?? "",
strategyName: strategyHandler?.name ?? "",
createdByAddress: getAddress(createdBy),
createdAtBlock: BigInt(this.event.blockNumber),
updatedAtBlock: BigInt(this.event.blockNumber),
Expand Down
3 changes: 3 additions & 0 deletions packages/processors/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from "./tokenPriceNotFound.exception.js";
export * from "./unsupportedEvent.exception.js";
export * from "./invalidArgument.exception.js";
export * from "./unsupportedStrategy.exception.js";
export * from "./projectNotFound.exception.js";
export * from "./roundNotFound.exception.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ChainId } from "@grants-stack-indexer/shared";

export class ProjectNotFound extends Error {
constructor(chainId: ChainId, anchorAddress: string) {
super(`Project not found for chainId: ${chainId} and anchorAddress: ${anchorAddress}`);
}
}
7 changes: 7 additions & 0 deletions packages/processors/src/exceptions/roundNotFound.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ChainId } from "@grants-stack-indexer/shared";

export class RoundNotFound extends Error {
constructor(chainId: ChainId, strategyAddress: string) {
super(`Round not found for chainId: ${chainId} and strategyAddress: ${strategyAddress}`);
}
}
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.

🤝

}
}
2 changes: 2 additions & 0 deletions packages/processors/src/external.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Add your external exports here
export { StrategyProcessor, AlloProcessor } from "./internal.js";
export type { IProcessor } from "./internal.js";

export { existsHandler } from "./internal.js";
3 changes: 3 additions & 0 deletions packages/processors/src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./roles.js";
export * from "./utils.js";
export * from "./tokenMath.js";
224 changes: 2 additions & 222 deletions packages/processors/src/helpers/strategy.ts
Original file line number Diff line number Diff line change
@@ -1,231 +1,11 @@
import type { EvmProvider } from "@grants-stack-indexer/chain-providers";
import type { Address, Branded } from "@grants-stack-indexer/shared";
import type { Address } from "@grants-stack-indexer/shared";

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

type SanitizedStrategyId = Branded<string, "SanitizedStrategyId">;
type Strategy = {
id: SanitizedStrategyId;
name: string | null;
// TODO: check if groups are required
groups: string[];
};

//TODO: refactor this into a mapping in Shared package from ID to the corresponding handler class
/*
* Extracts the strategy from the ID.
* @param _id - The ID of the strategy.
* @returns The strategy.
*/
export function extractStrategyFromId(_id: Address): Strategy | undefined {
const id = _id.toLowerCase();
/* eslint-disable no-fallthrough */
switch (id) {
// SQFSuperfluidv1
case "0xf8a14294e80ff012e54157ec9d1b2827421f1e7f6bde38c06730b1c031b3f935":
return {
id: id as SanitizedStrategyId,
name: "allov2.SQFSuperFluidStrategy",
groups: ["allov2.SQFSuperFluidStrategy"],
};

// MicroGrantsv1
case "0x697f0592ebd05466d2d24454477e11d69c475d7a7c4134f15ddc1ea9811bb16f":
return {
id: id as SanitizedStrategyId,
name: "allov2.MicroGrantsStrategy",
groups: ["allov2.MicroGrantsStrategy", "allov2.MicroGrantsCommon"],
};

// MicroGrantsGovv1
case "0x741ac1e2f387d83f219f6b5349d35ec34902cf94019d117335e0045d2e0ed912":
return {
id: id as SanitizedStrategyId,
name: "allov2.MicroGrantsGovStrategy",
groups: ["allov2.MicroGrantsGovStrategy", "allov2.MicroGrantsCommon"],
};

// MicroGrantsHatsv1
case "0x5aa24dcfcd55a1e059a172e987b3456736b4856c71e57aaf52e9a965897318dd":
return {
id: id as SanitizedStrategyId,
name: "allov2.MicroGrantsHatsStrategy",
groups: ["allov2.MicroGrantsHatsStrategy", "allov2.MicroGrantsCommon"],
};

// RFPSimpleStrategyv1.0
case "0x0d459e12d9e91d2b2a8fa12be8c7eb2b4f1c35e74573990c34b436613bc2350f":
return {
id: id as SanitizedStrategyId,
name: "allov2.RFPSimpleStrategy",
groups: ["allov2.RFPSimpleStrategy"],
};

// RFPCommitteeStrategyv1.0
case "0x7d143166a83c6a8a303ae32a6ccd287e48d79818f5d15d89e185391199909803":
return {
id: id as SanitizedStrategyId,
name: "allov2.RFPCommitteeStrategy",
groups: ["allov2.RFPCommitteeStrategy"],
};

// QVSimpleStrategyv1.0
case "0x22d006e191d6dc5ff1a25bb0733f47f64a9c34860b6703df88dea7cb3987b4c3":
return {
id: id as SanitizedStrategyId,
name: "allov2.QVSimpleStrategy",
groups: ["allov2.QVSimpleStrategy"],
};

// DonationVotingMerkleDistributionDirectTransferStrategyv1.0
case "0x6f9291df02b2664139cec5703c124e4ebce32879c74b6297faa1468aa5ff9ebf":
// DonationVotingMerkleDistributionDirectTransferStrategyv1.1
case "0x2f46bf157821dc41daa51479e94783bb0c8699eac63bf75ec450508ab03867ce":
// DonationVotingMerkleDistributionDirectTransferStrategyv2.0
case "0x2f0250d534b2d59b8b5cfa5eb0d0848a59ccbf5de2eaf72d2ba4bfe73dce7c6b":
// DonationVotingMerkleDistributionDirectTransferStrategyv2.1
case "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0":
return {
id: id as SanitizedStrategyId,
name: "allov2.DonationVotingMerkleDistributionDirectTransferStrategy",
groups: ["allov2.DonationVotingMerkleDistributionDirectTransferStrategy"],
};

// DonationVotingMerkleDistributionVaultStrategyv1.0
case "0x7e75375f0a7cd9f7ea159c8b065976e4f764f9dcef1edf692f31dd1842f70c87":
// DonationVotingMerkleDistributionVaultStrategyv1.1
case "0x093072375737c0e8872fef36808849aeba7f865e182d495f2b98308115c9ef13":
return {
id: id as SanitizedStrategyId,
name: "allov2.DonationVotingMerkleDistributionVaultStrategy",
groups: ["allov2.DonationVotingMerkleDistributionVaultStrategy"],
};

// DirectGrantsSimpleStrategyv1.1
case "0x263cb916541b6fc1fb5543a244829ccdba75264b097726e6ecc3c3cfce824bf5":
// DirectGrantsSimpleStrategyv2.1
case "0x53fb9d3bce0956ca2db5bb1441f5ca23050cb1973b33789e04a5978acfd9ca93":
return {
id: id as SanitizedStrategyId,
name: "allov2.DirectGrantsSimpleStrategy",
groups: ["allov2.DirectGrantsSimpleStrategy"],
};

// DirectGrantsLiteStrategyv1.0
case "0x103732a8e473467a510d4128ee11065262bdd978f0d9dad89ba68f2c56127e27":
return {
id: id as SanitizedStrategyId,
name: "allov2.DirectGrantsLiteStrategy",
groups: ["allov2.DirectGrantsLiteStrategy"],
};

// EasyRPGFStrategy1.0
case "0x662f5a0d3ea7e9b6ed1b351a9d96ac636a3c3ed727390aeff4ec931ae760d5ae":
return {
id: id as SanitizedStrategyId,
name: "allov2.EasyRPGFStrategy",
groups: ["allov2.EasyRPGFStrategy"],
};

// DirectAllocationStrategyv1.1
case "0x4cd0051913234cdd7d165b208851240d334786d6e5afbb4d0eec203515a9c6f3":
return {
id: id as SanitizedStrategyId,
name: "allov2.DirectAllocationStrategy",
groups: ["allov2.DirectAllocationStrategy"],
};
}

return undefined;
}

//TODO: refactor this into the StrategyHandler when implemented
// see if we can use a common interface or abstract class for all strategies
// so we don't have to do this switch statement
// most of the strategies don't need to fetch anything and just return null for all the times
export const getStrategyTimings = async (
evmProvider: EvmProvider,
strategy: Strategy,
strategyAddress: Address,
): Promise<StrategyTimings> => {
switch (strategy.name) {
case "allov2.DonationVotingMerkleDistributionDirectTransferStrategy":
return getDonationVotingMerkleDistributionDirectTransferStrategyTimings(
evmProvider,
strategyAddress,
);
case "allov2.DirectGrantsSimpleStrategy":
case "allov2.DirectGrantsLiteStrategy":
return getDirectGrantsStrategyTimings(evmProvider, strategyAddress);
default:
return {
applicationsStartTime: null,
applicationsEndTime: null,
donationsStartTime: null,
donationsEndTime: null,
};
}
};

/**
* Gets the strategy data for the DonationVotingMerkleDistributionDirectTransferStrategy
* @param evmProvider - The evm provider
* @param strategyId - The address of the strategy
* @returns The strategy data
*/
export const getDonationVotingMerkleDistributionDirectTransferStrategyTimings = async (
evmProvider: EvmProvider,
strategyId: Address,
): Promise<StrategyTimings> => {
let results: [bigint, bigint, bigint, bigint] = [0n, 0n, 0n, 0n];

const contractCalls = [
{
abi: DonationVotingMerkleDistributionDirectTransferStrategy,
functionName: "registrationStartTime",
address: strategyId,
},
{
abi: DonationVotingMerkleDistributionDirectTransferStrategy,
functionName: "registrationEndTime",
address: strategyId,
},
{
abi: DonationVotingMerkleDistributionDirectTransferStrategy,
functionName: "allocationStartTime",
address: strategyId,
},
{
abi: DonationVotingMerkleDistributionDirectTransferStrategy,
functionName: "allocationEndTime",
address: strategyId,
},
] as const;

if (evmProvider.getMulticall3Address()) {
results = await evmProvider.multicall({
contracts: contractCalls,
allowFailure: false,
});
} else {
results = (await Promise.all(
contractCalls.map((call) =>
evmProvider.readContract(call.address, call.abi, call.functionName),
),
)) as [bigint, bigint, bigint, bigint];
}

return {
applicationsStartTime: getDateFromTimestamp(results[0]),
applicationsEndTime: getDateFromTimestamp(results[1]),
donationsStartTime: getDateFromTimestamp(results[2]),
donationsEndTime: getDateFromTimestamp(results[3]),
};
};

//TODO: move this to the DirectGrantsStrategyHandler when implemented
/**
* Gets the strategy data for the DirectGrantsStrategy
* @param evmProvider - The evm provider
Expand Down
Loading