Skip to content

Commit

Permalink
feat: use snapshot api key in all snapshot requests (#1211)
Browse files Browse the repository at this point in the history
* feat: move scores calculation to backend

* fix: scores not working on sepolia

* refactor: move getProposalScores to backend
  • Loading branch information
andyesp authored and 1emu committed Sep 11, 2023
1 parent 1bbad18 commit d18f764
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 101 deletions.
69 changes: 50 additions & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"@dcl/ui-env": "1.2.1",
"@jparnaudo/react-crypto-icons": "^1.0.5",
"@otterspace-xyz/contracts": "^2.7.3",
"@snapshot-labs/snapshot.js": "0.4.52",
"@snapshot-labs/snapshot.js": "0.5.5",
"@tanstack/react-query": "^4.29.7",
"autoprefixer": "^10.4.4",
"chart.js": "^3.8.2",
Expand Down
32 changes: 24 additions & 8 deletions src/back/routes/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ import { Request } from 'express'

import { SnapshotVote } from '../../clients/SnapshotGraphqlTypes'
import { SnapshotService } from '../../services/SnapshotService'
import { validateAddress, validateDates, validateFields } from '../utils/validations'
import { validateAddress, validateDates, validateFields, validateProposalSnapshotId } from '../utils/validations'

export default routes((router) => {
router.get('/snapshot/status-space/:spaceName', handleAPI(getStatusAndSpace))
router.post('/snapshot/votes', handleAPI(getAddressesVotes))
router.get('/snapshot/votes/:id', handleAPI(getProposalVotes))
router.get('/snapshot/votes/:proposalSnapshotId', handleAPI(getProposalVotes))
router.post('/snapshot/votes/all', handleAPI(getAllVotesBetweenDates))
router.post('/snapshot/proposals', handleAPI(getProposals))
router.post('/snapshot/proposals/pending', handleAPI(getPendingProposals))
router.get('/snapshot/vp-distribution/:address/:proposalSnapshotId?', handleAPI(getVpDistribution))
router.post('/snapshot/scores', handleAPI(getScores))
router.get('/snapshot/proposal-scores/:proposalSnapshotId', handleAPI(getProposalScores))
})

async function getStatusAndSpace(req: Request<{ spaceName?: string }>) {
Expand All @@ -27,13 +29,11 @@ async function getAddressesVotes(req: Request) {
return await SnapshotService.getAddressesVotes(addresses)
}

async function getProposalVotes(req: Request<{ id?: string }>) {
const { id } = req.params
if (!id || id.length === 0) {
throw new RequestError('Invalid snapshot id')
}
async function getProposalVotes(req: Request<{ proposalSnapshotId?: string }>) {
const { proposalSnapshotId } = req.params
validateProposalSnapshotId(proposalSnapshotId)

return await SnapshotService.getProposalVotes(id!)
return await SnapshotService.getProposalVotes(proposalSnapshotId!)
}

async function getAllVotesBetweenDates(req: Request): Promise<SnapshotVote[]> {
Expand Down Expand Up @@ -65,3 +65,19 @@ async function getVpDistribution(req: Request<{ address: string; proposalSnapsho

return await SnapshotService.getVpDistribution(address, proposalSnapshotId)
}

async function getScores(req: Request) {
const addresses = req.body.addresses
if (!addresses || addresses.length === 0) {
throw new RequestError('Addresses missing', RequestError.BadRequest)
}

return await SnapshotService.getScores(addresses)
}

async function getProposalScores(req: Request<{ proposalSnapshotId?: string }>) {
const { proposalSnapshotId } = req.params
validateProposalSnapshotId(proposalSnapshotId)

return await SnapshotService.getProposalScores(proposalSnapshotId!)
}
6 changes: 6 additions & 0 deletions src/back/utils/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,9 @@ export function validateUniqueAddresses(addresses: string[]): boolean {

return uniqueSet.size === addresses.length
}

export function validateProposalSnapshotId(proposalSnapshotId?: string) {
if (!proposalSnapshotId || proposalSnapshotId.length === 0) {
throw new RequestError('Invalid snapshot id')
}
}
25 changes: 24 additions & 1 deletion src/clients/Governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ import { Vote, VotedProposal } from '../entities/Votes/types'
import Time from '../utils/date/Time'

import { TransparencyBudget } from './DclData'
import { SnapshotProposal, SnapshotSpace, SnapshotStatus, SnapshotVote, VpDistribution } from './SnapshotGraphqlTypes'
import {
DetailedScores,
SnapshotProposal,
SnapshotSpace,
SnapshotStatus,
SnapshotVote,
VpDistribution,
} from './SnapshotGraphqlTypes'
import { VestingInfo } from './VestingData'

type NewProposalMap = {
Expand Down Expand Up @@ -551,13 +558,29 @@ export class Governance extends API {
return response.data
}

async getProposalScores(proposalSnapshotId: string) {
const response = await this.fetch<ApiResponse<number[]>>(
`/snapshot/proposal-scores/${proposalSnapshotId}`,
this.options().method('GET')
)
return response.data
}

async getVpDistribution(address: string, proposalSnapshotId?: string) {
const snapshotId = proposalSnapshotId ? `/${proposalSnapshotId}` : ''
const url = `/snapshot/vp-distribution/${address}${snapshotId}`
const response = await this.fetch<ApiResponse<VpDistribution>>(url, this.options().method('GET'))
return response.data
}

async getScores(addresses: string[]) {
const response = await this.fetch<ApiResponse<DetailedScores>>(
'/snapshot/scores',
this.options().method('POST').json({ addresses })
)
return response.data
}

async getVestingContractData(addresses: string[]) {
const response = await this.fetch<ApiResponse<VestingInfo[]>>(
`/vesting`,
Expand Down
39 changes: 23 additions & 16 deletions src/clients/SnapshotApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import { CancelProposal, ProposalType, Vote } from '@snapshot-labs/snapshot.js/d
import logger from 'decentraland-gatsby/dist/entities/Development/logger'
import env from 'decentraland-gatsby/dist/utils/env'

import { SNAPSHOT_ADDRESS, SNAPSHOT_PRIVATE_KEY, SNAPSHOT_SPACE } from '../entities/Snapshot/constants'
import {
SNAPSHOT_ADDRESS,
SNAPSHOT_API_KEY,
SNAPSHOT_PRIVATE_KEY,
SNAPSHOT_SPACE,
} from '../entities/Snapshot/constants'
import { getChecksumAddress } from '../entities/Snapshot/utils'
import { ProposalInCreation, ProposalLifespan } from '../services/ProposalService'
import Time from '../utils/date/Time'
import { getEnvironmentChainId } from '../utils/votes/utils'

import { SnapshotGraphql } from './SnapshotGraphql'
import { SnapshotStrategy } from './SnapshotGraphqlTypes'
import { trimLastForwardSlash } from './utils'

const SNAPSHOT_PROPOSAL_TYPE: ProposalType = 'single-choice' // Each voter may select only one choice
Expand Down Expand Up @@ -150,29 +154,32 @@ export class SnapshotApi {
return (await this.client.vote(account, address, voteMessage)) as SnapshotReceipt
}

async getScores(
addresses: string[],
blockNumber?: number | string,
space?: string,
networkId?: string,
proposalStrategies?: SnapshotStrategy[]
) {
async getScores(addresses: string[]) {
const formattedAddresses = addresses.map((address) => getChecksumAddress(address))
const network = networkId && networkId.length > 0 ? networkId : getEnvironmentChainId().toString()
const spaceName = space && space.length > 0 ? space : SnapshotApi.getSpaceName()
const strategies = proposalStrategies || (await SnapshotGraphql.get().getSpace(spaceName)).strategies
const spaceName = SnapshotApi.getSpaceName()
const network = getEnvironmentChainId().toString()
const strategies = (await SnapshotGraphql.get().getSpace(spaceName)).strategies
const scoreApiUrl = `https://score.snapshot.org/?apiKey=${SNAPSHOT_API_KEY}`

try {
const scores = await snapshot.utils.getScores(spaceName, strategies, network, formattedAddresses, blockNumber)
const scores = await snapshot.utils.getScores(
spaceName,
strategies,
network,
formattedAddresses,
undefined,
scoreApiUrl
)

return {
scores: scores,
strategies: strategies,
scores,
strategies,
}
} catch (e) {
logger.log(
`Space: ${spaceName}, Strategies: ${JSON.stringify(
strategies
)}, Network: ${network}, Addresses: ${formattedAddresses}, Block: ${blockNumber}`
)}, Network: ${network}, Addresses: ${formattedAddresses}`
)
throw new Error('Error fetching proposal scores', e as Error)
}
Expand Down
1 change: 1 addition & 0 deletions src/clients/SnapshotGraphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class SnapshotGraphql extends API {
strategies {
name
params
network
}
}
}
Expand Down
46 changes: 1 addition & 45 deletions src/entities/Votes/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import isUUID from 'validator/lib/isUUID'

import { SnapshotApi } from '../../clients/SnapshotApi'
import { DetailedScores, SnapshotStrategy, SnapshotVote } from '../../clients/SnapshotGraphqlTypes'
import { isSameAddress } from '../Snapshot/utils'
import { SnapshotVote } from '../../clients/SnapshotGraphqlTypes'

import { ChoiceColor, Vote } from './types'

export type Scores = Record<string, number>
const DELEGATION_STRATEGY_NAME = 'delegation'

export function toProposalIds(ids?: undefined | null | string | string[]) {
if (!ids) {
Expand Down Expand Up @@ -166,44 +163,3 @@ export function abbreviateNumber(vp: number) {
function getFloorOrZero(number?: number) {
return Math.floor(number || 0)
}

export async function getScores(
addresses: string[],
block?: string | number,
space?: string,
networkId?: string,
proposalStrategies?: SnapshotStrategy[]
) {
const formattedAddresses = addresses.map((addr) => addr.toLowerCase())
const { scores, strategies } = await SnapshotApi.get().getScores(
formattedAddresses,
block,
space,
networkId,
proposalStrategies
)

const result: DetailedScores = {}
const delegationScores = scores[strategies.findIndex((s) => s.name === DELEGATION_STRATEGY_NAME)] || {}
for (const addr of formattedAddresses) {
result[addr] = {
ownVp: 0,
delegatedVp:
Math.round(delegationScores[Object.keys(delegationScores).find((key) => isSameAddress(key, addr)) || '']) || 0,
totalVp: 0,
}
}

for (const score of scores) {
for (const addr of Object.keys(score)) {
const address = addr.toLowerCase()
result[address].totalVp = (result[address].totalVp || 0) + Math.floor(score[addr] || 0)
}
}

for (const address of Object.keys(result)) {
result[address].ownVp = result[address].totalVp - result[address].delegatedVp
}

return result
}
Loading

0 comments on commit d18f764

Please sign in to comment.