Skip to content

Commit

Permalink
chore: cliff ending soon notification (#1891)
Browse files Browse the repository at this point in the history
* chore: cliff ending soon notification

* chore: fetch vestings that ended within 24 hs, add cron job, filter paused or revoked contracts

* chore: include bids, lowercase addresses before querying, coauthors fix, vestings query fix

* chore: try/catch cliff notifications job
  • Loading branch information
1emu authored Aug 28, 2024
1 parent 2b83ba6 commit 77373c3
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 97 deletions.
118 changes: 59 additions & 59 deletions src/clients/VestingsSubgraph.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
import fetch from 'isomorphic-fetch'

import { VESTINGS_QUERY_ENDPOINT } from '../entities/Snapshot/constants'
import Time from '../utils/date/Time'

import { SubgraphVesting } from './VestingSubgraphTypes'
import { trimLastForwardSlash } from './utils'

const OLDEST_INDEXED_BLOCK = 20463272

const VESTING_FIELDS = `id
version
duration
cliff
beneficiary
revoked
revocable
released
start
periodDuration
vestedPerPeriod
paused
pausable
stop
linear
token
owner
total
revokeTimestamp
releaseLogs{
id
timestamp
amount
}
pausedLogs{
id
timestamp
eventType
}`

export class VestingsSubgraph {
static Cache = new Map<string, VestingsSubgraph>()
private readonly queryEndpoint: string
Expand Down Expand Up @@ -41,39 +72,10 @@ export class VestingsSubgraph {
const query = `
query getVesting($address: String!) {
vestings(where: { id: $address }){
id
version
duration
cliff
beneficiary
revoked
revocable
released
start
periodDuration
vestedPerPeriod
paused
pausable
stop
linear
token
owner
total
revokeTimestamp
releaseLogs{
id
timestamp
amount
}
pausedLogs{
id
timestamp
eventType
}
${VESTING_FIELDS}
}
}
`

const variables = { address: address.toLowerCase() }
const response = await fetch(this.queryEndpoint, {
method: 'post',
Expand All @@ -97,35 +99,7 @@ export class VestingsSubgraph {
const query = `
query getVestings(${addressesParam}) {
vestings(${addressesQuery}){
id
version
duration
cliff
beneficiary
revoked
revocable
released
start
periodDuration
vestedPerPeriod
paused
pausable
stop
linear
token
owner
total
revokeTimestamp
releaseLogs{
id
timestamp
amount
}
pausedLogs{
id
timestamp
eventType
}
${VESTING_FIELDS}
}
}
`
Expand All @@ -144,4 +118,30 @@ export class VestingsSubgraph {
const body = await response.json()
return body?.data?.vestings || []
}

async getVestingsWithRecentlyEndedCliffs(): Promise<SubgraphVesting[]> {
const currentTimestamp = Time().getTime()
const aDayAgoTimestamp = Time().subtract(1, 'day').getTime()
const query = `
query getVestings($currentTimestamp: Int!, $aDayAgoTimestamp: Int!) {
vestings(where: { cliff_gt: $aDayAgoTimestamp, cliff_lt: $currentTimestamp,
revoked:false, paused:false}) {
${VESTING_FIELDS}
}
}
`

const variables = { currentTimestamp, aDayAgoTimestamp }
const response = await fetch(this.queryEndpoint, {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
variables,
}),
})

const body = await response.json()
return body?.data?.vestings || []
}
}
6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ function getBooleanStringVar(variableName: string, defaultValue: boolean) {
return defaultValue
}

export function getVestingContractUrl(address: string) {
return VESTING_DASHBOARD_URL.replace('%23', '#').concat(address.toLowerCase())
}

export const GOVERNANCE_URL = process.env.GATSBY_GOVERNANCE_URL || 'https://decentraland.zone/governance'
export const GOVERNANCE_API = process.env.GATSBY_GOVERNANCE_API || ''
export const FORUM_URL = process.env.GATSBY_DISCOURSE_API || ''
Expand Down Expand Up @@ -54,3 +58,5 @@ export const DCL_META_IMAGE_URL = 'https://decentraland.org/images/decentraland.
export const JOIN_DISCORD_URL = 'https://dcl.gg/discord'
export const BLOCKNATIVE_API_KEY = process.env.BLOCKNATIVE_API_KEY || ''
export const REASON_THRESHOLD = Number(process.env.GATSBY_REASON_THRESHOLD)

export const VESTING_DASHBOARD_URL = 'https://decentraland.org/vesting/%23/'
12 changes: 12 additions & 0 deletions src/entities/Proposal/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DiscourseService } from '../../services/DiscourseService'
import { ErrorService } from '../../services/ErrorService'
import { ProjectService } from '../../services/ProjectService'
import { ProposalService } from '../../services/ProposalService'
import { VestingService } from '../../services/VestingService'
import { DiscordService } from '../../services/discord'
import { EventsService } from '../../services/events'
import { NotificationService } from '../../services/notification'
Expand Down Expand Up @@ -251,3 +252,14 @@ async function updateProposalsAndBudgets(proposalsWithOutcome: ProposalWithOutco
client.release()
}
}

export async function notifyCliffEndingSoon() {
try {
const vestings = await VestingService.getVestingsWithRecentlyEndedCliffs()
const vestingAddresses = vestings.map((vesting) => vesting.address)
const proposalContributors = await ProposalService.findContributorsForProposalsByVestings(vestingAddresses)
await NotificationService.cliffEnded(proposalContributors)
} catch (error) {
ErrorService.report('Error notifying cliff ending soon', { error, category: ErrorCategory.Job })
}
}
32 changes: 32 additions & 0 deletions src/entities/Proposal/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
PriorityProposal,
PriorityProposalType,
ProposalAttributes,
ProposalContributors,
ProposalListFilter,
ProposalStatus,
ProposalType,
Expand Down Expand Up @@ -635,4 +636,35 @@ export default class ProposalModel extends Model<ProposalAttributes> {

return await this.namedQuery('get_priority_proposals', query)
}

static async findContributorsForProposalsByVestings(vestingAddresses: string[]): Promise<ProposalContributors[]> {
const query = SQL`
SELECT
p.id,
p.title,
COALESCE(array_agg(co.address) FILTER (WHERE co.address IS NOT NULL), '{}') AS coauthors,
p.vesting_addresses,
p.user,
p.configuration
FROM ${table(ProposalModel)} p
LEFT JOIN ${table(CoauthorModel)} co ON p.id = co.proposal_id
AND co.status = ${CoauthorStatus.APPROVED}
WHERE
p.type IN (${ProposalType.Grant}, ${ProposalType.Bid})
AND
EXISTS (
SELECT 1
FROM unnest(p.vesting_addresses) AS vesting_address
WHERE LOWER(vesting_address) = ANY(${vestingAddresses})
)
GROUP BY
p.id,
p.title,
p.vesting_addresses,
p.user
ORDER BY p.created_at DESC;
`

return await this.namedQuery<ProposalContributors>('get_authors_and_coauthors_for_vestings', query)
}
}
7 changes: 7 additions & 0 deletions src/entities/Proposal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,3 +828,10 @@ export type PriorityProposal = Pick<
}

