Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pool created handler #11

Merged
merged 8 commits into from
Oct 18, 2024
Merged

feat: pool created handler #11

merged 8 commits into from
Oct 18, 2024

Conversation

0xnigir1
Copy link
Collaborator

@0xnigir1 0xnigir1 commented Oct 16, 2024

🤖 Linear

Closes GIT-57 GIT-58

Description

Implements the PoolCreatedHandler on AlloProcessor.
On PoolCreated event, we:

  • check for possible Metadata on the event
  • extract some timestamps data from the strategy contract
  • for MerkleDonation, we calculate an optional matchTokenAmount
  • we calculate an optional initial pool fund amount
  • we return a Changeset (will be modified in the future for event emitter) consisting of:
    • insert the new Round
    • insert each corresponding round roles
    • delete the pending round roles

TODO:

  • move strategy data fetching to the StrategyHandler when we create them
  • get the correct token given the tokenAddress from event data (we will have a whitelisted map)
  • use EVM Provider instead of plain viemClient

Note:
Admin/Manager roles for the pool are emitted before the pool is created so a pending round role is inserted in the db. Now that the PoolCreated event is emitted, we can convert pending roles to actual round roles.

Checklist before requesting a review

  • I have conducted a self-review of my code.
  • I have conducted a QA.
  • If it is a core feature, I have included comprehensive tests.

Copy link

linear bot commented Oct 16, 2024

Copy link
Collaborator

@0xkenj1 0xkenj1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GJ ser, just some comments

