From 05a963cd2e9199070f225a016d8d0f8df2d5005e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Comerci?= <45410089+ncomerci@users.noreply.github.com> Date: Thu, 4 Jul 2024 11:05:46 -0300 Subject: [PATCH] feat: New project events (#1874) * new events migration * proposal finish event * added project enacted event --- src/entities/Proposal/jobs.ts | 2 + .../1719945719397_add-new-event-types.ts | 11 +++++ src/services/ProposalService.ts | 1 + src/services/events.ts | 42 +++++++++++++++++++ src/shared/types/events.ts | 24 +++++++++++ 5 files changed, 80 insertions(+) create mode 100644 src/migrations/1719945719397_add-new-event-types.ts diff --git a/src/entities/Proposal/jobs.ts b/src/entities/Proposal/jobs.ts index 42f2bd36d..373df5efc 100644 --- a/src/entities/Proposal/jobs.ts +++ b/src/entities/Proposal/jobs.ts @@ -10,6 +10,7 @@ import { ErrorService } from '../../services/ErrorService' import { ProjectService } from '../../services/ProjectService' import { ProposalService } from '../../services/ProposalService' import { DiscordService } from '../../services/discord' +import { EventsService } from '../../services/events' import { NotificationService } from '../../services/notification' import { ErrorCategory } from '../../utils/errorCategories' import { isProdEnv } from '../../utils/governanceEnvs' @@ -209,6 +210,7 @@ export async function finishProposal() { BadgesService.giveFinishProposalBadges(proposalsWithOutcome) DiscourseService.commentFinishedProposals(proposalsWithOutcome) DiscordService.notifyFinishedProposals(proposalsWithOutcome) + await EventsService.proposalFinished(proposalsWithOutcome) } catch (error) { ErrorService.report('Error finishing proposals', { error, category: ErrorCategory.Job }) } diff --git a/src/migrations/1719945719397_add-new-event-types.ts b/src/migrations/1719945719397_add-new-event-types.ts new file mode 100644 index 000000000..d47d0851c --- /dev/null +++ b/src/migrations/1719945719397_add-new-event-types.ts @@ -0,0 +1,11 @@ +import type { MigrationBuilder } from "node-pg-migrate" +import { EventType } from "../shared/types/events" + +export async function up(pgm: MigrationBuilder): Promise { + pgm.addTypeValue({name: 'event_type'}, EventType.ProposalFinished) + pgm.addTypeValue({name: 'event_type'}, EventType.VestingCreated) +} + +export async function down(): Promise { + return +} \ No newline at end of file diff --git a/src/services/ProposalService.ts b/src/services/ProposalService.ts index cd529c617..a11b7a9de 100644 --- a/src/services/ProposalService.ts +++ b/src/services/ProposalService.ts @@ -306,6 +306,7 @@ export class ProposalService { const project = await ProjectService.getUpdatedProject(proposal.project_id!) updatedProposal.project_status = project.status NotificationService.projectProposalEnacted(proposal) + await EventsService.projectEnacted(project) } DiscourseService.commentUpdatedProposal(updatedProposal) diff --git a/src/services/events.ts b/src/services/events.ts index 20904895a..0f8dfe15b 100644 --- a/src/services/events.ts +++ b/src/services/events.ts @@ -3,6 +3,7 @@ import { ethers } from 'ethers' import isEthereumAddress from 'validator/lib/isEthereumAddress' import ProposalModel from '../entities/Proposal/model' +import { ProposalWithOutcome } from '../entities/Proposal/outcome' import { ProposalAttributes } from '../entities/Proposal/types' import { SNAPSHOT_SPACE } from '../entities/Snapshot/constants' import UpdateModel from '../entities/Updates/model' @@ -12,6 +13,7 @@ import { UserAttributes } from '../entities/User/types' import { DISCOURSE_USER } from '../entities/User/utils' import { addressShortener } from '../helpers' import EventModel from '../models/Event' +import type { Project } from '../models/Project' import CacheService, { TTL_1_HS } from '../services/CacheService' import { DiscourseService } from '../services/DiscourseService' import { ErrorService } from '../services/ErrorService' @@ -27,11 +29,14 @@ import { ProjectUpdateCommentedEvent, ProposalCommentedEvent, ProposalCreatedEvent, + ProposalFinishedEvent, UpdateCreatedEvent, + VestingCreatedEvent, VotedEvent, } from '../shared/types/events' import { DEFAULT_AVATAR_IMAGE, getProfiles } from '../utils/Catalyst' import { DclProfile } from '../utils/Catalyst/types' +import Time from '../utils/date/Time' import { ErrorCategory } from '../utils/errorCategories' import { NotificationService } from './notification' @@ -354,6 +359,43 @@ export class EventsService { } } + static async proposalFinished(proposalsWithOutcome: ProposalWithOutcome[]) { + for (const proposal of proposalsWithOutcome) { + const { id, title, newStatus, finish_at, user } = proposal + const finishEvent: ProposalFinishedEvent = { + id: crypto.randomUUID(), + address: user, + event_type: EventType.ProposalFinished, + event_data: { proposal_id: id, proposal_title: title, new_status: newStatus }, + created_at: new Date(finish_at), + } + await EventModel.create(finishEvent) + } + } + + static async projectEnacted(project: Project) { + const { author, id, proposal_id, funding } = project + if (!funding || !funding.vesting) { + ErrorService.report('Project enacted without vesting', { project_id: id, category: ErrorCategory.Events }) + return + } + const { years, months, days } = Time(funding.vesting.finish_at).preciseDiff(Time(funding.vesting.start_at), true) + const vestingEvent: VestingCreatedEvent = { + id: crypto.randomUUID(), + address: author, + event_type: EventType.VestingCreated, + event_data: { + proposal_id, + proposal_title: project.title, + vesting_address: funding.vesting.address, + amount: funding.vesting.total, + duration_in_months: years * 12 + months + (days > 0 ? 1 : 0), + }, + created_at: funding.enacted_at ? new Date(funding.enacted_at) : new Date(), + } + await EventModel.create(vestingEvent) + } + private static decodeLogTopics(topics: string[]) { const methodSignature = topics[0] const delegator = this.decodeTopicToAddress(topics[1]) diff --git a/src/shared/types/events.ts b/src/shared/types/events.ts index 6f01c89b9..5ceedd665 100644 --- a/src/shared/types/events.ts +++ b/src/shared/types/events.ts @@ -1,5 +1,7 @@ import { z } from 'zod' +import { ProposalStatus } from '../../entities/Proposal/types' + import { DiscourseWebhookPost } from './discourse' export type CommonEventAttributes = { @@ -26,6 +28,16 @@ export type DiscourseEventData = { export type ProposalCommentedEventData = DiscourseEventData & ProposalEventData export type UpdateCommentedEventData = DiscourseEventData & UpdateEventData & ProposalEventData +export type ProposalFinishedEventData = ProposalEventData & { + new_status: ProposalStatus +} + +export type VestingCreatedEventData = ProposalEventData & { + vesting_address: string + amount: number + duration_in_months: number +} + type DelegationSetData = { new_delegate: string | null transaction_hash: string @@ -39,11 +51,13 @@ type DelegationClearData = { export enum EventType { Voted = 'voted', ProposalCreated = 'proposal_created', + ProposalFinished = 'proposal_finished', UpdateCreated = 'update_created', ProposalCommented = 'proposal_commented', ProjectUpdateCommented = 'project_update_commented', DelegationSet = 'delegation_set', DelegationClear = 'delegation_clear', + VestingCreated = 'vesting_created', } export const EventFilterSchema = z.object({ @@ -64,6 +78,16 @@ export type ProposalCreatedEvent = { event_data: ProposalEventData } & CommonEventAttributes +export type ProposalFinishedEvent = { + event_type: EventType.ProposalFinished + event_data: ProposalFinishedEventData +} & CommonEventAttributes + +export type VestingCreatedEvent = { + event_type: EventType.VestingCreated + event_data: VestingCreatedEventData +} & CommonEventAttributes + export type UpdateCreatedEvent = { event_type: EventType.UpdateCreated event_data: UpdateCreatedEventData