-
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: all remaining event handlers for DVMD strategy #30
Changes from 1 commit
b442db8
4187c7c
980f1f6
2bef2e4
b0d96ab
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 |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export enum ApplicationStatus { | ||
NONE = 0, | ||
PENDING, | ||
APPROVED, | ||
REJECTED, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./enums.js"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export class MetadataNotFound extends Error { | ||
constructor(message: string) { | ||
super(message); | ||
this.name = "MetadataNotFoundError"; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from "./projectMetadata.js"; | ||
export * from "./roundMetadata.js"; | ||
export * from "./applicationMetadata.js"; | ||
export * from "./matchingDistribution.js"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { z } from "zod"; | ||
|
||
export type MatchingDistribution = z.infer<typeof MatchingDistributionSchema>; | ||
|
||
// handle ethers bigint serialization | ||
const BigIntSchema = z.string().or( | ||
z.object({ type: z.literal("BigNumber"), hex: z.string() }).transform((val) => { | ||
return BigInt(val.hex).toString(); | ||
}), | ||
); | ||
|
||
export const MatchingDistributionSchema = z.object({ | ||
matchingDistribution: z.array( | ||
z.object({ | ||
applicationId: z.string(), | ||
projectPayoutAddress: z.string(), | ||
projectId: z.string(), | ||
projectName: z.string(), | ||
matchPoolPercentage: z.coerce.number(), | ||
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. this will coerce empty strings to 0 rather than fail validation--just checking if you're ok with this. If you'd prefer to fail validation you can check out this: colinhacks/zod#2461 (comment) 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. interesting 👀 , have to say that '' being 0 is kinda unexpected |
||
contributionsCount: z.coerce.number(), | ||
originalMatchAmountInToken: BigIntSchema.default("0"), | ||
matchAmountInToken: BigIntSchema.default("0"), | ||
}), | ||
), | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { getAddress } from "viem"; | ||
|
||
import { Changeset } from "@grants-stack-indexer/repository"; | ||
import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; | ||
|
||
import { | ||
IEventHandler, | ||
MetadataNotFound, | ||
MetadataParsingFailed, | ||
ProcessorDependencies, | ||
} from "../../internal.js"; | ||
import { MatchingDistribution, MatchingDistributionSchema } from "../../schemas/index.js"; | ||
|
||
type Dependencies = Pick<ProcessorDependencies, "metadataProvider" | "logger">; | ||
|
||
/** | ||
* BaseDistributionUpdatedHandler: Processes 'DistributionUpdated' events | ||
* | ||
* - Decodes the updated distribution metadata | ||
* - Creates a changeset to update the round with the new distribution | ||
* - Serves as a base class as all strategies share the same logic for this event. | ||
* | ||
* @dev: | ||
* - Strategy handlers that want to handle the DistributionUpdated event should create an instance of this class corresponding to the event. | ||
* | ||
*/ | ||
|
||
export class BaseDistributionUpdatedHandler | ||
implements IEventHandler<"Strategy", "DistributionUpdated"> | ||
{ | ||
constructor( | ||
readonly event: ProcessorEvent<"Strategy", "DistributionUpdated">, | ||
private readonly chainId: ChainId, | ||
private readonly dependencies: Dependencies, | ||
) {} | ||
|
||
async handle(): Promise<Changeset[]> { | ||
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. inheritdoc? |
||
const { logger, metadataProvider } = this.dependencies; | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const [_, pointer] = this.event.params.metadata; | ||
|
||
const strategyAddress = getAddress(this.event.srcAddress); | ||
const rawDistribution = await metadataProvider.getMetadata< | ||
MatchingDistribution | undefined | ||
>(pointer); | ||
|
||
if (!rawDistribution) { | ||
logger.warn(`No matching distribution found for pointer: ${pointer}`); | ||
|
||
throw new MetadataNotFound(`No matching distribution found for pointer: ${pointer}`); | ||
} | ||
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. Should MetadataProvider be responsible for throwing a "not found" error instead of having the handler doing it? |
||
|
||
const distribution = MatchingDistributionSchema.safeParse(rawDistribution); | ||
|
||
if (!distribution.success) { | ||
logger.warn(`Failed to parse matching distribution: ${distribution.error.message}`); | ||
|
||
throw new MetadataParsingFailed( | ||
`Failed to parse matching distribution: ${distribution.error.message}`, | ||
); | ||
} | ||
|
||
return [ | ||
{ | ||
type: "UpdateRoundByStrategyAddress", | ||
args: { | ||
chainId: this.chainId, | ||
strategyAddress, | ||
round: { | ||
readyForPayoutTransaction: this.event.transactionFields.hash, | ||
matchingDistribution: distribution.data.matchingDistribution, | ||
}, | ||
}, | ||
}, | ||
]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { Address, getAddress } from "viem"; | ||
|
||
import { Application, Changeset, Round } from "@grants-stack-indexer/repository"; | ||
import { ChainId, ProcessorEvent } from "@grants-stack-indexer/shared"; | ||
|
||
import { | ||
ApplicationNotFound, | ||
IEventHandler, | ||
ProcessorDependencies, | ||
RoundNotFound, | ||
} from "../../internal.js"; | ||
|
||
type Dependencies = Pick< | ||
ProcessorDependencies, | ||
"roundRepository" | "applicationRepository" | "logger" | ||
>; | ||
|
||
/** | ||
* BaseFundsDistributedHandler: Processes 'FundsDistributed' events | ||
* | ||
* - Handles funds distributed events across all strategies. | ||
* - Creates two changesets: | ||
* 1. UpdateApplication: Updates the application with the transaction hash. | ||
* 2. IncrementRoundTotalDistributed: Increments the total distributed amount for a round. | ||
* - Serves as a base class as all strategies share the same logic for this event. | ||
* | ||
* @dev: | ||
* - Strategy handlers that want to handle the FundsDistributed event should create an instance of this class corresponding to the event. | ||
* | ||
*/ | ||
|
||
export class BaseFundsDistributedHandler implements IEventHandler<"Strategy", "FundsDistributed"> { | ||
constructor( | ||
readonly event: ProcessorEvent<"Strategy", "FundsDistributed">, | ||
private readonly chainId: ChainId, | ||
private readonly dependencies: Dependencies, | ||
) {} | ||
|
||
/** | ||
* Handles the FundsDistributed event. | ||
* @throws {RoundNotFound} if the round is not found. | ||
* @throws {ApplicationNotFound} if the application is not found. | ||
* @returns An array of changesets with the following: | ||
* 1. UpdateApplication: Updates the application with the transaction hash. | ||
* 2. IncrementRoundTotalDistributed: Increments the total distributed amount for a round. | ||
*/ | ||
async handle(): Promise<Changeset[]> { | ||
const strategyAddress = getAddress(this.event.srcAddress); | ||
const round = await this.getRoundOrThrow(strategyAddress); | ||
|
||
const roundId = round.id; | ||
const anchorAddress = getAddress(this.event.params.recipientId); | ||
const application = await this.getApplicationOrThrow(roundId, anchorAddress); | ||
|
||
return [ | ||
{ | ||
type: "UpdateApplication", | ||
args: { | ||
chainId: this.chainId, | ||
roundId, | ||
applicationId: application.id, | ||
application: { | ||
distributionTransaction: this.event.transactionFields.hash, | ||
}, | ||
}, | ||
}, | ||
{ | ||
type: "IncrementRoundTotalDistributed", | ||
args: { | ||
chainId: this.chainId, | ||
roundId: round.id, | ||
amount: this.event.params.amount, | ||
}, | ||
}, | ||
]; | ||
} | ||
|
||
/** | ||
* Retrieves a round by its strategy address. | ||
* @param {Address} strategyAddress - The address of the strategy. | ||
* @returns {Promise<Round>} The round found. | ||
* @throws {RoundNotFound} if the round does not exist. | ||
*/ | ||
private async getRoundOrThrow(strategyAddress: Address): Promise<Round> { | ||
const { roundRepository } = this.dependencies; | ||
const round = await roundRepository.getRoundByStrategyAddress( | ||
this.chainId, | ||
strategyAddress, | ||
); | ||
|
||
if (!round) { | ||
throw new RoundNotFound(this.chainId, strategyAddress); | ||
} | ||
|
||
return round; | ||
} | ||
|
||
/** | ||
* Retrieves an application by its round ID and recipient address. | ||
* @param {string} roundId - The ID of the round. | ||
* @param {Address} recipientId - The address of the recipient. | ||
* @returns {Promise<Application>} The application found. | ||
* @throws {ApplicationNotFound} if the application does not exist. | ||
*/ | ||
private async getApplicationOrThrow( | ||
roundId: string, | ||
anchorAddress: Address, | ||
): Promise<Application> { | ||
const { applicationRepository } = this.dependencies; | ||
const application = await applicationRepository.getApplicationByAnchorAddress( | ||
this.chainId, | ||
roundId, | ||
anchorAddress, | ||
); | ||
|
||
if (!application) { | ||
throw new ApplicationNotFound(this.chainId, roundId, anchorAddress); | ||
} | ||
|
||
return application; | ||
} | ||
} |
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.
do you mean viem?
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.
just copy-pasted the comment, will delete it