From afba646dd8decb6179ee5e9e9961378d3c40ecae Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:40:53 -0300 Subject: [PATCH] fix: pr comments --- .../src/providers/evmProvider.ts | 5 + .../providers/evmProvider.service.spec.ts | 15 + packages/processors/package.json | 1 + .../processors/src/allo/allo.processor.ts | 22 +- .../src/allo/handlers/poolCreated.handler.ts | 289 +++++++----------- packages/processors/src/exceptions/index.ts | 1 + .../tokenPriceNotFound.exception.ts | 7 + packages/processors/src/helpers/roles.ts | 18 ++ packages/processors/src/helpers/strategy.ts | 193 +++++------- packages/processors/src/internal.ts | 2 + packages/processors/src/types/index.ts | 2 + .../processors/src/types/processor.types.ts | 15 + .../processors/src/types/strategy.types.ts | 9 + .../test/allo/allo.processor.spec.ts | 27 +- .../allo/handlers/poolCreated.handler.spec.ts | 116 +++---- .../processors/test/helpers/utils.spec.ts | 10 +- packages/shared/src/types/events/allo.ts | 5 +- pnpm-lock.yaml | 3 + 18 files changed, 344 insertions(+), 396 deletions(-) create mode 100644 packages/processors/src/exceptions/index.ts create mode 100644 packages/processors/src/exceptions/tokenPriceNotFound.exception.ts create mode 100644 packages/processors/src/helpers/roles.ts create mode 100644 packages/processors/src/types/index.ts create mode 100644 packages/processors/src/types/processor.types.ts create mode 100644 packages/processors/src/types/strategy.types.ts diff --git a/packages/chain-providers/src/providers/evmProvider.ts b/packages/chain-providers/src/providers/evmProvider.ts index 22c048a..65df554 100644 --- a/packages/chain-providers/src/providers/evmProvider.ts +++ b/packages/chain-providers/src/providers/evmProvider.ts @@ -16,6 +16,7 @@ import { fallback, FallbackTransport, GetBlockReturnType, + GetTransactionReturnType, Hex, http, HttpTransport, @@ -65,6 +66,10 @@ export class EvmProvider { return this.chain?.contracts?.multicall3?.address; } + async getTransaction(hash: Hex): Promise { + return this.client.getTransaction({ hash }); + } + /** * Retrieves the balance of the specified address. * @param {Address} address The address for which to retrieve the balance. diff --git a/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts b/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts index da4681a..ca0b22f 100644 --- a/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts +++ b/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts @@ -14,6 +14,7 @@ import { import { arrayAbiFixture, structAbiFixture } from "../../fixtures/batchRequest.fixture.js"; const mockClient = { + getTransaction: vi.fn(), getBalance: vi.fn(), getBlockNumber: vi.fn(), getGasPrice: vi.fn(), @@ -70,6 +71,20 @@ describe("EvmProvider", () => { }).toThrowError(RpcUrlsEmpty); }); + describe("getTransaction", () => { + it("returns the transaction for the given hash", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const hash = "0x123456789"; + const expectedTransaction = { from: "0x123456789", to: "0x987654321" }; + vi.spyOn(mockClient, "getTransaction").mockResolvedValue(expectedTransaction); + + const transaction = await viemProvider.getTransaction(hash); + + expect(transaction).toBe(expectedTransaction); + expect(mockClient.getTransaction).toHaveBeenCalledWith({ hash }); + }); + }); + describe("getBalance", () => { it("returns the balance of the specified address", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); diff --git a/packages/processors/package.json b/packages/processors/package.json index 95643e1..97af619 100644 --- a/packages/processors/package.json +++ b/packages/processors/package.json @@ -28,6 +28,7 @@ "test:cov": "vitest run --config vitest.config.ts --coverage" }, "dependencies": { + "@grants-stack-indexer/chain-providers": "workspace:*", "@grants-stack-indexer/metadata": "workspace:*", "@grants-stack-indexer/pricing": "workspace:*", "@grants-stack-indexer/repository": "workspace:*", diff --git a/packages/processors/src/allo/allo.processor.ts b/packages/processors/src/allo/allo.processor.ts index 3735c46..88e93a1 100644 --- a/packages/processors/src/allo/allo.processor.ts +++ b/packages/processors/src/allo/allo.processor.ts @@ -1,31 +1,23 @@ -import { Chain, PublicClient, Transport } from "viem"; - -import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; -import type { IPricingProvider } from "@grants-stack-indexer/pricing"; -import { Changeset, IRoundReadRepository } from "@grants-stack-indexer/repository"; +import { Changeset } from "@grants-stack-indexer/repository"; import { AlloEvent, ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; -import type { IProcessor } from "../internal.js"; +import type { IProcessor, ProcessorDependencies } from "../internal.js"; import { PoolCreatedHandler } from "./handlers/index.js"; export class AlloProcessor implements IProcessor<"Allo", AlloEvent> { constructor( private readonly chainId: ChainId, - //TODO: replace with provider abstraction - private readonly viemClient: PublicClient, - private readonly pricingProvider: IPricingProvider, - private readonly metadataProvider: IMetadataProvider, - private readonly roundRepository: IRoundReadRepository, + private readonly dependencies: ProcessorDependencies, ) {} async process(event: ProtocolEvent<"Allo", AlloEvent>): Promise { switch (event.eventName) { case "PoolCreated": return new PoolCreatedHandler(event, this.chainId, { - viemClient: this.viemClient, - pricingProvider: this.pricingProvider, - metadataProvider: this.metadataProvider, - roundRepository: this.roundRepository, + evmProvider: this.dependencies.evmProvider, + pricingProvider: this.dependencies.pricingProvider, + metadataProvider: this.dependencies.metadataProvider, + roundRepository: this.dependencies.roundRepository, }).handle(); default: throw new Error(`Unknown event name: ${event.eventName}`); diff --git a/packages/processors/src/allo/handlers/poolCreated.handler.ts b/packages/processors/src/allo/handlers/poolCreated.handler.ts index fb9317d..5315716 100644 --- a/packages/processors/src/allo/handlers/poolCreated.handler.ts +++ b/packages/processors/src/allo/handlers/poolCreated.handler.ts @@ -1,29 +1,23 @@ -import { - Address, - encodePacked, - getAddress, - keccak256, - pad, - parseUnits, - PublicClient, - zeroAddress, -} from "viem"; +import { Address, getAddress, parseUnits, zeroAddress } from "viem"; -import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; -import type { IPricingProvider } from "@grants-stack-indexer/pricing"; -import type { - Changeset, - IRoundReadRepository, - NewRound, - PendingRoundRole, -} from "@grants-stack-indexer/repository"; +import type { Changeset, NewRound, PendingRoundRole } from "@grants-stack-indexer/repository"; import type { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; import { isAlloNativeToken } from "@grants-stack-indexer/shared/"; -import type { IEventHandler } from "../../internal.js"; +import type { IEventHandler, ProcessorDependencies, StrategyTimings } from "../../internal.js"; +import { getRoundRoles } from "../../helpers/roles.js"; import { RoundMetadataSchema } from "../../helpers/schemas.js"; import { extractStrategyFromId, getStrategyTimings } from "../../helpers/strategy.js"; import { calculateAmountInUsd } from "../../helpers/tokenMath.js"; +import { TokenPriceNotFoundError } from "../../internal.js"; + +type Dependencies = Pick< + ProcessorDependencies, + "evmProvider" | "pricingProvider" | "metadataProvider" | "roundRepository" +>; + +// sometimes coingecko returns no prices for 1 hour range, 2 hours works better +const TIMESTAMP_DELTA_RANGE = 2 * 60 * 60 * 1000; /** /** @@ -37,29 +31,15 @@ import { calculateAmountInUsd } from "../../helpers/tokenMath.js"; * - Creates a new round object */ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> { - private readonly viemClient: PublicClient; - private readonly pricingProvider: IPricingProvider; - private readonly metadataProvider: IMetadataProvider; - private readonly roundRepository: IRoundReadRepository; - constructor( readonly event: ProtocolEvent<"Allo", "PoolCreated">, private readonly chainId: ChainId, - dependencies: { - viemClient: PublicClient; - pricingProvider: IPricingProvider; - metadataProvider: IMetadataProvider; - roundRepository: IRoundReadRepository; - }, - ) { - this.viemClient = dependencies.viemClient; - this.pricingProvider = dependencies.pricingProvider; - this.metadataProvider = dependencies.metadataProvider; - this.roundRepository = dependencies.roundRepository; - } + private readonly dependencies: Dependencies, + ) {} async handle(): Promise { - const { pointer: metadataPointer } = this.event.params.metadata; + const { metadataProvider, evmProvider } = this.dependencies; + const [metadataPointer] = this.event.params.metadata; const { poolId, strategyId, @@ -69,149 +49,114 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> } = this.event.params; const { hash: txHash, from: txFrom } = this.event.transactionFields; - try { - const metadata = await this.metadataProvider.getMetadata<{ - round?: unknown; - application?: unknown; - }>(metadataPointer); - const parsedRoundMetadata = RoundMetadataSchema.safeParse(metadata?.round); - - const checksummedTokenAddress = getAddress(tokenAddress); - const matchTokenAddress = isAlloNativeToken(checksummedTokenAddress) - ? zeroAddress - : checksummedTokenAddress; - - const strategy = extractStrategyFromId(strategyId); - - // TODO: get token for the chain - const token = { - address: matchTokenAddress, - decimals: 18, //TODO: get decimals from token - symbol: "USDC", //TODO: get symbol from token - name: "USDC", //TODO: get name from token - }; - - let strategyTimings: { - applicationsStartTime: Date | null; - applicationsEndTime: Date | null; - donationsStartTime: Date | null; - donationsEndTime: Date | null; - } = { - applicationsStartTime: null, - applicationsEndTime: null, - donationsStartTime: null, - donationsEndTime: null, - }; - - let matchAmount = 0n; - let matchAmountInUsd = 0; - - if (strategy) { - strategyTimings = await getStrategyTimings( - this.viemClient, - strategy, - strategyAddress, + const metadata = await metadataProvider.getMetadata<{ + round?: unknown; + application?: unknown; + }>(metadataPointer); + const parsedRoundMetadata = RoundMetadataSchema.safeParse(metadata?.round); + + const checksummedTokenAddress = getAddress(tokenAddress); + const matchTokenAddress = isAlloNativeToken(checksummedTokenAddress) + ? zeroAddress + : checksummedTokenAddress; + + const strategy = extractStrategyFromId(strategyId); + + // TODO: get token for the chain + const token = { + address: matchTokenAddress, + decimals: 18, //TODO: get decimals from token + symbol: "USDC", //TODO: get symbol from token + name: "USDC", //TODO: get name from token + }; + + let strategyTimings: StrategyTimings = { + applicationsStartTime: null, + applicationsEndTime: null, + donationsStartTime: null, + 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 !== null + ) { + matchAmount = parseUnits( + parsedRoundMetadata.data.quadraticFundingConfig.matchingFundsAvailable.toString(), + token.decimals, ); - //when creating strategy handlers, should this be moved there? - if ( - strategy.name === - "allov2.DonationVotingMerkleDistributionDirectTransferStrategy" && - parsedRoundMetadata.success && - token !== null - ) { - matchAmount = parseUnits( - parsedRoundMetadata.data.quadraticFundingConfig.matchingFundsAvailable.toString(), - token.decimals, - ); - - matchAmountInUsd = await this.getTokenAmountInUsd( - token, - matchAmount, - this.event.blockTimestamp, - ); - } - } - - let fundedAmountInUsd = 0; - - if (token !== null && fundedAmount > 0n) { - fundedAmountInUsd = await this.getTokenAmountInUsd( + matchAmountInUsd = await this.getTokenAmountInUsd( token, - fundedAmount, + matchAmount, this.event.blockTimestamp, ); } + } - // transaction sender - const createdBy = - txFrom ?? (await this.viemClient.getTransaction({ hash: txHash })).from; - - const roundRoles = this.getRoundRoles(poolId); + let fundedAmountInUsd = 0; - const newRound: NewRound = { - chainId: this.chainId, - id: poolId.toString(), - tags: ["allo-v2", ...(parsedRoundMetadata.success ? ["grants-stack"] : [])], - totalDonationsCount: 0, - totalAmountDonatedInUsd: 0, - uniqueDonorsCount: 0, - matchTokenAddress, - matchAmount, - matchAmountInUsd, + if (token !== null && fundedAmount > 0n) { + fundedAmountInUsd = await this.getTokenAmountInUsd( + token, fundedAmount, - fundedAmountInUsd, - applicationMetadataCid: metadataPointer, - applicationMetadata: metadata?.application ?? {}, - roundMetadataCid: metadataPointer, - roundMetadata: metadata?.round ?? null, - ...strategyTimings, - ...roundRoles, - strategyAddress, - strategyId, - strategyName: strategy?.name ?? "", - createdByAddress: getAddress(createdBy), - createdAtBlock: BigInt(this.event.blockNumber), - updatedAtBlock: BigInt(this.event.blockNumber), - projectId: this.event.params.profileId, - totalDistributed: 0n, - readyForPayoutTransaction: null, - matchingDistribution: null, - }; - - const changes: Changeset[] = [ - { - type: "InsertRound", - args: { round: newRound }, - }, - ]; - - changes.push(...(await this.handlePendingRoles(this.chainId, poolId.toString()))); - - return changes; - } catch (error: unknown) { - console.error( - `An error occurred while processing the PoolCreated event. Event: ${this.event} - Error: ${error}`, + this.event.blockTimestamp, ); - return []; } - } - /** - * Get the manager and admin roles for the pool - * Note: POOL_MANAGER_ROLE = bytes32(poolId); - * Note: POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, "admin")); - * @param poolId - The ID of the pool. - * @returns The manager and admin roles. - */ - private getRoundRoles(poolId: bigint): { managerRole: string; adminRole: string } { - // POOL_MANAGER_ROLE = bytes32(poolId); - const managerRole = pad(`0x${poolId.toString(16)}`); + // transaction sender + const createdBy = txFrom ?? (await evmProvider.getTransaction(txHash)).from; + + const roundRoles = getRoundRoles(poolId); + + const newRound: NewRound = { + chainId: this.chainId, + id: poolId.toString(), + tags: ["allo-v2", ...(parsedRoundMetadata.success ? ["grants-stack"] : [])], + totalDonationsCount: 0, + totalAmountDonatedInUsd: 0, + uniqueDonorsCount: 0, + matchTokenAddress, + matchAmount, + matchAmountInUsd, + fundedAmount, + fundedAmountInUsd, + applicationMetadataCid: metadataPointer, + applicationMetadata: metadata?.application ?? {}, + roundMetadataCid: metadataPointer, + roundMetadata: metadata?.round ?? null, + ...strategyTimings, + ...roundRoles, + strategyAddress, + strategyId, + strategyName: strategy?.name ?? "", + createdByAddress: getAddress(createdBy), + createdAtBlock: BigInt(this.event.blockNumber), + updatedAtBlock: BigInt(this.event.blockNumber), + projectId: this.event.params.profileId, + totalDistributed: 0n, + readyForPayoutTransaction: null, + matchingDistribution: null, + }; + + const changes: Changeset[] = [ + { + type: "InsertRound", + args: { round: newRound }, + }, + ]; + + changes.push(...(await this.handlePendingRoles(this.chainId, poolId.toString()))); - // POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, "admin")); - const adminRawRole = encodePacked(["uint256", "string"], [poolId, "admin"]); - const adminRole = keccak256(adminRawRole); - return { managerRole, adminRole }; + return changes; } /** @@ -226,11 +171,12 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> * pending roles to actual round roles. */ private async handlePendingRoles(chainId: ChainId, roundId: string): Promise { + const { roundRepository } = this.dependencies; const changes: Changeset[] = []; const allPendingRoles: PendingRoundRole[] = []; for (const roleName of ["admin", "manager"] as const) { - const pendingRoles = await this.roundRepository.getPendingRoundRoles(chainId, roleName); + const pendingRoles = await roundRepository.getPendingRoundRoles(chainId, roleName); for (const pr of pendingRoles) { changes.push({ type: "InsertRoundRole", @@ -264,15 +210,16 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> amount: bigint, timestamp: number, ): Promise { - const tokenPrice = await this.pricingProvider.getTokenPrice( + const { pricingProvider } = this.dependencies; + const tokenPrice = await pricingProvider.getTokenPrice( this.chainId, token.address, timestamp, - timestamp + 1200000, + timestamp + TIMESTAMP_DELTA_RANGE, ); if (!tokenPrice) { - throw new Error("Token price not found"); + throw new TokenPriceNotFoundError(token.address, timestamp); } return calculateAmountInUsd(amount, tokenPrice.priceUsd, token.decimals); diff --git a/packages/processors/src/exceptions/index.ts b/packages/processors/src/exceptions/index.ts new file mode 100644 index 0000000..4bb65ad --- /dev/null +++ b/packages/processors/src/exceptions/index.ts @@ -0,0 +1 @@ +export * from "./tokenPriceNotFound.exception.js"; diff --git a/packages/processors/src/exceptions/tokenPriceNotFound.exception.ts b/packages/processors/src/exceptions/tokenPriceNotFound.exception.ts new file mode 100644 index 0000000..77b6906 --- /dev/null +++ b/packages/processors/src/exceptions/tokenPriceNotFound.exception.ts @@ -0,0 +1,7 @@ +import { Address } from "@grants-stack-indexer/shared"; + +export class TokenPriceNotFoundError extends Error { + constructor(tokenAddress: Address, timestamp: number) { + super(`Token price not found for token ${tokenAddress} at timestamp ${timestamp}`); + } +} diff --git a/packages/processors/src/helpers/roles.ts b/packages/processors/src/helpers/roles.ts new file mode 100644 index 0000000..5eb5a88 --- /dev/null +++ b/packages/processors/src/helpers/roles.ts @@ -0,0 +1,18 @@ +import { encodePacked, keccak256, pad } from "viem"; + +/** + * Get the manager and admin roles for the pool + * Note: POOL_MANAGER_ROLE = bytes32(poolId); + * Note: POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, "admin")); + * @param poolId - The ID of the pool. + * @returns The manager and admin roles. + */ +export const getRoundRoles = (poolId: bigint): { managerRole: string; adminRole: string } => { + // POOL_MANAGER_ROLE = bytes32(poolId); + const managerRole = pad(`0x${poolId.toString(16)}`); + + // POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, "admin")); + const adminRawRole = encodePacked(["uint256", "string"], [poolId, "admin"]); + const adminRole = keccak256(adminRawRole); + return { managerRole, adminRole }; +}; diff --git a/packages/processors/src/helpers/strategy.ts b/packages/processors/src/helpers/strategy.ts index bf0ef57..76192e7 100644 --- a/packages/processors/src/helpers/strategy.ts +++ b/packages/processors/src/helpers/strategy.ts @@ -1,9 +1,9 @@ -import { Address, PublicClient } from "viem"; - -import { Branded } from "@grants-stack-indexer/shared"; +import type { EvmProvider } from "@grants-stack-indexer/chain-providers"; +import type { Address, Branded } 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; @@ -143,24 +143,19 @@ export function extractStrategyFromId(_id: Address): Strategy | undefined { //TODO: refactor this into the StrategyHandler when implemented export const getStrategyTimings = async ( - viemClient: PublicClient, + evmProvider: EvmProvider, strategy: Strategy, strategyAddress: Address, -): Promise<{ - applicationsStartTime: Date | null; - applicationsEndTime: Date | null; - donationsStartTime: Date | null; - donationsEndTime: Date | null; -}> => { +): Promise => { switch (strategy.name) { case "allov2.DonationVotingMerkleDistributionDirectTransferStrategy": return getDonationVotingMerkleDistributionDirectTransferStrategyTimings( - viemClient, + evmProvider, strategyAddress, ); case "allov2.DirectGrantsSimpleStrategy": case "allov2.DirectGrantsLiteStrategy": - return getDirectGrantsStrategyTimings(viemClient, strategyAddress); + return getDirectGrantsStrategyTimings(evmProvider, strategyAddress); default: return { applicationsStartTime: null, @@ -178,83 +173,52 @@ export const getStrategyTimings = async ( * @returns The strategy data */ export const getDonationVotingMerkleDistributionDirectTransferStrategyTimings = async ( - viemClient: PublicClient, + evmProvider: EvmProvider, strategyId: Address, -): Promise<{ - applicationsStartTime: Date | null; - applicationsEndTime: Date | null; - donationsStartTime: Date | null; - donationsEndTime: Date | null; -}> => { - let registrationStartTimeResolved: bigint; - let registrationEndTimeResolved: bigint; - let allocationStartTimeResolved: bigint; - let allocationEndTimeResolved: bigint; +): Promise => { + 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 (viemClient.chain?.contracts?.multicall3) { - const results = await viemClient.multicall({ - contracts: [ - { - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "registrationStartTime", - address: strategyId, - }, - { - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "registrationEndTime", - address: strategyId, - }, - { - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "allocationStartTime", - address: strategyId, - }, - { - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "allocationEndTime", - address: strategyId, - }, - ], + if (evmProvider.getMulticall3Address()) { + results = await evmProvider.multicall({ + contracts: contractCalls, allowFailure: false, }); - registrationStartTimeResolved = results[0]; - registrationEndTimeResolved = results[1]; - allocationStartTimeResolved = results[2]; - allocationEndTimeResolved = results[3]; } else { - const results = await Promise.all([ - viemClient.readContract({ - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "registrationStartTime", - address: strategyId, - }), - viemClient.readContract({ - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "registrationEndTime", - address: strategyId, - }), - viemClient.readContract({ - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "allocationStartTime", - address: strategyId, - }), - viemClient.readContract({ - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "allocationEndTime", - address: strategyId, - }), - ]); - registrationStartTimeResolved = results[0]; - registrationEndTimeResolved = results[1]; - allocationStartTimeResolved = results[2]; - allocationEndTimeResolved = results[3]; + results = (await Promise.all( + contractCalls.map((call) => + evmProvider.readContract(call.address, call.abi, call.functionName), + ), + )) as [bigint, bigint, bigint, bigint]; } return { - applicationsStartTime: getDateFromTimestamp(registrationStartTimeResolved), - applicationsEndTime: getDateFromTimestamp(registrationEndTimeResolved), - donationsStartTime: getDateFromTimestamp(allocationStartTimeResolved), - donationsEndTime: getDateFromTimestamp(allocationEndTimeResolved), + applicationsStartTime: getDateFromTimestamp(results[0]), + applicationsEndTime: getDateFromTimestamp(results[1]), + donationsStartTime: getDateFromTimestamp(results[2]), + donationsEndTime: getDateFromTimestamp(results[3]), }; }; @@ -265,55 +229,40 @@ export const getDonationVotingMerkleDistributionDirectTransferStrategyTimings = * @returns The strategy data */ export const getDirectGrantsStrategyTimings = async ( - viemClient: PublicClient, + evmProvider: EvmProvider, strategyAddress: Address, -): Promise<{ - applicationsStartTime: Date | null; - applicationsEndTime: Date | null; - donationsStartTime: Date | null; - donationsEndTime: Date | null; -}> => { - let registrationStartTimeResolved: bigint; - let registrationEndTimeResolved: bigint; +): Promise => { + let results: [bigint, bigint] = [0n, 0n]; + + const contractCalls = [ + { + abi: DirectGrantsLiteStrategy, + functionName: "registrationStartTime", + address: strategyAddress, + }, + { + abi: DirectGrantsLiteStrategy, + functionName: "registrationEndTime", + address: strategyAddress, + }, + ] as const; - if (viemClient.chain?.contracts?.multicall3) { - const results = await viemClient.multicall({ - contracts: [ - { - abi: DirectGrantsLiteStrategy, - functionName: "registrationStartTime", - address: strategyAddress, - }, - { - abi: DirectGrantsLiteStrategy, - functionName: "registrationEndTime", - address: strategyAddress, - }, - ], + if (evmProvider.getMulticall3Address()) { + results = await evmProvider.multicall({ + contracts: contractCalls, allowFailure: false, }); - registrationStartTimeResolved = results[0]; - registrationEndTimeResolved = results[1]; } else { - const results = await Promise.all([ - viemClient.readContract({ - abi: DirectGrantsLiteStrategy, - functionName: "registrationStartTime", - address: strategyAddress, - }), - viemClient.readContract({ - abi: DirectGrantsLiteStrategy, - functionName: "registrationEndTime", - address: strategyAddress, - }), - ]); - registrationStartTimeResolved = results[0]; - registrationEndTimeResolved = results[1]; + results = (await Promise.all( + contractCalls.map((call) => + evmProvider.readContract(call.address, call.abi, call.functionName), + ), + )) as [bigint, bigint]; } return { - applicationsStartTime: getDateFromTimestamp(registrationStartTimeResolved), - applicationsEndTime: getDateFromTimestamp(registrationEndTimeResolved), + applicationsStartTime: getDateFromTimestamp(results[0]), + applicationsEndTime: getDateFromTimestamp(results[1]), donationsStartTime: null, donationsEndTime: null, }; diff --git a/packages/processors/src/internal.ts b/packages/processors/src/internal.ts index 10f9b05..9b54404 100644 --- a/packages/processors/src/internal.ts +++ b/packages/processors/src/internal.ts @@ -1,4 +1,6 @@ // Add your internal exports here +export * from "./types/index.js"; export * from "./interfaces/index.js"; +export * from "./exceptions/index.js"; export * from "./allo/index.js"; export * from "./strategy/index.js"; diff --git a/packages/processors/src/types/index.ts b/packages/processors/src/types/index.ts new file mode 100644 index 0000000..cc56870 --- /dev/null +++ b/packages/processors/src/types/index.ts @@ -0,0 +1,2 @@ +export * from "./processor.types.js"; +export * from "./strategy.types.js"; diff --git a/packages/processors/src/types/processor.types.ts b/packages/processors/src/types/processor.types.ts new file mode 100644 index 0000000..101a7aa --- /dev/null +++ b/packages/processors/src/types/processor.types.ts @@ -0,0 +1,15 @@ +import type { EvmProvider } from "@grants-stack-indexer/chain-providers"; +import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import type { IPricingProvider } from "@grants-stack-indexer/pricing"; +import type { + IProjectReadRepository, + IRoundReadRepository, +} from "@grants-stack-indexer/repository"; + +export type ProcessorDependencies = { + evmProvider: EvmProvider; + pricingProvider: IPricingProvider; + metadataProvider: IMetadataProvider; + roundRepository: IRoundReadRepository; + projectRepository: IProjectReadRepository; +}; diff --git a/packages/processors/src/types/strategy.types.ts b/packages/processors/src/types/strategy.types.ts new file mode 100644 index 0000000..fc206a7 --- /dev/null +++ b/packages/processors/src/types/strategy.types.ts @@ -0,0 +1,9 @@ +/** + * This type represents the time fields for a strategy. + */ +export type StrategyTimings = { + applicationsStartTime: Date | null; + applicationsEndTime: Date | null; + donationsStartTime: Date | null; + donationsEndTime: Date | null; +}; diff --git a/packages/processors/test/allo/allo.processor.spec.ts b/packages/processors/test/allo/allo.processor.spec.ts index 8e7119b..7d0967b 100644 --- a/packages/processors/test/allo/allo.processor.spec.ts +++ b/packages/processors/test/allo/allo.processor.spec.ts @@ -1,9 +1,12 @@ -import { Chain, PublicClient, Transport } from "viem"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { EvmProvider } from "@grants-stack-indexer/chain-providers"; import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; import type { IPricingProvider } from "@grants-stack-indexer/pricing"; -import type { IRoundReadRepository } from "@grants-stack-indexer/repository"; +import type { + IProjectReadRepository, + IRoundReadRepository, +} from "@grants-stack-indexer/repository"; import type { AlloEvent, ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; import { AlloProcessor } from "../../src/allo/allo.processor.js"; @@ -22,24 +25,24 @@ vi.mock("../../src/allo/handlers/poolCreated.handler.js", () => { describe("AlloProcessor", () => { const mockChainId = 10 as ChainId; let processor: AlloProcessor; - let mockViemClient: PublicClient; + let mockEvmProvider: EvmProvider; let mockPricingProvider: IPricingProvider; let mockMetadataProvider: IMetadataProvider; let mockRoundRepository: IRoundReadRepository; beforeEach(() => { - mockViemClient = {} as PublicClient; + mockEvmProvider = {} as EvmProvider; mockPricingProvider = {} as IPricingProvider; mockMetadataProvider = {} as IMetadataProvider; mockRoundRepository = {} as IRoundReadRepository; - processor = new AlloProcessor( - mockChainId, - mockViemClient, - mockPricingProvider, - mockMetadataProvider, - mockRoundRepository, - ); + processor = new AlloProcessor(mockChainId, { + evmProvider: mockEvmProvider, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: {} as IProjectReadRepository, + }); // Reset mocks before each test vi.clearAllMocks(); @@ -56,7 +59,7 @@ describe("AlloProcessor", () => { await processor.process(mockEvent); expect(PoolCreatedHandler).toHaveBeenCalledWith(mockEvent, mockChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, diff --git a/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts b/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts index 5fc396d..1332cbd 100644 --- a/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts +++ b/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts @@ -1,23 +1,14 @@ -import { Chain, GetTransactionReturnType, parseUnits, PublicClient, Transport } from "viem"; +import { GetTransactionReturnType, parseUnits } from "viem"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { EvmProvider } from "@grants-stack-indexer/chain-providers"; +import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import type { IPricingProvider } from "@grants-stack-indexer/pricing"; +import type { IRoundReadRepository, Round } from "@grants-stack-indexer/repository"; import type { ChainId, DeepPartial, ProtocolEvent } from "@grants-stack-indexer/shared"; -import { IMetadataProvider } from "@grants-stack-indexer/metadata"; -import { IPricingProvider } from "@grants-stack-indexer/pricing"; -import { IRoundReadRepository, Round } from "@grants-stack-indexer/repository"; import { mergeDeep } from "@grants-stack-indexer/shared"; import { PoolCreatedHandler } from "../../../src/allo/handlers/poolCreated.handler.js"; -import * as strategy from "../../../src/helpers/strategy.js"; - -vi.mock("../../../src/helpers/strategy.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - getDonationVotingMerkleDistributionDirectTransferStrategyTimings: vi.fn(), - getDirectGrantsStrategyTimings: vi.fn(), - }; -}); // Function to create a mock event with optional overrides function createMockEvent( @@ -38,10 +29,7 @@ function createMockEvent( strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", amount: 0n, token: "0x4200000000000000000000000000000000000042", - metadata: { - pointer: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", - protocol: 1n, - }, + metadata: ["bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", 1n], }, transactionFields: { hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", @@ -54,16 +42,18 @@ function createMockEvent( } describe("PoolCreatedHandler", () => { - let mockViemClient: PublicClient; + let mockEvmProvider: EvmProvider; let mockPricingProvider: IPricingProvider; let mockMetadataProvider: IMetadataProvider; let mockRoundRepository: IRoundReadRepository; beforeEach(() => { - mockViemClient = { + mockEvmProvider = { readContract: vi.fn(), getTransaction: vi.fn(), - } as unknown as PublicClient; + multicall: vi.fn(), + getMulticall3Address: vi.fn().mockRejectedValue("0xmulticall3"), + } as unknown as EvmProvider; mockPricingProvider = { getTokenPrice: vi.fn(), }; @@ -92,7 +82,7 @@ describe("PoolCreatedHandler", () => { vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, @@ -119,7 +109,7 @@ describe("PoolCreatedHandler", () => { vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, @@ -139,10 +129,7 @@ describe("PoolCreatedHandler", () => { createdByAddress: mockEvent.transactionFields.from, }); expect(mockPricingProvider.getTokenPrice).not.toHaveBeenCalled(); - expect(strategy.getDirectGrantsStrategyTimings).not.toHaveBeenCalled(); - expect( - strategy.getDonationVotingMerkleDistributionDirectTransferStrategyTimings, - ).not.toHaveBeenCalled(); + expect(mockEvmProvider.multicall).not.toHaveBeenCalled(); }); it("process a DonationVotingMerkleDistributionDirectTransferStrategy", async () => { @@ -165,15 +152,12 @@ describe("PoolCreatedHandler", () => { priceUsd: 100, timestampMs: 1708369911, }); - vi.spyOn( - strategy, - "getDonationVotingMerkleDistributionDirectTransferStrategyTimings", - ).mockResolvedValue({ - applicationsStartTime: new Date(), - applicationsEndTime: new Date(), - donationsStartTime: new Date(), - donationsEndTime: new Date(), - }); + vi.spyOn(mockEvmProvider, "multicall").mockResolvedValue([ + 1609459200n, + 1609459200n, + 1609459200n, + 1609459200n, + ]); vi.spyOn(mockRoundRepository, "getPendingRoundRoles") .mockResolvedValueOnce([ @@ -187,7 +171,7 @@ describe("PoolCreatedHandler", () => { .mockResolvedValue([]); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, @@ -223,6 +207,10 @@ describe("PoolCreatedHandler", () => { matchingFundsAvailable: 1, }, }, + applicationsStartTime: new Date("2021-01-01T00:00:00.000Z"), + applicationsEndTime: new Date("2021-01-01T00:00:00.000Z"), + donationsStartTime: new Date("2021-01-01T00:00:00.000Z"), + donationsEndTime: new Date("2021-01-01T00:00:00.000Z"), strategyAddress: mockEvent.params.contractAddress, strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", strategyName: "allov2.DonationVotingMerkleDistributionDirectTransferStrategy", @@ -236,6 +224,7 @@ describe("PoolCreatedHandler", () => { }); expect(mockPricingProvider.getTokenPrice).toHaveBeenCalled(); expect(mockMetadataProvider.getMetadata).toHaveBeenCalled(); + expect(mockEvmProvider.multicall).toHaveBeenCalled(); }); it("fetches transaction sender if not present in event", async () => { @@ -252,12 +241,12 @@ describe("PoolCreatedHandler", () => { timestampMs: 1708369911, }); vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); - vi.spyOn(mockViemClient, "getTransaction").mockResolvedValue({ + vi.spyOn(mockEvmProvider, "getTransaction").mockResolvedValue({ from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", } as unknown as GetTransactionReturnType); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, @@ -269,28 +258,25 @@ describe("PoolCreatedHandler", () => { expect(changeset.args.round.createdByAddress).toBe( "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", ); - expect(mockViemClient.getTransaction).toHaveBeenCalledWith({ - hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", - }); + expect(mockEvmProvider.getTransaction).toHaveBeenCalledWith( + "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", + ); }); it("handles an undefined metadata", async () => { const mockEvent = createMockEvent(); vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); - vi.spyOn( - strategy, - "getDonationVotingMerkleDistributionDirectTransferStrategyTimings", - ).mockResolvedValue({ - applicationsStartTime: new Date(), - applicationsEndTime: new Date(), - donationsStartTime: new Date(), - donationsEndTime: new Date(), - }); + vi.spyOn(mockEvmProvider, "multicall").mockResolvedValue([ + 1609459200n, + 1609459200n, + 1609459200n, + 1609459200n, + ]); vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, @@ -320,22 +306,21 @@ describe("PoolCreatedHandler", () => { expect(mockMetadataProvider.getMetadata).toHaveBeenCalled(); }); - it("returns empty changeset if token price fetch fails", async () => { - const mockEvent = createMockEvent({ params: { amount: 1n } }); + it("throws an error if token price fetch fails", async () => { + const mockEvent = createMockEvent({ params: { amount: 1n, strategyId: "0xunknown" } }); vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue(undefined); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, }); - const result = await handler.handle(); - expect(result).toHaveLength(0); + await expect(() => handler.handle()).rejects.toThrow("Token price not found"); }); it("handles pending round roles", async () => { @@ -346,15 +331,12 @@ describe("PoolCreatedHandler", () => { priceUsd: 100, timestampMs: 1708369911, }); - vi.spyOn( - strategy, - "getDonationVotingMerkleDistributionDirectTransferStrategyTimings", - ).mockResolvedValue({ - applicationsStartTime: new Date(), - applicationsEndTime: new Date(), - donationsStartTime: new Date(), - donationsEndTime: new Date(), - }); + vi.spyOn(mockEvmProvider, "multicall").mockResolvedValue([ + 1609459200n, + 1609459200n, + 1609459200n, + 1609459200n, + ]); vi.spyOn(mockRoundRepository, "getPendingRoundRoles") .mockResolvedValueOnce([ @@ -384,7 +366,7 @@ describe("PoolCreatedHandler", () => { ]); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, diff --git a/packages/processors/test/helpers/utils.spec.ts b/packages/processors/test/helpers/utils.spec.ts index 1bbdb3e..eccc86b 100644 --- a/packages/processors/test/helpers/utils.spec.ts +++ b/packages/processors/test/helpers/utils.spec.ts @@ -4,33 +4,33 @@ import { getDateFromTimestamp } from "../../src/helpers/utils.js"; describe("utils", () => { describe("getDateFromTimestamp", () => { - it("should convert a valid timestamp to a Date object", () => { + it("converts a valid timestamp to a Date object", () => { const timestamp = 1609459200n; // 2021-01-01 00:00:00 UTC const result = getDateFromTimestamp(timestamp); expect(result).toBeInstanceOf(Date); expect(result?.toISOString()).toBe("2021-01-01T00:00:00.000Z"); }); - it("should handle the minimum valid timestamp (0)", () => { + it("handles the minimum valid timestamp (0)", () => { const timestamp = 0n; const result = getDateFromTimestamp(timestamp); expect(result).toBeInstanceOf(Date); expect(result?.toISOString()).toBe("1970-01-01T00:00:00.000Z"); }); - it("should handle the maximum valid timestamp", () => { + it("handles the maximum valid timestamp", () => { const maxTimestamp = 18446744073709551615n - 1n; const result = getDateFromTimestamp(maxTimestamp); expect(result).toBeInstanceOf(Date); }); - it("should return null for timestamps equal to or greater than UINT64_MAX", () => { + it("returns null for timestamps equal to or greater than UINT64_MAX", () => { const maxTimestamp = 18446744073709551615n; expect(getDateFromTimestamp(maxTimestamp)).toBeNull(); expect(getDateFromTimestamp(maxTimestamp + 1n)).toBeNull(); }); - it("should return null for negative timestamps", () => { + it("returns null for negative timestamps", () => { expect(getDateFromTimestamp(-1n)).toBeNull(); expect(getDateFromTimestamp(-1000000n)).toBeNull(); }); diff --git a/packages/shared/src/types/events/allo.ts b/packages/shared/src/types/events/allo.ts index 353d39d..32f5c44 100644 --- a/packages/shared/src/types/events/allo.ts +++ b/packages/shared/src/types/events/allo.ts @@ -22,8 +22,5 @@ export type PoolCreatedParams = { strategyId: Address; token: Address; amount: bigint; - metadata: { - pointer: string; - protocol: bigint; - }; + metadata: [pointer: string, protocol: bigint]; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51930fb..50f9985 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,6 +160,9 @@ importers: packages/processors: dependencies: + "@grants-stack-indexer/chain-providers": + specifier: workspace:* + version: link:../chain-providers "@grants-stack-indexer/metadata": specifier: workspace:* version: link:../metadata