process(_event: ProtocolEvent<"Allo", AlloEvent>): Promise<Changeset> {
throw new Error("Method not implemented.");
constructor(
private readonly chainId: ChainId,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can get rid of chainId as attribute, since we don't need processors to be attached to a certain chainId, just the core pipeline. I think we can just use the chainId from the event. Wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought but actually the InsertRound object needs the chainId, we could attach it later but i think that's kindof unclean thing to do, we would be delegating a task to the caller

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0xkenj1 should we pass just the chainId or a Viem Chain type?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My two cents here; probably coupling the proccessor's public interface to viem's types is not the happiest thing to do (ie what if we need to change viem for other lib?)

constructor(
readonly event: ProtocolEvent<"Allo", "PoolCreated">,
private readonly chainId: ChainId,
dependencies: {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can create a type called ProcessorDependencies and use it everywhere, to simplify code

round?: unknown;
application?: unknown;
}>(metadataPointer);
const parsedRoundMetadata = RoundMetadataSchema.safeParse(metadata?.round);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we pass schema validator in getMetadata method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgot i implemented that jajaja

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll come back later to this because i'll need to make adjustments since code flow should go on even if validation fails, so i need to make adjustments to the metadata provider
this makes me think if it's correct to have the validation function as arg 🤔

Comment on lines 90 to 91
symbol: "USDC", //TODO: get symbol from token
name: "USDC", //TODO: get name from token
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would leave these two as unknown wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually idk what the TokenType will have, when we copy the map from the old repo we'll see

Comment on lines 94 to 99
let strategyTimings: {
applicationsStartTime: Date | null;
applicationsEndTime: Date | null;
donationsStartTime: Date | null;
donationsEndTime: Date | null;
} = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type StrategyTimings ?

Comment on lines 189 to 251
let registrationStartTimeResolved: bigint;
let registrationEndTimeResolved: bigint;
let allocationStartTimeResolved: bigint;
let allocationEndTimeResolved: bigint;

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,
},
],
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];
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let registrationStartTimeResolved: bigint;
let registrationEndTimeResolved: bigint;
let allocationStartTimeResolved: bigint;
let allocationEndTimeResolved: bigint;
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,
},
],
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];
}
const results: {
registrationStartTimeResolved: bigint;
registrationEndTimeResolved: bigint;
allocationStartTimeResolved: bigint;
allocationEndTimeResolved: bigint;
};
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,
},
];
if (viemClient.chain?.contracts?.multicall3) {
results = await viemClient.multicall({
contracts: contractCalls,
allowFailure: false,
});
} else {
results = await Promise.all(
contractCalls.map((contractCall) =>
viemClient.readContract(contractCall)
)
);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't know that trick 🆒

Comment on lines 276 to 313
let registrationStartTimeResolved: bigint;
let registrationEndTimeResolved: bigint;

if (viemClient.chain?.contracts?.multicall3) {
const results = await viemClient.multicall({
contracts: [
{
abi: DirectGrantsLiteStrategy,
functionName: "registrationStartTime",
address: strategyAddress,
},
{
abi: DirectGrantsLiteStrategy,
functionName: "registrationEndTime",
address: strategyAddress,
},
],
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];
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shoulds in tests

Comment on lines 25 to 28
metadata: {
pointer: string;
protocol: bigint;
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the data for metadata comes as a tuple/array

Suggested change
metadata: {
pointer: string;
protocol: bigint;
};
metadata: [
pointer: string;
protocol: bigint;
];

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aaaa thanks for the heads up

},
};

return mergeDeep(defaultEvent, overrides) as ProtocolEvent<"Allo", "PoolCreated">;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💎

Comment on lines 192 to 197
} catch (error: unknown) {
console.error(
`An error occurred while processing the PoolCreated event. Event: ${this.event} - Error: ${error}`,
);
return [];
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think at handlers level we just need to throw the errors, and let the processor to handle them and take actions based on the type of error (like for example retry the processing) and we avoid repeating logic of error handling on the handlers. wdyt ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like that approach 🫡

@0xnigir1 0xnigir1 requested a review from 0xkenj1 October 16, 2024 21:48
Copy link
Collaborator

@0xyaco 0xyaco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool stuff! Eager to see more PRs; left some random comments.

process(_event: ProtocolEvent<"Allo", AlloEvent>): Promise<Changeset> {
throw new Error("Method not implemented.");
constructor(
private readonly chainId: ChainId,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My two cents here; probably coupling the proccessor's public interface to viem's types is not the happiest thing to do (ie what if we need to change viem for other lib?)


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/";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the last / mean something?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wildcat eye to catch that 👁️

Comment on lines +22 to +32
export function extractStrategyFromId(_id: Address): Strategy | undefined {
const id = _id.toLowerCase();
/* eslint-disable no-fallthrough */
switch (id) {
// SQFSuperfluidv1
case "0xf8a14294e80ff012e54157ec9d1b2827421f1e7f6bde38c06730b1c031b3f935":
return {
id: id as SanitizedStrategyId,
name: "allov2.SQFSuperFluidStrategy",
groups: ["allov2.SQFSuperFluidStrategy"],
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asking to get more business context; are strategies going to be always hardcoded or do you plan to use some kind of API/service/repository to get these mappings?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll have a mapping in shared, from ID to the handler class actually. This is just the same class from the current indexer

I'll add the TODO to make it clear

Comment on lines 11 to 16
export const calculateAmountInUsd = (
amount: bigint,
tokenPriceInUsd: number,
tokenDecimals: number,
truncateDecimals?: number,
): number => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd strongly suggest including at this point of the project, given that you are building the bases of your features, some kind of bigdecimal/bignumber lib to handle money values; anywhere you mention USD, price, etc. It'll make the development extremely robust and you won't need to worry about Number precision, "stringification" of numbers, "numberification" of strings, etc.

There are two drawbacks that you might need to take into account:

  • Operations between these values might imply some special methods (eg a.plus(b)) and not the js operators (a + b).
  • Performance will be definitely worse, but if you are not doing 100M of operations per second it shouldn't be something to worry about.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an bonus comment to show the degeneracy of JS numbers implementation, try doing this:

const n = 9332654729891549;
console.log(n.toString());

Check the last digit of the output. ☠️

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great contribution ser 🫡
I also lean into this but we would have to change the DB data type at some point since now they handle it like this with truncation (don't ask why)

cc @0xkenj1 should we make the change now or leave it as a todo/nth for after the MVP?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed offline, we ended up using bignumber and will change DB types to numeric, so we return string instead of hardcasting to number

@0xnigir1 0xnigir1 requested a review from 0xyaco October 17, 2024 19:55
jahabeebs
jahabeebs previously approved these changes Oct 17, 2024
Copy link
Collaborator

@jahabeebs jahabeebs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good, just some optional suggestions 👍🏻


if (truncateDecimals !== undefined) {
if (truncateDecimals < 0 || truncateDecimals > 18) {
throw new Error("Truncate decimals must be between 0 and 18");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if we prefer a custom error here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do : D

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right ser 💯

case "PoolCreated":
return new PoolCreatedHandler(event, this.chainId, this.dependencies).handle();
default:
throw new Error(`Unknown event name: ${event.eventName}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if we prefer a custom error here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes we do 🤣

actually here i have my implementation for this error:

import { ContractName } from "@grants-stack-indexer/shared";

export class UnsupportedEventException extends Error {
    constructor(
        contract: ContractName,
        public readonly eventName: string,
    ) {
        super(`Event ${eventName} unsupported for ${contract} processor`);
    }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for this xd

case "PoolCreated":
return new PoolCreatedHandler(event, this.chainId, this.dependencies).handle();
default:
throw new Error(`Unknown event name: ${event.eventName}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes we do 🤣

actually here i have my implementation for this error:

import { ContractName } from "@grants-stack-indexer/shared";

export class UnsupportedEventException extends Error {
    constructor(
        contract: ContractName,
        public readonly eventName: string,
    ) {
        super(`Event ${eventName} unsupported for ${contract} processor`);
    }
}


/**
* Gets the strategy data for the DonationVotingMerkleDistributionDirectTransferStrategy
* @param viemClient - The viem client
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param viemClient - The viem client
* @param evmProvider


/**
* Gets the strategy data for the DirectGrantsStrategy
* @param viemClient - The viem client
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto


if (truncateDecimals !== undefined) {
if (truncateDecimals < 0 || truncateDecimals > 18) {
throw new Error("Truncate decimals must be between 0 and 18");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do : D

Copy link
Collaborator

@jahabeebs jahabeebs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻

@0xnigir1 0xnigir1 merged commit f585a86 into dev Oct 18, 2024
6 checks passed
@0xnigir1 0xnigir1 deleted the feat/pool-created-handler branch October 18, 2024 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants