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: registry processor #14

Merged
merged 13 commits into from
Oct 23, 2024
7 changes: 5 additions & 2 deletions apps/indexer/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@ contracts:
handler: src/handlers/Allo.ts
events:
- event: PoolCreated(uint256 indexed poolId, bytes32 indexed profileId, address strategy, address token, uint256 amount, (uint256,string) metadata)
- event: RoleGranted(uint64 indexed roleId, address indexed account, uint32 delay, uint48 since, bool newMember)
- event: PoolMetadataUpdated(uint256 indexed poolId, (uint256,string) metadata)
- event: PoolFunded(uint256 indexed poolId, uint256 amount, uint256 fee)
- event: RoleRevoked(uint64 indexed roleId, address indexed account)
- event: RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
- event: RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
- name: Registry
handler: src/handlers/Registry.ts
events:
- event: ProfileCreated(bytes32 indexed profileId, uint256 nonce, string name, (uint256,string) metadata, address owner, address anchor)
- event: ProfileMetadataUpdated(bytes32 indexed profileId, (uint256,string) metadata)
- event: ProfileNameUpdated(bytes32 indexed profileId, string name, address anchor)
- event: ProfileOwnerUpdated(bytes32 indexed profileId, address owner)
- event: RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
- event: RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);

- name: Strategy
handler: src/handlers/Strategy.ts
events:
Expand Down
6 changes: 6 additions & 0 deletions apps/indexer/src/handlers/Registry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Registry } from "../../generated";

// Handler for RoleGranted event
Registry.RoleGranted.handler(async ({}) => {});
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe a TODO for this file

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There is nothing to do here. Won't need to fill the handler with any code, this line is just needed to save the event on the database :)


// Handler for RoleRevoked event
Registry.RoleRevoked.handler(async ({}) => {});

// Handler for ProfileCreated event
Registry.ProfileCreated.handler(async ({}) => {});

Expand Down
4 changes: 4 additions & 0 deletions packages/indexer-client/test/unit/envioIndexerClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ describe("EnvioIndexerClient", () => {
srcAddress: "0x1234567890123456789012345678901234567890",
logIndex: 0,
params: { contractAddress: "0x1234" },
transactionFields: {
hash: "0x123",
transactionIndex: 1,
},
},
];

Expand Down
2 changes: 1 addition & 1 deletion packages/indexer-client/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*"]
"include": ["src/**/*", "test/**/*"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { getToken } from "@grants-stack-indexer/shared/dist/src/internal.js";

import type { IEventHandler, ProcessorDependencies, StrategyTimings } from "../../internal.js";
import { getRoundRoles } from "../../helpers/roles.js";
import { RoundMetadataSchema } from "../../helpers/schemas.js";
import { extractStrategyFromId, getStrategyTimings } from "../../helpers/strategy.js";
import { calculateAmountInUsd } from "../../helpers/tokenMath.js";
import { TokenPriceNotFoundError } from "../../internal.js";
import { RoundMetadataSchema } from "../../schemas/index.js";

type Dependencies = Pick<
ProcessorDependencies,
Expand Down
17 changes: 17 additions & 0 deletions packages/processors/src/interfaces/factory.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
ChainId,
ContractName,
ContractToEventName,
ProtocolEvent,
} from "@grants-stack-indexer/shared";

import { ProcessorDependencies } from "../types/processor.types.js";
import { IEventHandler } from "./index.js";

export interface IEventHandlerFactory<C extends ContractName, E extends ContractToEventName<C>> {
createHandler(
event: ProtocolEvent<C, E>,
chainId: ChainId,
dependencies: ProcessorDependencies,
): IEventHandler<C, E>;
}
1 change: 1 addition & 0 deletions packages/processors/src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./processor.interface.js";
export * from "./factory.interface.js";
export * from "./eventHandler.interface.js";
2 changes: 2 additions & 0 deletions packages/processors/src/registry/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./profileCreated.handler.js";
export * from "./roleGranted.handler.js";
127 changes: 127 additions & 0 deletions packages/processors/src/registry/handlers/profileCreated.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { getAddress } from "viem";

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

import { IEventHandler, ProcessorDependencies } from "../../internal.js";
import { ProjectMetadata, ProjectMetadataSchema } from "../../schemas/projectMetadata.js";

