Skip to content

Commit 338721b

Browse files
committed
Merge branch 'dev' into feat/direct-grants-simple-strategy
2 parents 6cf441a + 0623cda commit 338721b

File tree

47 files changed

+2095
-415
lines changed

Some content is hidden

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

47 files changed

+2095
-415
lines changed

apps/processing/src/services/sharedDependencies.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { IpfsProvider } from "@grants-stack-indexer/metadata";
88
import { PricingProviderFactory } from "@grants-stack-indexer/pricing";
99
import {
1010
createKyselyDatabase,
11+
KyselyApplicationPayoutRepository,
1112
KyselyApplicationRepository,
1213
KyselyDonationRepository,
1314
KyselyProjectRepository,
@@ -50,6 +51,10 @@ export class SharedDependenciesService {
5051
kyselyDatabase,
5152
env.DATABASE_SCHEMA,
5253
);
54+
const applicationPayoutRepository = new KyselyApplicationPayoutRepository(
55+
kyselyDatabase,
56+
env.DATABASE_SCHEMA,
57+
);
5358
const pricingProvider = PricingProviderFactory.create(env, { logger });
5459

5560
const metadataProvider = new IpfsProvider(env.IPFS_GATEWAYS_URL, logger);
@@ -72,6 +77,7 @@ export class SharedDependenciesService {
7277
pricingProvider,
7378
donationRepository,
7479
metadataProvider,
80+
applicationPayoutRepository,
7581
},
7682
registries: {
7783
eventsRegistry,

packages/data-flow/src/data-loader/dataLoader.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
Changeset,
3+
IApplicationPayoutRepository,
34
IApplicationRepository,
45
IDonationRepository,
56
IProjectRepository,
@@ -10,6 +11,7 @@ import { ILogger, stringify } from "@grants-stack-indexer/shared";
1011
import { ExecutionResult, IDataLoader, InvalidChangeset } from "../internal.js";
1112
import {
1213
createApplicationHandlers,
14+
createApplicationPayoutHandlers,
1315
createDonationHandlers,
1416
createProjectHandlers,
1517
createRoundHandlers,
@@ -38,6 +40,7 @@ export class DataLoader implements IDataLoader {
3840
round: IRoundRepository;
3941
application: IApplicationRepository;
4042
donation: IDonationRepository;
43+
applicationPayout: IApplicationPayoutRepository;
4144
},
4245
private readonly logger: ILogger,
4346
) {
@@ -46,6 +49,7 @@ export class DataLoader implements IDataLoader {
4649
...createRoundHandlers(repositories.round),
4750
...createApplicationHandlers(repositories.application),
4851
...createDonationHandlers(repositories.donation),
52+
...createApplicationPayoutHandlers(repositories.applicationPayout),
4953
};
5054
}
5155

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {
2+
ApplicationPayoutChangeset,
3+
IApplicationPayoutRepository,
4+
} from "@grants-stack-indexer/repository";
5+
6+
import { ChangesetHandler } from "../types/index.js";
7+
8+
/**
9+
* Collection of handlers for application-related operations.
10+
* Each handler corresponds to a specific Application changeset type.
11+
*/
12+
export type ApplicationPayoutHandlers = {
13+
[K in ApplicationPayoutChangeset["type"]]: ChangesetHandler<K>;
14+
};
15+
16+
/**
17+
* Creates handlers for managing application-related operations.
18+
*
19+
* @param repository - The application repository instance used for database operations
20+
* @returns An object containing all application-related handlers
21+
*/
22+
export const createApplicationPayoutHandlers = (
23+
repository: IApplicationPayoutRepository,
24+
): ApplicationPayoutHandlers => ({
25+
InsertApplicationPayout: (async (changeset): Promise<void> => {
26+
await repository.insertApplicationPayout(changeset.args.applicationPayout);
27+
}) satisfies ChangesetHandler<"InsertApplicationPayout">,
28+
});

packages/data-flow/src/data-loader/handlers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from "./application.handlers.js";
22
export * from "./project.handlers.js";
33
export * from "./round.handlers.js";
44
export * from "./donation.handlers.js";
5+
export * from "./applicationPayout.handlers.js";

packages/data-flow/src/orchestrator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export class Orchestrator {
9191
round: this.dependencies.roundRepository,
9292
application: this.dependencies.applicationRepository,
9393
donation: this.dependencies.donationRepository,
94+
applicationPayout: this.dependencies.applicationPayoutRepository,
9495
},
9596
this.logger,
9697
);

packages/data-flow/src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ProcessorDependencies } from "@grants-stack-indexer/processors";
22
import {
33
Changeset,
4+
IApplicationPayoutRepository,
45
IApplicationRepository,
56
IDonationRepository,
67
IProjectRepository,
@@ -33,4 +34,5 @@ export type CoreDependencies = Pick<
3334
projectRepository: IProjectRepository;
3435
applicationRepository: IApplicationRepository;
3536
donationRepository: IDonationRepository;
37+
applicationPayoutRepository: IApplicationPayoutRepository;
3638
};