export type LinkedProposal = Pick<ProposalAttributes, 'id' | 'finish_at' | 'start_at' | 'created_at'>

export type ProposalContributors = Pick<
ProposalAttributes,
'id' | 'title' | 'user' | 'vesting_addresses' | 'configuration'
> & {
coauthors?: string[]
}
3 changes: 2 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import swaggerUi from 'swagger-ui-express'
import YAML from 'yaml'

import { updateGovernanceBudgets } from './entities/Budget/jobs'
import { activateProposals, finishProposal, publishBids } from './entities/Proposal/jobs'
import { activateProposals, finishProposal, notifyCliffEndingSoon, publishBids } from './entities/Proposal/jobs'
import { giveAndRevokeLandOwnerBadges, giveTopVoterBadges, runQueuedAirdropJobs } from './jobs/BadgeAirdrop'
import { pingSnapshot } from './jobs/PingSnapshot'
import { withLock } from './jobs/jobLocks'
Expand Down Expand Up @@ -50,6 +50,7 @@ jobs.cron('@eachMinute', activateProposals)
jobs.cron('@each5Minute', withLock('publishBids', publishBids))
jobs.cron('@each10Second', pingSnapshot)
jobs.cron('30 0 * * *', updateGovernanceBudgets) // Runs at 00:30 daily
jobs.cron('35 0 * * *', notifyCliffEndingSoon) // Runs at 00:35 daily
jobs.cron('30 1 * * *', runQueuedAirdropJobs) // Runs at 01:30 daily
jobs.cron('30 2 * * *', giveAndRevokeLandOwnerBadges) // Runs at 02:30 daily
jobs.cron('30 3 1 * *', giveTopVoterBadges) // Runs at 03:30 on the first day of the month
Expand Down
4 changes: 4 additions & 0 deletions src/services/ProposalService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,4 +335,8 @@ export class ProposalService {
}
return update
}

static async findContributorsForProposalsByVestings(vestingAddresses: string[]) {
return await ProposalModel.findContributorsForProposalsByVestings(vestingAddresses)
}
}
7 changes: 6 additions & 1 deletion src/services/VestingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export class VestingService {
return sortedVestings
}

static async getVestingsWithRecentlyEndedCliffs(): Promise<VestingWithLogs[]> {
const vestingsData = await VestingsSubgraph.get().getVestingsWithRecentlyEndedCliffs()
return vestingsData.map(this.parseSubgraphVesting)
}

static async getVestingWithLogs(
vestingAddress: string | null | undefined,
proposalId?: string
Expand Down Expand Up @@ -142,7 +147,7 @@ export class VestingService {
const token = getTokenSymbolFromAddress(vestingData.token)

return {
address: vestingData.id,
address: vestingData.id.toLowerCase(),
cliff: toISOString(cliffEnd),
vestedPerPeriod: vestingData.vestedPerPeriod.map(Number),
...getVestingDates(contractStart, contractEndsTimestamp),
Expand Down
Loading

0 comments on commit 77373c3

Please sign in to comment.