Skip to content

Commit

Permalink
feat: direct grants lite strategy (#34)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes GIT-162 GIT-164 GIT-165 GIT-158 GIT-168 GIT-143 GIT-141

## Description
- add `ApplicationPayout` model, repository and Changesets
- add `DirectGrantsLiteStrategy` handler and events handlers:
  - `Registered`
  - `UpdatedRegistration`
  - `TimestampsUpdated`
  - `Allocated`
  - `RecipientStatusUpdated`

## 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.
  • Loading branch information
0xnigir1 authored Nov 19, 2024
1 parent 3dcffdb commit 0623cda
Show file tree
Hide file tree
Showing 44 changed files with 2,078 additions and 352 deletions.
6 changes: 6 additions & 0 deletions apps/processing/src/services/sharedDependencies.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { IpfsProvider } from "@grants-stack-indexer/metadata";
import { PricingProviderFactory } from "@grants-stack-indexer/pricing";
import {
createKyselyDatabase,
KyselyApplicationPayoutRepository,
KyselyApplicationRepository,
KyselyDonationRepository,
KyselyProjectRepository,
Expand Down Expand Up @@ -50,6 +51,10 @@ export class SharedDependenciesService {
kyselyDatabase,
env.DATABASE_SCHEMA,
);
const applicationPayoutRepository = new KyselyApplicationPayoutRepository(
kyselyDatabase,
env.DATABASE_SCHEMA,
);
const pricingProvider = PricingProviderFactory.create(env, { logger });

const metadataProvider = new IpfsProvider(env.IPFS_GATEWAYS_URL, logger);
Expand All @@ -72,6 +77,7 @@ export class SharedDependenciesService {
pricingProvider,
donationRepository,
metadataProvider,
applicationPayoutRepository,
},
registries: {
eventsRegistry,
Expand Down
4 changes: 4 additions & 0 deletions packages/data-flow/src/data-loader/dataLoader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Changeset,
IApplicationPayoutRepository,
IApplicationRepository,
IDonationRepository,
IProjectRepository,
Expand All @@ -10,6 +11,7 @@ import { ILogger, stringify } from "@grants-stack-indexer/shared";
import { ExecutionResult, IDataLoader, InvalidChangeset } from "../internal.js";
import {
createApplicationHandlers,
createApplicationPayoutHandlers,
createDonationHandlers,
createProjectHandlers,
createRoundHandlers,
Expand Down Expand Up @@ -38,6 +40,7 @@ export class DataLoader implements IDataLoader {
round: IRoundRepository;
application: IApplicationRepository;
donation: IDonationRepository;
applicationPayout: IApplicationPayoutRepository;
},
private readonly logger: ILogger,
) {
Expand All @@ -46,6 +49,7 @@ export class DataLoader implements IDataLoader {
...createRoundHandlers(repositories.round),
...createApplicationHandlers(repositories.application),
...createDonationHandlers(repositories.donation),
...createApplicationPayoutHandlers(repositories.applicationPayout),
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
ApplicationPayoutChangeset,
IApplicationPayoutRepository,
} from "@grants-stack-indexer/repository";

import { ChangesetHandler } from "../types/index.js";

/**
* Collection of handlers for application-related operations.
* Each handler corresponds to a specific Application changeset type.
*/
export type ApplicationPayoutHandlers = {
[K in ApplicationPayoutChangeset["type"]]: ChangesetHandler<K>;
};

/**
* Creates handlers for managing application-related operations.
*
* @param repository - The application repository instance used for database operations
* @returns An object containing all application-related handlers
*/
export const createApplicationPayoutHandlers = (
repository: IApplicationPayoutRepository,
): ApplicationPayoutHandlers => ({
InsertApplicationPayout: (async (changeset): Promise<void> => {
await repository.insertApplicationPayout(changeset.args.applicationPayout);
}) satisfies ChangesetHandler<"InsertApplicationPayout">,
});
1 change: 1 addition & 0 deletions packages/data-flow/src/data-loader/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./application.handlers.js";
export * from "./project.handlers.js";
export * from "./round.handlers.js";
export * from "./donation.handlers.js";
export * from "./applicationPayout.handlers.js";
1 change: 1 addition & 0 deletions packages/data-flow/src/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export class Orchestrator {
round: this.dependencies.roundRepository,
application: this.dependencies.applicationRepository,
donation: this.dependencies.donationRepository,
applicationPayout: this.dependencies.applicationPayoutRepository,
},
this.logger,
);
Expand Down
2 changes: 2 additions & 0 deletions packages/data-flow/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ProcessorDependencies } from "@grants-stack-indexer/processors";
import {
Changeset,
IApplicationPayoutRepository,
IApplicationRepository,
IDonationRepository,
IProjectRepository,
Expand Down Expand Up @@ -33,4 +34,5 @@ export type CoreDependencies = Pick<
projectRepository: IProjectRepository;
applicationRepository: IApplicationRepository;
donationRepository: IDonationRepository;
applicationPayoutRepository: IApplicationPayoutRepository;
};
6 changes: 6 additions & 0 deletions packages/data-flow/test/data-loader/dataLoader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";

import {
Changeset,
IApplicationPayoutRepository,
IApplicationRepository,
IDonationRepository,
IProjectRepository,
Expand Down Expand Up @@ -33,6 +34,10 @@ describe("DataLoader", () => {
insertManyDonations: vi.fn(),
} as IDonationRepository;

const mockApplicationPayoutRepository = {
insertApplicationPayout: vi.fn(),
} as IApplicationPayoutRepository;

const logger: ILogger = {
debug: vi.fn(),
error: vi.fn(),
Expand All @@ -46,6 +51,7 @@ describe("DataLoader", () => {
round: mockRoundRepository,
application: mockApplicationRepository,
donation: mockDonationRepository,
applicationPayout: mockApplicationPayoutRepository,
},
logger,
);
Expand Down
2 changes: 2 additions & 0 deletions packages/data-flow/test/unit/orchestrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { IIndexerClient } from "@grants-stack-indexer/indexer-client";
import { UnsupportedStrategy } from "@grants-stack-indexer/processors";
import {
Changeset,
IApplicationPayoutRepository,
IApplicationRepository,
IDonationRepository,
IProjectRepository,
Expand Down Expand Up @@ -94,6 +95,7 @@ describe("Orchestrator", { sequential: true }, () => {
roundRepository: {} as unknown as IRoundRepository,
applicationRepository: {} as unknown as IApplicationRepository,
donationRepository: {} as unknown as IDonationRepository,
applicationPayoutRepository: {} as unknown as IApplicationPayoutRepository,
pricingProvider: {
getTokenPrice: vi.fn(),
},
Expand Down
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 "../index.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,109 @@
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,
) {}

/**
* Handles the AllocatedWithToken event for the Direct Grants Lite strategy.
* @returns The changeset with an InsertApplicationPayout operation.
* @throws RoundNotFound if the round is not found.
* @throws ApplicationNotFound if the application is not found.
* @throws TokenNotFound if the token is not found.
* @throws TokenPriceNotFound if the token price is not found.
*/
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);

let amountInUsd = "0";
let amountInRoundMatchToken = 0n;

if (amount > 0) {
const { amountInUsd: amountInUsdString } = await getTokenAmountInUsd(
this.dependencies.pricingProvider,
token,
amount,
this.event.blockTimestamp,
);
amountInUsd = amountInUsdString;

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";
Loading

0 comments on commit 0623cda

Please sign in to comment.