packages/data-flow/test/data-loader/dataLoader.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
22

33
import {
44
Changeset,
5+
IApplicationPayoutRepository,
56
IApplicationRepository,
67
IDonationRepository,
78
IProjectRepository,
@@ -33,6 +34,10 @@ describe("DataLoader", () => {
3334
insertManyDonations: vi.fn(),
3435
} as IDonationRepository;
3536

37+
const mockApplicationPayoutRepository = {
38+
insertApplicationPayout: vi.fn(),
39+
} as IApplicationPayoutRepository;
40+
3641
const logger: ILogger = {
3742
debug: vi.fn(),
3843
error: vi.fn(),
@@ -46,6 +51,7 @@ describe("DataLoader", () => {
4651
round: mockRoundRepository,
4752
application: mockApplicationRepository,
4853
donation: mockDonationRepository,
54+
applicationPayout: mockApplicationPayoutRepository,
4955
},
5056
logger,
5157
);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { IIndexerClient } from "@grants-stack-indexer/indexer-client";
66
import { UnsupportedStrategy } from "@grants-stack-indexer/processors";
77
import {
88
Changeset,
9+
IApplicationPayoutRepository,
910
IApplicationRepository,
1011
IDonationRepository,
1112
IProjectRepository,
@@ -94,6 +95,7 @@ describe("Orchestrator", { sequential: true }, () => {
9495
roundRepository: {} as unknown as IRoundRepository,
9596
applicationRepository: {} as unknown as IApplicationRepository,
9697
donationRepository: {} as unknown as IDonationRepository,
98+
applicationPayoutRepository: {} as unknown as IApplicationPayoutRepository,
9799
pricingProvider: {
98100
getTokenPrice: vi.fn(),
99101
},
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Changeset } from "@grants-stack-indexer/repository";
2+
import { Address, ChainId, ProcessorEvent, StrategyEvent } from "@grants-stack-indexer/shared";
3+
4+
import DirectGrantsLiteStrategy from "../../../abis/allo-v2/v1/DirectGrantsLiteStrategy.js";
5+
import { getDateFromTimestamp } from "../../../helpers/index.js";
6+
import {
7+
BaseRecipientStatusUpdatedHandler,
8+
ProcessorDependencies,
9+
StrategyTimings,
10+
UnsupportedEventException,
11+
} from "../../../internal.js";
12+
import { BaseStrategyHandler } from "../index.js";
13+
import {
14+
DGLiteAllocatedHandler,
15+
DGLiteRegisteredHandler,
16+
DGLiteTimestampsUpdatedHandler,
17+
DGLiteUpdatedRegistrationHandler,
18+
} from "./handlers/index.js";
19+
20+
const STRATEGY_NAME = "allov2.DirectGrantsLiteStrategy";
21+
22+
/**
23+
* This handler is responsible for processing events related to the
24+
* Direct Grants Lite strategy.
25+
*
26+
* The following events are currently handled by this strategy:
27+
* - Registered
28+
* - UpdatedRegistrationWithStatus
29+
* - TimestampsUpdated
30+
* - AllocatedWithToken
31+
* - RecipientStatusUpdatedWithFullRow
32+
*/
33+
export class DirectGrantsLiteStrategyHandler extends BaseStrategyHandler {
34+
constructor(
35+
private readonly chainId: ChainId,
36+
private readonly dependencies: ProcessorDependencies,
37+
) {
38+
super(STRATEGY_NAME);
39+
}
40+
41+
/** @inheritdoc */
42+
async handle(event: ProcessorEvent<"Strategy", StrategyEvent>): Promise<Changeset[]> {
43+
switch (event.eventName) {
44+
case "RecipientStatusUpdatedWithFullRow":
45+
return new BaseRecipientStatusUpdatedHandler(
46+
event as ProcessorEvent<"Strategy", "RecipientStatusUpdatedWithFullRow">,
47+
this.chainId,
48+
this.dependencies,
49+
).handle();
50+
case "RegisteredWithSender":
51+
return new DGLiteRegisteredHandler(
52+
event as ProcessorEvent<"Strategy", "RegisteredWithSender">,
53+
this.chainId,
54+
this.dependencies,
55+
).handle();
56+
case "UpdatedRegistrationWithStatus":
57+
return new DGLiteUpdatedRegistrationHandler(
58+
event as ProcessorEvent<"Strategy", "UpdatedRegistrationWithStatus">,
59+
this.chainId,
60+
this.dependencies,
61+
).handle();
62+
case "TimestampsUpdated":
63+
return new DGLiteTimestampsUpdatedHandler(
64+
event as ProcessorEvent<"Strategy", "TimestampsUpdated">,
65+
this.chainId,
66+
this.dependencies,
67+
).handle();
68+
case "AllocatedWithToken":
69+
return new DGLiteAllocatedHandler(
70+
event as ProcessorEvent<"Strategy", "AllocatedWithToken">,
71+
this.chainId,
72+
this.dependencies,
73+
).handle();
74+
default:
75+
throw new UnsupportedEventException("Strategy", event.eventName, this.name);
76+
}
77+
}
78+
79+
/** @inheritdoc */
80+
override async fetchStrategyTimings(strategyId: Address): Promise<StrategyTimings> {
81+
const { evmProvider } = this.dependencies;
82+
let results: [bigint, bigint] = [0n, 0n];
83+
84+
const contractCalls = [
85+
{
86+
abi: DirectGrantsLiteStrategy,
87+
functionName: "registrationStartTime",
88+
address: strategyId,
89+
},
90+
{
91+
abi: DirectGrantsLiteStrategy,
92+
functionName: "registrationEndTime",
93+
address: strategyId,
94+
},
95+
] as const;
96+
97+
// TODO: refactor when evmProvider implements this natively
98+
if (evmProvider.getMulticall3Address()) {
99+
results = await evmProvider.multicall({
100+
contracts: contractCalls,
101+
allowFailure: false,
102+
});
103+
} else {
104+
results = (await Promise.all(
105+
contractCalls.map((call) =>
106+
evmProvider.readContract(call.address, call.abi, call.functionName),
107+
),
108+
)) as [bigint, bigint];
109+
}
110+
111+
return {
112+
applicationsStartTime: getDateFromTimestamp(results[0]),
113+
applicationsEndTime: getDateFromTimestamp(results[1]),
114+
donationsStartTime: null,
115+
donationsEndTime: null,
116+
};
117+
}
118+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { getAddress } from "viem";
2+
3+
import { Changeset } from "@grants-stack-indexer/repository";
4+
import { ChainId, getTokenOrThrow, ProcessorEvent } from "@grants-stack-indexer/shared";
5+
6+
import { getTokenAmountInUsd, getUsdInTokenAmount } from "../../../../helpers/index.js";
7+
import { IEventHandler, ProcessorDependencies } from "../../../../internal.js";
8+
9+
type Dependencies = Pick<
10+
ProcessorDependencies,
11+
"roundRepository" | "applicationRepository" | "pricingProvider"
12+
>;
13+
14+
/**
15+
* Handler for processing AllocatedWithToken events from the DirectGrantsLite strategy.
16+
*
17+
* When a round operator allocates funds to a recipient, this handler:
18+
* 1. Retrieves the round and application based on the strategy address and recipient
19+
* 2. Converts the allocated token amount to USD value
20+
* 3. Calculates the equivalent amount in the round's match token
21+
* 4. Updates the application with the allocation details
22+
*/
23+
24+
export class DGLiteAllocatedHandler implements IEventHandler<"Strategy", "AllocatedWithToken"> {
25+
constructor(
26+
readonly event: ProcessorEvent<"Strategy", "AllocatedWithToken">,
27+
private readonly chainId: ChainId,
28+
private readonly dependencies: Dependencies,
29+
) {}
30+
31+
/**
32+
* Handles the AllocatedWithToken event for the Direct Grants Lite strategy.
33+
* @returns The changeset with an InsertApplicationPayout operation.
34+
* @throws RoundNotFound if the round is not found.
35+
* @throws ApplicationNotFound if the application is not found.
36+
* @throws TokenNotFound if the token is not found.
37+
* @throws TokenPriceNotFound if the token price is not found.
38+
*/
39+
async handle(): Promise<Changeset[]> {
40+
const { roundRepository, applicationRepository } = this.dependencies;
41+
const { srcAddress } = this.event;
42+
const { recipientId: _recipientId, amount: strAmount, token: _token } = this.event.params;
43+
44+
const amount = BigInt(strAmount);
45+
46+
const round = await roundRepository.getRoundByStrategyAddressOrThrow(
47+
this.chainId,
48+
getAddress(srcAddress),
49+
);
50+
51+
const recipientId = getAddress(_recipientId);
52+
const tokenAddress = getAddress(_token);
53+
const application = await applicationRepository.getApplicationByAnchorAddressOrThrow(
54+
this.chainId,
55+
round.id,
56+
recipientId,
57+
);
58+
59+
const token = getTokenOrThrow(this.chainId, tokenAddress);
60+
const matchToken = getTokenOrThrow(this.chainId, round.matchTokenAddress);
61+
62+
let amountInUsd = "0";
63+
let amountInRoundMatchToken = 0n;
64+
65+
if (amount > 0) {
66+
const { amountInUsd: amountInUsdString } = await getTokenAmountInUsd(
67+
this.dependencies.pricingProvider,
68+
token,
69+
amount,
70+
this.event.blockTimestamp,
71+
);
72+
amountInUsd = amountInUsdString;
73+
74+
amountInRoundMatchToken =
75+
matchToken.address === token.address
76+
? amount
77+
: (
78+
await getUsdInTokenAmount(
79+
this.dependencies.pricingProvider,
80+
matchToken,
81+
amountInUsd,
82+
this.event.blockTimestamp,
83+
)
84+
).amount;
85+
}
86+
87+
const timestamp = this.event.blockTimestamp;
88+
89+
return [
90+
{
91+
type: "InsertApplicationPayout",
92+
args: {
93+
applicationPayout: {
94+
amount,
95+
applicationId: application.id,
96+
roundId: round.id,
97+
chainId: this.chainId,
98+
tokenAddress,
99+
amountInRoundMatchToken,
100+
amountInUsd,
101+
transactionHash: this.event.transactionFields.hash,
102+
sender: getAddress(this.event.params.sender),
103+
timestamp: new Date(timestamp),
104+
},
105+
},
106+
},
107+
];
108+
}
109+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from "./registered.handler.js";
2+
export * from "./updatedRegistration.handler.js";
3+
export * from "./timestampsUpdated.handler.js";
4+
export * from "./allocated.handler.js";

0 commit comments

Comments
 (0)