Skip to content

Commit

Permalink
Merge branch 'dev' into feat/direct-grants-lite-strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
0xnigir1 committed Nov 14, 2024
2 parents 8eee37b + 3dcffdb commit 7533c07
Show file tree
Hide file tree
Showing 20 changed files with 486 additions and 15 deletions.
2 changes: 1 addition & 1 deletion apps/indexer/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ contracts:
- event: AllocatedWithNft(address indexed recipientId, uint256 votes, address nft, address allocator)

# DirectAllocated
- event: DirectAllocated(address indexed recipient, uint256 amount, address token, address sender)
- event: DirectAllocated(bytes32 indexed profileId, address profileOwner, uint256 amount, address token, address sender)

# RecipientStatusUpdated
- event: RecipientStatusUpdated(address indexed recipientId, uint256 applicationId, uint8 status, address sender)
Expand Down
2 changes: 1 addition & 1 deletion apps/processing/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ INDEXER_ADMIN_SECRET=testing

IPFS_GATEWAYS_URL=["https://ipfs.io","https://gateway.pinata.cloud","https://dweb.link", "https://ipfs.eth.aragon.network"]

PRICING_SOURCE= #coingecko | dummy
PRICING_SOURCE= # 'coingecko' or 'dummy'

COINGECKO_API_KEY={{YOUR_KEY}}
COINGECKO_API_TYPE=demo
1 change: 1 addition & 0 deletions packages/data-flow/test/unit/orchestrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ describe("Orchestrator", { sequential: true }, () => {
UpdatedRegistrationWithStatus: "",
UpdatedRegistration: "",
UpdatedRegistrationWithApplicationId: "",
DirectAllocated: "",
};

for (const event of Object.keys(strategyEvents) as StrategyEvent[]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ export class UnsupportedEventException extends Error {
constructor(
contract: ContractName,
public readonly eventName: string,
strategyName?: string,
) {
super(`Event ${eventName} unsupported for ${contract} processor`);
super(
`Event ${eventName} unsupported for ${contract} processor${strategyName ? `, strategy ${strategyName}` : ""}`,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Changeset } from "@grants-stack-indexer/repository";
import { ChainId, ProcessorEvent, StrategyEvent } from "@grants-stack-indexer/shared";

import { ProcessorDependencies, UnsupportedEventException } from "../../../internal.js";
import { BaseStrategyHandler } from "../index.js";
import { DirectAllocatedHandler } from "./handlers/index.js";

const STRATEGY_NAME = "allov2.DirectAllocationStrategy";

/**
* This handler is responsible for processing events related to the
* Direct Allocation strategy.
*
* The following events are currently handled by this strategy:
* - DirectAllocated
*/
export class DirectAllocationStrategyHandler 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 "DirectAllocated":
return new DirectAllocatedHandler(
event as ProcessorEvent<"Strategy", "DirectAllocated">,
this.chainId,
this.dependencies,
).handle();
default:
throw new UnsupportedEventException("Strategy", event.eventName, this.name);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { getAddress, zeroAddress } from "viem";

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

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

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

/**
* Handles the DirectAllocated event for the Direct Allocation strategy.
*
* This handler processes direct allocations of funds to a project by:
* - Validating that both the round and project exist
* - Retrieving token price data to calculate USD amounts
* - Creating a new donation record with the allocated amount
*
* Unlike other allocation handlers, this one does not require an application
* since funds are allocated directly to projects.
*/
export class DirectAllocatedHandler implements IEventHandler<"Strategy", "DirectAllocated"> {
constructor(
readonly event: ProcessorEvent<"Strategy", "DirectAllocated">,
private readonly chainId: ChainId,
private readonly dependencies: Dependencies,
) {}

/**
* Handles the DirectAllocated event for the Direct Allocation strategy.
* @returns {Changeset[]} The changeset containing an InsertDonation change
* @throws {ProjectNotFound} if the project does not exist
* @throws {RoundNotFound} if the round does not exist
* @throws {UnknownToken} if the token does not exist
* @throws {TokenPriceNotFoundError} if the token price is not found
*/
async handle(): Promise<Changeset[]> {
const { projectRepository, roundRepository, pricingProvider } = this.dependencies;
const strategyAddress = getAddress(this.event.srcAddress);

const round = await roundRepository.getRoundByStrategyAddressOrThrow(
this.chainId,
strategyAddress,
);
const project = await projectRepository.getProjectByIdOrThrow(
this.chainId,
this.event.params.profileId,
);

const donationId = getDonationId(this.event.blockNumber, this.event.logIndex);

const amount = BigInt(this.event.params.amount);
const token = getTokenOrThrow(this.chainId, this.event.params.token);
const sender = getAddress(this.event.params.sender);

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

const donation: Donation = {
id: donationId,
chainId: this.chainId,
roundId: round.id,
applicationId: zeroAddress,
donorAddress: sender,
recipientAddress: getAddress(this.event.params.profileOwner),
projectId: project.id,
transactionHash: this.event.transactionFields.hash,
blockNumber: BigInt(this.event.blockNumber),
tokenAddress: token.address,
amount: amount,
amountInUsd,
amountInRoundMatchToken: 0n,
timestamp: new Date(priceTimestamp),
};

return [
{
type: "InsertDonation",
args: { donation },
},
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./directAllocated.handler.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./handlers/index.js";
export * from "./directAllocation.handler.js";
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export class DVMDDirectTransferStrategyHandler extends BaseStrategyHandler {
this.dependencies,
).handle();
default:
throw new UnsupportedEventException("Strategy", event.eventName);
throw new UnsupportedEventException("Strategy", event.eventName, this.name);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { encodePacked, getAddress, keccak256 } from "viem";
import { getAddress } from "viem";

import { Changeset, Donation } from "@grants-stack-indexer/repository";
import { ChainId, getTokenOrThrow, ProcessorEvent } from "@grants-stack-indexer/shared";
Expand All @@ -10,6 +10,7 @@ import {
ProcessorDependencies,
} from "../../../../internal.js";
import { ApplicationMetadata, ApplicationMetadataSchema } from "../../../../schemas/index.js";
import { getDonationId } from "../../helpers/index.js";

type Dependencies = Pick<
ProcessorDependencies,
Expand Down Expand Up @@ -59,7 +60,7 @@ export class DVMDAllocatedHandler implements IEventHandler<"Strategy", "Allocate
getAddress(_recipientId),
);

const donationId = this.getDonationId(this.event.blockNumber, this.event.logIndex);
const donationId = getDonationId(this.event.blockNumber, this.event.logIndex);

const token = getTokenOrThrow(this.chainId, _token);
const matchToken = getTokenOrThrow(this.chainId, round.matchTokenAddress);
Expand Down Expand Up @@ -110,13 +111,6 @@ export class DVMDAllocatedHandler implements IEventHandler<"Strategy", "Allocate
];
}

/**
* DONATION_ID = keccak256(abi.encodePacked(blockNumber, "-", logIndex));
*/
private getDonationId(blockNumber: number, logIndex: number): string {
return keccak256(encodePacked(["string"], [`${blockNumber}-${logIndex}`]));
}

/**
* Parses the application metadata.
* @param {unknown} metadata - The metadata to parse.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { encodePacked, keccak256 } from "viem/utils";

/**
* DONATION_ID = keccak256(abi.encodePacked(blockNumber, "-", logIndex));
*/
export const getDonationId = (blockNumber: number, logIndex: number): string => {
return keccak256(encodePacked(["string"], [`${blockNumber}-${logIndex}`]));
};
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./decoder.js";
export * from "./applicationStatus.js";
export * from "./allocated.js";
1 change: 1 addition & 0 deletions packages/processors/src/processors/strategy/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./common/index.js";
export * from "./donationVotingMerkleDistributionDirectTransfer/handlers/index.js";
export * from "./directAllocation/index.js";
export * from "./strategyHandler.factory.js";
export * from "./strategy.processor.js";
// Export mapping separately to avoid circular dependencies
Expand Down
3 changes: 3 additions & 0 deletions packages/processors/src/processors/strategy/mapping.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Hex } from "viem";

import type { StrategyHandlerConstructor } from "../../internal.js";
import { DirectAllocationStrategyHandler } from "./directAllocation/index.js";
import { DVMDDirectTransferStrategyHandler } from "./donationVotingMerkleDistributionDirectTransfer/dvmdDirectTransfer.handler.js";

/**
Expand All @@ -18,6 +19,8 @@ const strategyIdToHandler: Readonly<Record<string, StrategyHandlerConstructor>>
DVMDDirectTransferStrategyHandler, // DonationVotingMerkleDistributionDirectTransferStrategyv2.0
"0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0":
DVMDDirectTransferStrategyHandler, // DonationVotingMerkleDistributionDirectTransferStrategyv2.1
"0x4cd0051913234cdd7d165b208851240d334786d6e5afbb4d0eec203515a9c6f3":
DirectAllocationStrategyHandler,
} as const;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { EvmProvider } from "@grants-stack-indexer/chain-providers";
import { IMetadataProvider } from "@grants-stack-indexer/metadata";
import { IPricingProvider } from "@grants-stack-indexer/pricing";
import {
IApplicationReadRepository,
IProjectReadRepository,
IRoundReadRepository,
} from "@grants-stack-indexer/repository";
import { ChainId, ILogger, ProcessorEvent, StrategyEvent } from "@grants-stack-indexer/shared";

import { UnsupportedEventException } from "../../../src/internal.js";
import { DirectAllocationStrategyHandler } from "../../../src/processors/strategy/directAllocation/directAllocation.handler.js";
import { DirectAllocatedHandler } from "../../../src/processors/strategy/directAllocation/handlers/directAllocated.handler.js";

vi.mock(
"../../../src/processors/strategy/directAllocation/handlers/directAllocated.handler.js",
() => {
const DirectAllocatedHandler = vi.fn();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
DirectAllocatedHandler.prototype.handle = vi.fn();
return { DirectAllocatedHandler };
},
);

describe("DirectAllocationStrategyHandler", () => {
let handler: DirectAllocationStrategyHandler;
let mockMetadataProvider: IMetadataProvider;
let mockRoundRepository: IRoundReadRepository;
let mockProjectRepository: IProjectReadRepository;
let mockEVMProvider: EvmProvider;
let mockPricingProvider: IPricingProvider;
let mockApplicationRepository: IApplicationReadRepository;
let mockLogger: ILogger;
const chainId = 10 as ChainId;

beforeEach(() => {
mockMetadataProvider = {} as IMetadataProvider;
mockRoundRepository = {} as IRoundReadRepository;
mockProjectRepository = {} as IProjectReadRepository;
mockEVMProvider = {} as unknown as EvmProvider;
mockPricingProvider = {} as IPricingProvider;
mockApplicationRepository = {} as IApplicationReadRepository;
mockLogger = {} as ILogger;

handler = new DirectAllocationStrategyHandler(chainId, {
metadataProvider: mockMetadataProvider,
roundRepository: mockRoundRepository,
projectRepository: mockProjectRepository,
evmProvider: mockEVMProvider,
pricingProvider: mockPricingProvider,
applicationRepository: mockApplicationRepository,
logger: mockLogger,
});
});

afterEach(() => {
vi.clearAllMocks();
});

it("returns correct name", () => {
expect(handler.name).toBe("allov2.DirectAllocationStrategy");
});

it("calls DirectAllocatedHandler for DirectAllocated event", async () => {
const mockEvent = {
eventName: "DirectAllocated",
} as ProcessorEvent<"Strategy", "DirectAllocated">;

vi.spyOn(DirectAllocatedHandler.prototype, "handle").mockResolvedValue([]);

await handler.handle(mockEvent);

expect(DirectAllocatedHandler).toHaveBeenCalledWith(mockEvent, chainId, {
metadataProvider: mockMetadataProvider,
roundRepository: mockRoundRepository,
projectRepository: mockProjectRepository,
evmProvider: mockEVMProvider,
pricingProvider: mockPricingProvider,
applicationRepository: mockApplicationRepository,
logger: mockLogger,
});
expect(DirectAllocatedHandler.prototype.handle).toHaveBeenCalled();
});

it("throws UnsupportedEventException for unknown events", async () => {
const mockEvent = {
eventName: "UnknownEvent",
} as unknown as ProcessorEvent<"Strategy", StrategyEvent>;

await expect(handler.handle(mockEvent)).rejects.toThrow(
new UnsupportedEventException("Strategy", "UnknownEvent", handler.name),
);
});
});
Loading

0 comments on commit 7533c07

Please sign in to comment.