diff --git a/package-lock.json b/package-lock.json index d151edc30..70398997c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,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", @@ -6591,21 +6591,22 @@ } }, "node_modules/@snapshot-labs/snapshot.js": { - "version": "0.4.52", - "resolved": "https://registry.npmjs.org/@snapshot-labs/snapshot.js/-/snapshot.js-0.4.52.tgz", - "integrity": "sha512-gm9tQL1FtpjSXf0jGVQgaf31KBMEjUe0ZQPzttu9GvRKCwBAEQosu7T3lcQvUpgIXz87aRAMzFQHouqoMg9rOw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@snapshot-labs/snapshot.js/-/snapshot.js-0.5.5.tgz", + "integrity": "sha512-JNBGdqim9+IriPRv0R8vhLe3ojVv7i2R1qrx6PxQCbdHwiBr7d7Npei+h8VpnzOKYcVzPAkttOedxDvxxR2wnA==", "dependencies": { "@ensdomains/eth-ens-namehash": "^2.0.15", "@ethersproject/abi": "^5.6.4", "@ethersproject/address": "^5.6.1", "@ethersproject/bytes": "^5.6.1", "@ethersproject/contracts": "^5.6.2", - "@ethersproject/hash": "^5.6.1", + "@ethersproject/hash": "^5.7.0", "@ethersproject/providers": "^5.6.8", + "@ethersproject/units": "^5.7.0", "@ethersproject/wallet": "^5.6.2", "ajv": "^8.11.0", "ajv-formats": "^2.1.1", - "cross-fetch": "^3.1.5", + "cross-fetch": "^3.1.6", "json-to-graphql-query": "^2.2.4", "lodash.set": "^4.3.2" }, @@ -14160,11 +14161,30 @@ } }, "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { - "node-fetch": "2.6.7" + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/cross-spawn": { @@ -48428,21 +48448,22 @@ } }, "@snapshot-labs/snapshot.js": { - "version": "0.4.52", - "resolved": "https://registry.npmjs.org/@snapshot-labs/snapshot.js/-/snapshot.js-0.4.52.tgz", - "integrity": "sha512-gm9tQL1FtpjSXf0jGVQgaf31KBMEjUe0ZQPzttu9GvRKCwBAEQosu7T3lcQvUpgIXz87aRAMzFQHouqoMg9rOw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@snapshot-labs/snapshot.js/-/snapshot.js-0.5.5.tgz", + "integrity": "sha512-JNBGdqim9+IriPRv0R8vhLe3ojVv7i2R1qrx6PxQCbdHwiBr7d7Npei+h8VpnzOKYcVzPAkttOedxDvxxR2wnA==", "requires": { "@ensdomains/eth-ens-namehash": "^2.0.15", "@ethersproject/abi": "^5.6.4", "@ethersproject/address": "^5.6.1", "@ethersproject/bytes": "^5.6.1", "@ethersproject/contracts": "^5.6.2", - "@ethersproject/hash": "^5.6.1", + "@ethersproject/hash": "^5.7.0", "@ethersproject/providers": "^5.6.8", + "@ethersproject/units": "^5.7.0", "@ethersproject/wallet": "^5.6.2", "ajv": "^8.11.0", "ajv-formats": "^2.1.1", - "cross-fetch": "^3.1.5", + "cross-fetch": "^3.1.6", "json-to-graphql-query": "^2.2.4", "lodash.set": "^4.3.2" }, @@ -54499,11 +54520,21 @@ } }, "cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", "requires": { - "node-fetch": "2.6.7" + "node-fetch": "^2.6.12" + }, + "dependencies": { + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + } + } } }, "cross-spawn": { diff --git a/package.json b/package.json index 0ba770d70..967d517ac 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/back/routes/snapshot.ts b/src/back/routes/snapshot.ts index 25b2d1297..af26020db 100644 --- a/src/back/routes/snapshot.ts +++ b/src/back/routes/snapshot.ts @@ -15,6 +15,7 @@ export default routes((router) => { 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)) }) async function getStatusAndSpace(req: Request<{ spaceName?: string }>) { @@ -65,3 +66,12 @@ 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) +} diff --git a/src/clients/Governance.ts b/src/clients/Governance.ts index 700775f1f..f9338f22a 100644 --- a/src/clients/Governance.ts +++ b/src/clients/Governance.ts @@ -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 = { @@ -558,6 +565,14 @@ export class Governance extends API { return response.data } + async getScores(addresses: string[]) { + const response = await this.fetch>( + '/snapshot/scores', + this.options().method('POST').json({ addresses }) + ) + return response.data + } + async getVestingContractData(addresses: string[]) { const response = await this.fetch>( `/vesting`, diff --git a/src/clients/SnapshotApi.ts b/src/clients/SnapshotApi.ts index 49c10cdc5..eb483e0b4 100644 --- a/src/clients/SnapshotApi.ts +++ b/src/clients/SnapshotApi.ts @@ -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 @@ -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) } diff --git a/src/entities/Votes/utils.ts b/src/entities/Votes/utils.ts index db03da881..da1c2a8ab 100644 --- a/src/entities/Votes/utils.ts +++ b/src/entities/Votes/utils.ts @@ -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 -const DELEGATION_STRATEGY_NAME = 'delegation' export function toProposalIds(ids?: undefined | null | string | string[]) { if (!ids) { @@ -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 -} diff --git a/src/hooks/useProposalOutcome.ts b/src/hooks/useProposalOutcome.ts index b1e7aad36..df1e318b1 100644 --- a/src/hooks/useProposalOutcome.ts +++ b/src/hooks/useProposalOutcome.ts @@ -14,6 +14,7 @@ const useProposalOutcome = (snapshotId: ProposalAttributes['snapshot_id'], choic return null } + // TODO: Move this to backend scores/proposal/:snapshotId return SnapshotGraphql.get().getProposalScores(snapshotId) }, staleTime: FIVE_MINUTES_MS, diff --git a/src/hooks/useVotingPowerBalanceList.ts b/src/hooks/useVotingPowerBalanceList.ts index 902b3fedc..2ebe9b7b7 100644 --- a/src/hooks/useVotingPowerBalanceList.ts +++ b/src/hooks/useVotingPowerBalanceList.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' -import { getScores } from '../entities/Votes/utils' +import { Governance } from '../clients/Governance' import { DEFAULT_QUERY_STALE_TIME } from './constants' @@ -9,7 +9,7 @@ export default function useVotingPowerBalanceList(addresses: string[]) { queryKey: [`votingPower#${JSON.stringify(addresses)}`], queryFn: async () => { if (addresses.length < 1) return {} - return await getScores(addresses) + return await Governance.get().getScores(addresses) }, staleTime: DEFAULT_QUERY_STALE_TIME, }) diff --git a/src/services/SnapshotService.ts b/src/services/SnapshotService.ts index b152eec80..1f565682c 100644 --- a/src/services/SnapshotService.ts +++ b/src/services/SnapshotService.ts @@ -3,16 +3,19 @@ import isNumber from 'lodash/isNumber' import { SnapshotApi, SnapshotReceipt } from '../clients/SnapshotApi' import { SnapshotGraphql } from '../clients/SnapshotGraphql' -import { SnapshotProposal, SnapshotVote, VpDistribution } from '../clients/SnapshotGraphqlTypes' +import { DetailedScores, SnapshotProposal, SnapshotVote, VpDistribution } from '../clients/SnapshotGraphqlTypes' import * as templates from '../entities/Proposal/templates' import { proposalUrl, snapshotProposalUrl } from '../entities/Proposal/utils' import { SNAPSHOT_SPACE } from '../entities/Snapshot/constants' +import { isSameAddress } from '../entities/Snapshot/utils' import { inBackground } from '../helpers' import { Avatar } from '../utils/Catalyst/types' import { ProposalInCreation, ProposalLifespan } from './ProposalService' import RpcService from './RpcService' +const DELEGATION_STRATEGY_NAME = 'delegation' + export class SnapshotService { static async createProposal( proposalInCreation: ProposalInCreation, @@ -123,4 +126,34 @@ export class SnapshotService { static async getVpDistribution(address: string, proposalSnapshotId?: string): Promise { return await SnapshotGraphql.get().getVpDistribution(address, proposalSnapshotId) } + + static async getScores(addresses: string[]) { + const formattedAddresses = addresses.map((addr) => addr.toLowerCase()) + const { scores, strategies } = await SnapshotApi.get().getScores(formattedAddresses) + + 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 + } }