Skip to content
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: add whale vote and voted on behalf notifications #1889

Merged
merged 3 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: 0 additions & 1 deletion src/routes/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ async function getProject(req: Request<{ project: string }>) {
try {
return await ProjectService.getUpdatedProject(id)
} catch (e) {
console.log(`Error getting project: ${e}`) //TODO: remove before merging projects to main
throw new RequestError(`Project "${id}" not found`, RequestError.NotFound)
}
}
Expand Down
1 change: 1 addition & 0 deletions src/services/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,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
}
}
}
]
}
Loading