Skip to content

Commit

Permalink
feat: direct grants lite event handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
0xnigir1 committed Nov 15, 2024
1 parent 7533c07 commit 2ebd4cf
Show file tree
Hide file tree
Showing 15 changed files with 1,623 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Changeset } from "@grants-stack-indexer/repository";
import { Address, ChainId, ProcessorEvent, StrategyEvent } from "@grants-stack-indexer/shared";

import DirectGrantsLiteStrategy from "../../../abis/allo-v2/v1/DirectGrantsLiteStrategy.js";
import { getDateFromTimestamp } from "../../../helpers/index.js";
import {
BaseRecipientStatusUpdatedHandler,
ProcessorDependencies,
StrategyTimings,
UnsupportedEventException,
} from "../../../internal.js";
import { BaseStrategyHandler } from "../common/base.strategy.js";
import {
DGLiteAllocatedHandler,
DGLiteRegisteredHandler,
DGLiteTimestampsUpdatedHandler,
DGLiteUpdatedRegistrationHandler,
} from "./handlers/index.js";

const STRATEGY_NAME = "allov2.DirectGrantsLiteStrategy";

/**
* This handler is responsible for processing events related to the
* Direct Grants Lite strategy.
*
* The following events are currently handled by this strategy:
* - Registered
* - UpdatedRegistrationWithStatus
* - TimestampsUpdated
* - AllocatedWithToken
* - RecipientStatusUpdatedWithFullRow
*/
export class DirectGrantsLiteStrategyHandler extends BaseStrategyHandler {
constructor(
private readonly chainId: ChainId,
private readonly dependencies: ProcessorDependencies,
) {
super(STRATEGY_NAME);
}

/** @inheritdoc */
async handle(event: ProcessorEvent<"Strategy", StrategyEvent>): Promise<Changeset[]> {
switch (event.eventName) {
case "RecipientStatusUpdatedWithFullRow":
return new BaseRecipientStatusUpdatedHandler(
event as ProcessorEvent<"Strategy", "RecipientStatusUpdatedWithFullRow">,
this.chainId,
this.dependencies,
).handle();
case "RegisteredWithSender":
return new DGLiteRegisteredHandler(
event as ProcessorEvent<"Strategy", "RegisteredWithSender">,
this.chainId,
this.dependencies,
).handle();
case "UpdatedRegistrationWithStatus":
return new DGLiteUpdatedRegistrationHandler(
event as ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus">,
this.chainId,
this.dependencies,
).handle();
case "TimestampsUpdated":
return new DGLiteTimestampsUpdatedHandler(
event as ProcessorEvent<"Strategy", "TimestampsUpdated">,
this.chainId,
this.dependencies,
).handle();
case "AllocatedWithToken":
return new DGLiteAllocatedHandler(
event as ProcessorEvent<"Strategy", "AllocatedWithToken">,
this.chainId,
this.dependencies,
).handle();
default:
throw new UnsupportedEventException("Strategy", event.eventName, this.name);
}
}

/** @inheritdoc */
override async fetchStrategyTimings(strategyId: Address): Promise<StrategyTimings> {
const { evmProvider } = this.dependencies;
let results: [bigint, bigint] = [0n, 0n];

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

// TODO: refactor when evmProvider implements this natively
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];
}