type Dependencies = Pick<
ProcessorDependencies,
"projectRepository" | "evmProvider" | "metadataProvider"
>;
/**
* Handles the ProfileCreated event for the Registry contract from Allo protocol.
*/
export class ProfileCreatedHandler implements IEventHandler<"Registry", "ProfileCreated"> {
constructor(
readonly event: ProtocolEvent<"Registry", "ProfileCreated">,
readonly chainId: ChainId,
private dependencies: Dependencies,
) {}
async handle(): Promise<Changeset[]> {
const { metadataProvider, evmProvider, projectRepository } = this.dependencies;
const profileId = this.event.params.profileId;
const metadataCid = this.event.params.metadata[1];
const metadata = await metadataProvider.getMetadata(metadataCid);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I noticed there's no error handling here--are you okay with the error bubbling up?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, the idea is to buble the error up to the Processors and finally handle the errors there. But will figure it out on a specific milestone that we set up specifically for error handling :)


const parsedMetadata = ProjectMetadataSchema.safeParse(metadata);

let projectType: ProjectType = "canonical";
let isProgram = false;
let metadataValue = null;

if (parsedMetadata.success) {
projectType = this.getProjectTypeFromMetadata(parsedMetadata.data);
isProgram = parsedMetadata.data.type === "program";
metadataValue = parsedMetadata.data;
} else {
//TODO: Replace with logger
console.warn({
msg: `ProfileCreated: Failed to parse metadata for profile ${profileId}`,
event: this.event,
metadataCid,
metadata,
});
}

const createdBy =
this.event.transactionFields.from ??
(await evmProvider.getTransaction(this.event.transactionFields.hash)).from;
const programTags = isProgram ? ["program"] : [];

const changes: Changeset[] = [
{
type: "InsertProject",
args: {
project: {
tags: ["allo-v2", ...programTags],
chainId: this.chainId,
registryAddress: getAddress(this.event.srcAddress),
id: profileId,
name: this.event.params.name,
nonce: this.event.params.nonce,
anchorAddress: getAddress(this.event.params.anchor),
projectNumber: null,
metadataCid: metadataCid,
metadata: metadataValue,
createdByAddress: getAddress(createdBy),
createdAtBlock: BigInt(this.event.blockNumber),
updatedAtBlock: BigInt(this.event.blockNumber),
projectType,
},
},
},
{
type: "InsertProjectRole",
args: {
projectRole: {
chainId: this.chainId,
projectId: this.event.params.profileId,
address: getAddress(this.event.params.owner),
role: "owner",
createdAtBlock: BigInt(this.event.blockNumber),
},
},
},
];

const pendingProjectRoles = await projectRepository.getPendingProjectRolesByRole(
this.chainId,
profileId,
);

if (pendingProjectRoles.length !== 0) {
for (const role of pendingProjectRoles) {
changes.push({
type: "InsertProjectRole",
args: {
projectRole: {
chainId: this.chainId,
projectId: profileId,
address: getAddress(role.address),
role: "member",
createdAtBlock: BigInt(this.event.blockNumber),
},
},
});
}

changes.push({
type: "DeletePendingProjectRoles",
args: { ids: pendingProjectRoles.map((r) => r.id!) },
});
}

return changes;
}

private getProjectTypeFromMetadata(metadata: ProjectMetadata): ProjectType {
// if the metadata contains a canonical reference, it's a linked project
if ("canonical" in metadata) {
return "linked";
}

return "canonical";
}
}
64 changes: 64 additions & 0 deletions packages/processors/src/registry/handlers/roleGranted.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { getAddress } from "viem";

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

import { IEventHandler } from "../../internal.js";
import { ProcessorDependencies } from "../../types/processor.types.js";

Copy link
Collaborator

Choose a reason for hiding this comment

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

add high level overview of the handler

