-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from 10 commits
cc5b5b2
c12fb66
105a70b
46d206b
fcf00b0
775fad8
30c3eaf
ac86f6f
fab3bc2
3991da5
41cd088
3faf95e
38aff32
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
@@ -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>; | ||
} |
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"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./profileCreated.handler.js"; | ||
export * from "./roleGranted.handler.js"; |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; | ||
} | ||
} |
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"; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. *exist |
||
// 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), | ||
}, | ||
}, | ||
}, | ||
]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./registry.processor.js"; |
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); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./projectMetadata.js"; | ||
export * from "./roundMetadata.js"; |
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>; |
This file was deleted.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 :)