Skip to content

Commit

Permalink
feat: add whale vote and voted on behalf notifications (#1889)
Browse files Browse the repository at this point in the history
* feat: add whale vote and voted on behalf notifications

* fix: logs texts

* feat: move whale vote threshold to env var and fix address check
  • Loading branch information
andyesp authored Aug 13, 2024
1 parent 7db165f commit a1dc8e1
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ GATSBY_GOVERNANCE_API=https://localhost:8000/api
# Snapshot integration
GATSBY_SNAPSHOT_API=https://testnet.hub.snapshot.org/
GATSBY_SNAPSHOT_URL=https://testnet.snapshot.org/
GATSBY_SNAPSHOT_QUERY_ENDPOINT=https://api.thegraph.com/subgraphs/name/snapshot-labs/snapshot
GATSBY_SNAPSHOT_SPACE=daotest.dcl.eth
GATSBY_SNAPSHOT_DURATION=600
GATSBY_SNAPSHOT_ADDRESS=
Expand Down Expand Up @@ -109,6 +108,7 @@ NOTIFICATIONS_SERVICE_ENABLED=false
PUSH_API_URL=https://backend-staging.epns.io
PUSH_CHANNEL_OWNER_PK=
GATSBY_PUSH_CHANNEL_ID=
WHALE_VOTE_THRESHOLD=

# New Decentraland Notifications Service
DCL_NOTIFICATIONS_SERVICE_ENABLED=false
Expand Down
5 changes: 2 additions & 3 deletions src/clients/SnapshotSubgraph.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fetch from 'isomorphic-fetch'

import { SNAPSHOT_QUERY_ENDPOINT } from '../entities/Snapshot/constants'
import { PICKED_BY_QUERY } from '../entities/Snapshot/queries'
import { PICKED_BY_QUERY, getDelegatedQuery } from '../entities/Snapshot/queries'

import { Delegation } from './SnapshotTypes'
import { inBatches, trimLastForwardSlash } from './utils'
Expand Down Expand Up @@ -48,7 +48,6 @@ export class SnapshotSubgraph {

async getDelegates(
key: 'delegatedTo' | 'delegatedFrom',
query: string,
variables: { address: string; space: string; blockNumber?: string | number }
) {
const delegations: Delegation[] = await inBatches(
Expand All @@ -57,7 +56,7 @@ export class SnapshotSubgraph {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
query: getDelegatedQuery(key, variables.blockNumber),
variables: { ...vars, skip, first },
}),
})
Expand Down
3 changes: 2 additions & 1 deletion src/entities/Snapshot/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const getDelegationType = (key: 'delegatedTo' | 'delegatedFrom') => {

export const getDelegatedQuery = (key: 'delegatedTo' | 'delegatedFrom', blockNumber?: BlockNumber) => `
query ($space: String!, $address: String!, $first: Int!, $skip: Int!, $blockNumber: Int) {
${key}: delegations(${getBlockNumberFilter(blockNumber)}
${key}: delegations(
${getBlockNumberFilter(blockNumber)}
where: { space_in: ["", $space], ${getDelegationType(key)}: $address },
first: $first, skip: $skip, orderBy: timestamp, orderDirection: desc) {
delegator
Expand Down
13 changes: 2 additions & 11 deletions src/entities/Snapshot/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
import logger from '../../utils/logger'

import { SNAPSHOT_SPACE } from './constants'
import { getDelegatedQuery } from './queries'

export type Match = {
proposal_id: string
Expand Down Expand Up @@ -165,16 +164,8 @@ export async function getDelegations(
}
const variables = getDelegatesVariables(address, blockNumber)
try {
const delegatedTo = await SnapshotSubgraph.get().getDelegates(
'delegatedTo',
getDelegatedQuery('delegatedTo', blockNumber),
variables
)
const delegatedFrom = await SnapshotSubgraph.get().getDelegates(
'delegatedFrom',
getDelegatedQuery('delegatedFrom', blockNumber),
variables
)
const delegatedTo = await SnapshotSubgraph.get().getDelegates('delegatedTo', variables)
const delegatedFrom = await SnapshotSubgraph.get().getDelegates('delegatedFrom', variables)

if (!delegatedTo && !delegatedFrom) {
return EMPTY_DELEGATION
Expand Down
1 change: 1 addition & 0 deletions src/services/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export class EventsService {
created_at: new Date(),
}
await EventModel.create(votedEvent)
NotificationService.newVote(proposal_id, address)
} catch (error) {
this.reportEventError(error as Error, EventType.Voted, { address, proposal_id, proposal_title, choice })
}
Expand Down
128 changes: 128 additions & 0 deletions src/services/notification.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ChainId } from '@dcl/schemas/dist/dapps/chain-id'
import { ethers } from 'ethers'

import { SnapshotSubgraph } from '../clients/SnapshotSubgraph'
import { DCL_NOTIFICATIONS_SERVICE_ENABLED, NOTIFICATIONS_SERVICE_ENABLED, PUSH_CHANNEL_ID } from '../constants'
import ProposalModel from '../entities/Proposal/model'
import { ProposalWithOutcome } from '../entities/Proposal/outcome'
import { ProposalAttributes, ProposalStatus, ProposalType } from '../entities/Proposal/types'
import { proposalUrl } from '../entities/Proposal/utils'
import { isSameAddress } from '../entities/Snapshot/utils'
import { getUpdateUrl } from '../entities/Updates/utils'
import { inBackground } from '../helpers'
import { ErrorService } from '../services/ErrorService'
Expand All @@ -17,6 +19,8 @@ import logger from '../utils/logger'
import { NotificationType, Notifications, getCaipAddress, getPushNotificationsEnv } from '../utils/notifications'
import { areValidAddresses } from '../utils/validations'

import { ProposalService } from './ProposalService'
import { SnapshotService } from './SnapshotService'
import { CoauthorService } from './coauthor'
import { DiscordService } from './discord'
import { VoteService } from './vote'
Expand Down Expand Up @@ -605,4 +609,128 @@ export class NotificationService {
}
})
}

static async newVote(proposalId: ProposalAttributes['id'], voterAddress: string) {
inBackground(async () => {
const proposal = await ProposalService.getProposal(proposalId)
const votes = await SnapshotService.getVotesByProposal(proposal.snapshot_id)
const addressVote = votes.find((vote) => isSameAddress(vote.voter, voterAddress))

if ((addressVote?.vp || 0) >= Number(process.env.WHALE_VOTE_THRESHOLD)) {
this.whaleVote(proposal)
}

const delegators = await SnapshotSubgraph.get().getDelegates('delegatedFrom', {
address: voterAddress,
space: proposal.snapshot_space,
blockNumber: proposal.snapshot_proposal.snapshot,
})
if (delegators.length === 0) {
return
}

const votesAddresses = votes.map((vote) => vote.voter)
const delegatorsWhoVoted = votesAddresses.filter((vote) =>
delegators.some((delegator) => delegator.delegator === vote)
)
if (delegatorsWhoVoted.length > 0) {
this.votedOnBehalf(proposal, delegatorsWhoVoted)
}
})
}

private static whaleVote(proposal: ProposalAttributes) {
inBackground(async () => {
try {
const addresses = await this.getAuthorAndCoauthors(proposal)
const title = Notifications.WhaleVote.title(proposal)
const body = Notifications.WhaleVote.body

DiscordService.sendDirectMessages(addresses, {
title,
action: body,
url: proposalUrl(proposal.id),
fields: [],
})

const dclNotifications = addresses.map((address) => ({
type: 'governance_whale_vote',
address,
eventKey: proposal.id,
metadata: {
proposalId: proposal.id,
proposalTitle: proposal.title,
title,
description: body,
link: proposalUrl(proposal.id),
},
timestamp: Date.now(),
}))

await Promise.all([
this.sendPushNotification({
title,
body,
recipient: addresses,
url: proposalUrl(proposal.id),
customType: NotificationCustomType.WhaleVote,
}),
this.sendDCLNotifications(dclNotifications),
])
} catch (error) {
ErrorService.report('Error sending notifications for whale vote', {
error: `${error}`,
category: ErrorCategory.Notifications,
proposal_id: proposal.id,
})
}
})
}

private static votedOnBehalf(proposal: ProposalAttributes, addresses: string[]) {
inBackground(async () => {
try {
const title = Notifications.VotedOnYourBehalf.title(proposal)
const body = Notifications.VotedOnYourBehalf.body

DiscordService.sendDirectMessages(addresses, {
title,
action: body,
url: proposalUrl(proposal.id),
fields: [],
})

const dclNotifications = addresses.map((address) => ({
type: 'governance_voted_on_behalf',
address,
eventKey: proposal.id,
metadata: {
proposalId: proposal.id,
proposalTitle: proposal.title,
title,
description: body,
link: proposalUrl(proposal.id),
},
timestamp: Date.now(),
}))

await Promise.all([
this.sendPushNotification({
title,
body,
recipient: addresses,
url: proposalUrl(proposal.id),
customType: NotificationCustomType.VotedOnBehalf,
}),
this.sendDCLNotifications(dclNotifications),
])
} catch (error) {
ErrorService.report('Error sending notifications for delegated vote', {
error: `${error}`,
category: ErrorCategory.Notifications,
proposal_id: proposal.id,
})
}
})
}
}
2 changes: 2 additions & 0 deletions src/shared/types/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export enum NotificationCustomType {
Grant = 'grant',
TenderPassed = 'tender_passed',
PitchPassed = 'pitch_passed',
WhaleVote = 'whale_vote',
VotedOnBehalf = 'voted_on_behalf',
}

export type Notification = {
Expand Down
8 changes: 8 additions & 0 deletions src/utils/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,12 @@ export const Notifications = {
title: (proposal: ProposalAttributes) => `The Tender "${proposal.title}" can now receive Bid Projects`,
body: 'If think you can tackle this solution, propose a Project and get funding from the DAO',
},
WhaleVote: {
title: (proposal: ProposalAttributes) => `A whale voted on your proposal "${proposal.title}"`,
body: 'A wallet holding over 250k VP has just cast a vote. Stay informed and see how this significant vote impacts the outcome.',
},
VotedOnYourBehalf: {
title: (proposal: ProposalAttributes) => `Your delegate voted on the proposal "${proposal.title}"`,
body: 'See if their vote is aligned with your vision. You can always override their decision by voting on your own.',
},
}
104 changes: 104 additions & 0 deletions strategy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
{
"symbol": "VP (delegated)",
"strategies": [
{
"name": "erc20-balance-of",
"params": {
"symbol": "WMANA",
"address": "0xfd09cf7cfffa9932e33668311c4777cb9db3c9be",
"decimals": 18
}
},
{
"name": "erc721-with-multiplier",
"params": {
"symbol": "LAND",
"address": "0xf87e31492faf9a91b02ee0deaad50d51d56d5d4d",
"multiplier": 2000
}
},
{
"name": "decentraland-estate-size",
"params": {
"symbol": "ESTATE",
"address": "0x959e104e1a4db6317fa58f8295f586e1a978c297",
"multiplier": 2000
}
},
{
"name": "multichain",
"params": {
"name": "multichain",
"graphs": {
"137": "https://subgraph.decentraland.org/blocks-matic-mainnet"
},
"symbol": "MANA",
"strategies": [
{
"name": "erc20-balance-of",
"params": {
"address": "0x0f5d2fb29fb7d3cfee444a200298f468908cc942",
"decimals": 18
},
"network": "1"
},
{
"name": "erc20-balance-of",
"params": {
"address": "0xA1c57f48F0Deb89f569dFbE6E2B7f46D33606fD4",
"decimals": 18
},
"network": "137"
}
]
}
},
{
"name": "erc721-with-multiplier",
"params": {
"symbol": "NAMES",
"address": "0x2a187453064356c898cae034eaed119e1663acb8",
"multiplier": 100
}
},
{
"name": "decentraland-wearable-rarity",
"params": {
"symbol": "WEARABLE",
"collections": [
"0x32b7495895264ac9d0b12d32afd435453458b1c6",
"0xd35147be6401dcb20811f2104c33de8e97ed6818",
"0xc04528c14c8ffd84c7c1fb6719b4a89853035cdd",
"0xc1f4b0eea2bd6690930e6c66efd3e197d620b9c2",
"0xf64dc33a192e056bb5f0e5049356a0498b502d50",
"0xc3af02c0fd486c8e9da5788b915d6fff3f049866"
],
"multipliers": {
"epic": 10,
"rare": 5,
"mythic": 1000,
"uncommon": 1,
"legendary": 100
}
}
},
{
"name": "decentraland-rental-lessors",
"params": {
"symbol": "RENTAL",
"addresses": {
"land": "0xf87e31492faf9a91b02ee0deaad50d51d56d5d4d",
"estate": "0x959e104e1a4db6317fa58f8295f586e1a978c297"
},
"subgraphs": {
"rentals": "https://subgraph.decentraland.org/rentals-ethereum-mainnet",
"marketplace": "https://subgraph.decentraland.org/marketplace"
},
"multipliers": {
"land": 2000,
"estateSize": 2000
}
}
}
]
}

0 comments on commit a1dc8e1

Please sign in to comment.