/**
* Handles the RoleGranted event for the Registry contract from Allo protocol.
*/
export class RoleGrantedHandler implements IEventHandler<"Registry", "RoleGranted"> {
constructor(
readonly event: ProtocolEvent<"Registry", "RoleGranted">,
readonly chainId: ChainId,
private readonly dependencies: ProcessorDependencies,
) {}
async handle(): Promise<Changeset[]> {
0xkenj1 marked this conversation as resolved.
Show resolved Hide resolved
const { projectRepository } = this.dependencies;
const role = this.event.params.role.toLocaleLowerCase();
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this supposed to be toLocaleLowerCase rather than toLowerCase()? Not sure if you want different behavior depending on locale-specific case mappings

if (role === ALLO_OWNER_ROLE) {
return [];
}

const account = getAddress(this.event.params.account);
const project = await projectRepository.getProjectById(this.chainId, role);
Copy link
Collaborator

Choose a reason for hiding this comment

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

just double checking that the role will definitely be the profileId unless role === ALLO_OWNER_ROLE. No other validation needed?

Copy link
Collaborator Author

@0xkenj1 0xkenj1 Oct 23, 2024

Choose a reason for hiding this comment

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

no other needed, i will double check it anyways :)


// The member role for an Allo V2 profile, is the profileId itself.
// If a project exists with that id, we create the member role
// If it doesn't exists we create a pending project role. This can happens
Copy link
Collaborator

@jahabeebs jahabeebs Oct 22, 2024

Choose a reason for hiding this comment

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

*exist
*happen

// when a new project is created, since in Allo V2 the RoleGranted event for a member is
// emitted before the ProfileCreated event.
if (project) {
return [
{
type: "InsertProjectRole",
args: {
projectRole: {
chainId: this.chainId,
projectId: project.id,
address: account,
role: "member",
createdAtBlock: BigInt(this.event.blockNumber),
},
},
},
];
}

return [
{
type: "InsertPendingProjectRole",
args: {
pendingProjectRole: {
chainId: this.chainId,
role: role,
address: account,
createdAtBlock: BigInt(this.event.blockNumber),
},
},
},
];
}
}
1 change: 1 addition & 0 deletions packages/processors/src/registry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./registry.processor.js";
34 changes: 34 additions & 0 deletions packages/processors/src/registry/registry.processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Changeset } from "@grants-stack-indexer/repository";
import { ChainId, ProtocolEvent, RegistryEvent } from "@grants-stack-indexer/shared";

import type { IProcessor } from "../internal.js";
import { UnsupportedEventException } from "../internal.js";
import { ProcessorDependencies } from "../types/processor.types.js";
import { ProfileCreatedHandler, RoleGrantedHandler } from "./handlers/index.js";

export class RegistryProcessor implements IProcessor<"Registry", RegistryEvent> {
constructor(
private readonly chainId: ChainId,
private readonly dependencies: ProcessorDependencies,
) {}

async process(event: ProtocolEvent<"Registry", RegistryEvent>): Promise<Changeset[]> {
//TODO: Implement robust error handling and retry logic
switch (event.eventName) {
case "RoleGranted":
return new RoleGrantedHandler(
event as ProtocolEvent<"Registry", "RoleGranted">,
0xkenj1 marked this conversation as resolved.
Show resolved Hide resolved
this.chainId,
this.dependencies,
).handle();
case "ProfileCreated":
return new ProfileCreatedHandler(
event as ProtocolEvent<"Registry", "ProfileCreated">,
0xkenj1 marked this conversation as resolved.
Show resolved Hide resolved
this.chainId,
this.dependencies,
).handle();
default:
throw new UnsupportedEventException("Registry", event.eventName);
}
}
}
2 changes: 2 additions & 0 deletions packages/processors/src/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./projectMetadata.js";
export * from "./roundMetadata.js";
30 changes: 30 additions & 0 deletions packages/processors/src/schemas/projectMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { z } from "zod";

export const ProjectMetadataSchema = z.union([
z
.object({
title: z.string(),
description: z.string(),
})
.passthrough()
.transform((data) => ({ type: "project" as const, ...data })),
z
.object({
canonical: z.object({
registryAddress: z.string(),
chainId: z.coerce.number(),
}),
})
.transform((data) => ({ type: "project" as const, ...data })),
z.object({
type: z.literal("program"),
name: z.string(),
}),
z
.object({
name: z.string(),
})
.transform((data) => ({ type: "program" as const, ...data })),
]);

export type ProjectMetadata = z.infer<typeof ProjectMetadataSchema>;
5 changes: 0 additions & 5 deletions packages/processors/test/index.spec.ts

This file was deleted.

Loading