return {
applicationsStartTime: getDateFromTimestamp(results[0]),
applicationsEndTime: getDateFromTimestamp(results[1]),
donationsStartTime: null,
donationsEndTime: null,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { getAddress } from "viem";

import { Changeset } from "@grants-stack-indexer/repository";
import { ChainId, getTokenOrThrow, ProcessorEvent } from "@grants-stack-indexer/shared";

import { getTokenAmountInUsd, getUsdInTokenAmount } from "../../../../helpers/index.js";
import { IEventHandler, ProcessorDependencies } from "../../../../internal.js";

type Dependencies = Pick<
ProcessorDependencies,
"roundRepository" | "applicationRepository" | "pricingProvider"
>;

/**
* Handler for processing AllocatedWithToken events from the DirectGrantsLite strategy.
*
* When a round operator allocates funds to a recipient, this handler:
* 1. Retrieves the round and application based on the strategy address and recipient
* 2. Converts the allocated token amount to USD value
* 3. Calculates the equivalent amount in the round's match token
* 4. Updates the application with the allocation details
*/

export class DGLiteAllocatedHandler implements IEventHandler<"Strategy", "AllocatedWithToken"> {
constructor(
readonly event: ProcessorEvent<"Strategy", "AllocatedWithToken">,
private readonly chainId: ChainId,
private readonly dependencies: Dependencies,
) {}

/** @inheritdoc */
async handle(): Promise<Changeset[]> {
const { roundRepository, applicationRepository } = this.dependencies;
const { srcAddress } = this.event;
const { recipientId: _recipientId, amount: strAmount, token: _token } = this.event.params;

const amount = BigInt(strAmount);

const round = await roundRepository.getRoundByStrategyAddressOrThrow(
this.chainId,
getAddress(srcAddress),
);

const recipientId = getAddress(_recipientId);
const tokenAddress = getAddress(_token);
const application = await applicationRepository.getApplicationByAnchorAddressOrThrow(
this.chainId,
round.id,
recipientId,
);

const token = getTokenOrThrow(this.chainId, tokenAddress);
const matchToken = getTokenOrThrow(this.chainId, round.matchTokenAddress);

const { amountInUsd } = await getTokenAmountInUsd(
this.dependencies.pricingProvider,
token,
amount,
this.event.blockTimestamp,
);

let amountInRoundMatchToken: bigint | null = null;
amountInRoundMatchToken =
matchToken.address === token.address
? amount
: (
await getUsdInTokenAmount(
this.dependencies.pricingProvider,
matchToken,
amountInUsd,
this.event.blockTimestamp,
)
).amount;

const timestamp = this.event.blockTimestamp;

return [
{
type: "InsertApplicationPayout",
args: {
applicationPayout: {
amount,
applicationId: application.id,
roundId: round.id,
chainId: this.chainId,
tokenAddress,
amountInRoundMatchToken,
amountInUsd,
transactionHash: this.event.transactionFields.hash,
sender: getAddress(this.event.params.sender),
timestamp: new Date(timestamp),
},
},
},
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./registered.handler.js";
export * from "./updatedRegistration.handler.js";
export * from "./timestampsUpdated.handler.js";
export * from "./allocated.handler.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { getAddress } from "viem";

import { Changeset, NewApplication } from "@grants-stack-indexer/repository";
import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared";

import { IEventHandler, ProcessorDependencies } from "../../../../internal.js";
import { decodeDVMDExtendedApplicationData } from "../../helpers/index.js";

type Dependencies = Pick<
ProcessorDependencies,
"roundRepository" | "projectRepository" | "metadataProvider"
>;

/**
* Handles the Registered event for the Direct Grants Lite strategy.
*
* This handler performs the following core actions when a project registers for a round:
* - Validates that both the project and round exist
* - Decodes the application data from the event
* - Retrieves the application metadata
* - Creates a new application record with PENDING status
* - Links the application to both the project and round
*/

export class DGLiteRegisteredHandler implements IEventHandler<"Strategy", "RegisteredWithSender"> {
constructor(
readonly event: ProcessorEvent<"Strategy", "RegisteredWithSender">,
private readonly chainId: ChainId,
private readonly dependencies: Dependencies,
) {}

/** @inheritdoc */
async handle(): Promise<Changeset[]> {
const { projectRepository, roundRepository, metadataProvider } = this.dependencies;
const { data: encodedData, recipientId, sender } = this.event.params;
const { blockNumber, blockTimestamp } = this.event;

const anchorAddress = getAddress(recipientId);
const project = await projectRepository.getProjectByAnchorOrThrow(
this.chainId,
anchorAddress,
);

const strategyAddress = getAddress(this.event.srcAddress);
const round = await roundRepository.getRoundByStrategyAddressOrThrow(
this.chainId,
strategyAddress,
);

const values = decodeDVMDExtendedApplicationData(encodedData);
// ID is defined as recipientsCounter - 1, which is a value emitted by the strategy
const id = (Number(values.recipientsCounter) - 1).toString();

const metadata = await metadataProvider.getMetadata(values.metadata.pointer);

const application: NewApplication = {
chainId: this.chainId,
id: id,
projectId: project.id,
anchorAddress,
roundId: round.id,
status: "PENDING",
metadataCid: values.metadata.pointer,
metadata: metadata ?? null,
createdAtBlock: BigInt(blockNumber),
createdByAddress: getAddress(sender),
statusUpdatedAtBlock: BigInt(blockNumber),
statusSnapshots: [
{
status: "PENDING",
updatedAtBlock: blockNumber.toString(),
updatedAt: new Date(blockTimestamp * 1000), // timestamp is in seconds, convert to ms
},
],
distributionTransaction: null,
totalAmountDonatedInUsd: 0,
totalDonationsCount: 0,
uniqueDonorsCount: 0,
tags: ["allo-v2"],
};

return [
{
type: "InsertApplication",
args: application,
},
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { getAddress } from "viem";

import { Changeset } from "@grants-stack-indexer/repository";
import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared";

import { getDateFromTimestamp } from "../../../../helpers/index.js";
import { IEventHandler, ProcessorDependencies } from "../../../../internal.js";

type Dependencies = Pick<ProcessorDependencies, "roundRepository">;

/**
* Handles the TimestampsUpdated event for the Direct Grants Lite strategy.
*
* This handler processes updates to the round timestamps:
* - Validates the round exists for the strategy address
* - Converts the updated registration timestamps to dates
* - Returns a changeset to update the round's application timestamps
*/
export class DGLiteTimestampsUpdatedHandler
implements IEventHandler<"Strategy", "TimestampsUpdated">
{
constructor(
readonly event: ProcessorEvent<"Strategy", "TimestampsUpdated">,
private readonly chainId: ChainId,
private readonly dependencies: Dependencies,
) {}

/**
* Handles the TimestampsUpdated event for the Direct Grants Lite strategy.
* @returns The changeset with an UpdateRound operation.
* @throws RoundNotFound if the round is not found.
*/
async handle(): Promise<Changeset[]> {
const strategyAddress = getAddress(this.event.srcAddress);
const round = await this.dependencies.roundRepository.getRoundByStrategyAddressOrThrow(
this.chainId,
strategyAddress,
);

const { startTime: strStartTime, endTime: strEndTime } = this.event.params;

const applicationsStartTime = getDateFromTimestamp(BigInt(strStartTime));
const applicationsEndTime = getDateFromTimestamp(BigInt(strEndTime));

return [
{
type: "UpdateRound",
args: {
chainId: this.chainId,
roundId: round.id,
round: {
applicationsStartTime,
applicationsEndTime,
},
},
},
];
}
}
Loading

0 comments on commit 2ebd4cf

Please sign in to comment.