diff --git a/gatsby-browser.js b/gatsby-browser.js index 9f7e8f5a9..017d247dd 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -31,6 +31,7 @@ import Segment from "decentraland-gatsby/dist/components/Development/Segment" import { SEGMENT_KEY, SSO_URL } from "./src/constants" import { flattenMessages } from "./src/utils/intl" import en from "./src/intl/en.json" +import SnapshotStatus from "./src/components/Debug/SnapshotStatus" const queryClient = new QueryClient() @@ -49,6 +50,7 @@ export const wrapPageElement = ({ element, props }) => { + }> {element} diff --git a/package-lock.json b/package-lock.json index e659e0ccc..83c3febfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "keccak": "^3.0.1", "lodash": "^4.17.21", "nft.storage": "^7.1.1", + "node-cache": "^5.1.2", "node-pg-migrate": "^6.2.1", "numeral": "^2.0.6", "patch-package": "^6.4.7", @@ -38010,6 +38011,17 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", @@ -80284,6 +80296,14 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" }, + "node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "requires": { + "clone": "2.x" + } + }, "node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", diff --git a/package.json b/package.json index 23b0f9c51..edb7ed1d6 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "keccak": "^3.0.1", "lodash": "^4.17.21", "nft.storage": "^7.1.1", + "node-cache": "^5.1.2", "node-pg-migrate": "^6.2.1", "numeral": "^2.0.6", "patch-package": "^6.4.7", diff --git a/src/back/jobs/PingSnapshot.ts b/src/back/jobs/PingSnapshot.ts new file mode 100644 index 000000000..bc3d81d43 --- /dev/null +++ b/src/back/jobs/PingSnapshot.ts @@ -0,0 +1,5 @@ +import { SnapshotService } from '../../services/SnapshotService' + +export async function pingSnapshot() { + await SnapshotService.ping() +} diff --git a/src/back/routes/debug.ts b/src/back/routes/debug.ts index 75ec36443..b8bd91959 100644 --- a/src/back/routes/debug.ts +++ b/src/back/routes/debug.ts @@ -2,7 +2,7 @@ import { WithAuth, auth } from 'decentraland-gatsby/dist/entities/Auth/middlewar import handleAPI from 'decentraland-gatsby/dist/entities/Route/handle' import routes from 'decentraland-gatsby/dist/entities/Route/routes' -import { DEBUG_ADDRESSES } from '../../entities/Debug/isDebugAddress' +import { DEBUG_ADDRESSES } from '../../constants' import { ErrorService } from '../../services/ErrorService' import { giveAndRevokeLandOwnerBadges, giveTopVoterBadges, runQueuedAirdropJobs } from '../jobs/BadgeAirdrop' import { validateDebugAddress } from '../utils/validations' diff --git a/src/back/routes/snapshot.ts b/src/back/routes/snapshot.ts index c0c8410de..71d1ea0b6 100644 --- a/src/back/routes/snapshot.ts +++ b/src/back/routes/snapshot.ts @@ -3,12 +3,13 @@ import handleAPI from 'decentraland-gatsby/dist/entities/Route/handle' import routes from 'decentraland-gatsby/dist/entities/Route/routes' import { Request } from 'express' -import { SnapshotVote } from '../../clients/SnapshotGraphqlTypes' +import { SnapshotVote } from '../../clients/SnapshotTypes' import { SnapshotService } from '../../services/SnapshotService' import { validateAddress, validateDates, validateFields, validateProposalSnapshotId } from '../utils/validations' export default routes((router) => { - router.get('/snapshot/status-space/:spaceName', handleAPI(getStatusAndSpace)) + router.get('/snapshot/status', handleAPI(getStatus)) + router.get('/snapshot/config/:spaceName', handleAPI(getConfig)) router.post('/snapshot/votes', handleAPI(getAddressesVotes)) router.get('/snapshot/votes/:proposalSnapshotId', handleAPI(getProposalVotes)) router.post('/snapshot/votes/all', handleAPI(getAllVotesBetweenDates)) @@ -19,9 +20,13 @@ export default routes((router) => { router.get('/snapshot/proposal-scores/:proposalSnapshotId', handleAPI(getProposalScores)) }) -async function getStatusAndSpace(req: Request<{ spaceName?: string }>) { +async function getStatus(req: Request) { + return await SnapshotService.getStatus() +} + +async function getConfig(req: Request<{ spaceName?: string }>) { const { spaceName } = req.params - return await SnapshotService.getStatusAndSpace(spaceName) + return await SnapshotService.getConfig(spaceName) } async function getAddressesVotes(req: Request) { diff --git a/src/back/routes/votes.ts b/src/back/routes/votes.ts index acf1c9546..cde9e1971 100644 --- a/src/back/routes/votes.ts +++ b/src/back/routes/votes.ts @@ -4,7 +4,7 @@ import { Request } from 'express' import isNumber from 'lodash/isNumber' import { SnapshotGraphql } from '../../clients/SnapshotGraphql' -import { SnapshotVote } from '../../clients/SnapshotGraphqlTypes' +import { SnapshotVote } from '../../clients/SnapshotTypes' import ProposalModel from '../../entities/Proposal/model' import { ProposalAttributes } from '../../entities/Proposal/types' import VotesModel from '../../entities/Votes/model' diff --git a/src/back/services/vote.ts b/src/back/services/vote.ts index b1ead6162..f786e49a9 100644 --- a/src/back/services/vote.ts +++ b/src/back/services/vote.ts @@ -1,4 +1,4 @@ -import { SnapshotVote } from '../../clients/SnapshotGraphqlTypes' +import { SnapshotVote } from '../../clients/SnapshotTypes' import { VOTES_VP_THRESHOLD } from '../../constants' import VoteModel from '../../entities/Votes/model' import { VoteCount, Voter } from '../../entities/Votes/types' diff --git a/src/back/utils/validations.ts b/src/back/utils/validations.ts index fd019499e..1aa6eb4d1 100644 --- a/src/back/utils/validations.ts +++ b/src/back/utils/validations.ts @@ -2,7 +2,7 @@ import RequestError from 'decentraland-gatsby/dist/entities/Route/error' import isEthereumAddress from 'validator/lib/isEthereumAddress' import isUUID from 'validator/lib/isUUID' -import { SnapshotProposal } from '../../clients/SnapshotGraphqlTypes' +import { SnapshotProposal } from '../../clients/SnapshotTypes' import isDebugAddress from '../../entities/Debug/isDebugAddress' export function validateDates(start?: string, end?: string) { diff --git a/src/clients/Governance.ts b/src/clients/Governance.ts index d20f0aef8..43e7191b6 100644 --- a/src/clients/Governance.ts +++ b/src/clients/Governance.ts @@ -40,12 +40,13 @@ import Time from '../utils/date/Time' import { TransparencyBudget } from './DclData' import { DetailedScores, + SnapshotConfig, SnapshotProposal, SnapshotSpace, SnapshotStatus, SnapshotVote, VpDistribution, -} from './SnapshotGraphqlTypes' +} from './SnapshotTypes' import { VestingInfo } from './VestingData' type NewProposalMap = { @@ -527,14 +528,19 @@ export class Governance extends API { return response.data } - async getSnapshotStatusAndSpace(spaceName?: string) { - const response = await this.fetch>( - `/snapshot/status-space/${spaceName}`, + async getSnapshotConfigAndSpace(spaceName?: string) { + const response = await this.fetch>( + `/snapshot/config/${spaceName}`, this.options().method('GET') ) return response.data } + async getSnapshotStatus() { + const response = await this.fetch>(`/snapshot/status`, this.options().method('GET')) + return response.data + } + async getAddressesVotes(addresses: string[]) { const result = await this.fetch>( `/snapshot/votes/`, diff --git a/src/clients/SnapshotApi.ts b/src/clients/SnapshotApi.ts index eb483e0b4..400e82abd 100644 --- a/src/clients/SnapshotApi.ts +++ b/src/clients/SnapshotApi.ts @@ -6,6 +6,7 @@ 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 { DEBUG_ADDRESSES } from '../constants' import { SNAPSHOT_ADDRESS, SNAPSHOT_API_KEY, @@ -155,11 +156,9 @@ export class SnapshotApi { } async getScores(addresses: string[]) { - const formattedAddresses = addresses.map((address) => getChecksumAddress(address)) - 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}` + const { formattedAddresses, spaceName, network, strategies, scoreApiUrl } = await this.prepareScoresQueryArgs( + addresses + ) try { const scores = await snapshot.utils.getScores( @@ -188,4 +187,29 @@ export class SnapshotApi { private static toSnapshotTimestamp(time: number) { return Number(time.toString().slice(0, -3)) } + + async ping(addressesSample: string[]) { + const addresses = addressesSample.length === 0 ? DEBUG_ADDRESSES : addressesSample + try { + const { formattedAddresses, spaceName, network, strategies, scoreApiUrl } = await this.prepareScoresQueryArgs( + addresses + ) + const now = new Date() + const startTime = now.getTime() + await snapshot.utils.getScores(spaceName, strategies, network, formattedAddresses, undefined, scoreApiUrl) + const endTime = new Date().getTime() + return endTime - startTime + } catch (error) { + return -1 // Return -1 to indicate API failures + } + } + + private async prepareScoresQueryArgs(addresses: string[]) { + const formattedAddresses = addresses.map((address) => getChecksumAddress(address)) + 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}` + return { formattedAddresses, spaceName, network, strategies, scoreApiUrl } + } } diff --git a/src/clients/SnapshotGraphql.ts b/src/clients/SnapshotGraphql.ts index ef6a384be..5454560a1 100644 --- a/src/clients/SnapshotGraphql.ts +++ b/src/clients/SnapshotGraphql.ts @@ -3,10 +3,12 @@ import env from 'decentraland-gatsby/dist/utils/env' import uniqBy from 'lodash/uniqBy' import { SNAPSHOT_API, SNAPSHOT_API_KEY, SNAPSHOT_SPACE } from '../entities/Snapshot/constants' +import { getAMonthAgo } from '../utils/date/aMonthAgo' import { ErrorCategory } from '../utils/errorCategories' import { ErrorClient } from './ErrorClient' import { + SnapshotConfig, SnapshotProposal, SnapshotProposalContent, SnapshotProposalResponse, @@ -14,20 +16,33 @@ import { SnapshotQueryResponse, SnapshotScoresState, SnapshotSpace, - SnapshotStatus, SnapshotVote, SnapshotVoteResponse, SnapshotVpResponse, StrategyOrder, VpDistribution, -} from './SnapshotGraphqlTypes' +} from './SnapshotTypes' import { inBatches, trimLastForwardSlash } from './utils' export const getQueryTimestamp = (dateTimestamp: number) => Math.round(dateTimestamp / 1000) const GRAPHQL_ENDPOINT = `/graphql` const BATCH_SIZE = 1000 -const OLDEST_PROPOSAL_TIMESTAMP = 1621036800 +const GET_VOTES_QUERY = ` + query getVotes($space: String!, $start: Int!, $end: Int!, $first: Int!) { + votes(where: {space: $space, created_gte: $start, created_lt: $end}, orderBy: "created", orderDirection: asc, first: $first) { + id + voter + created + vp + choice + proposal { + id + choices + } + } + } +` export class SnapshotGraphql extends API { static Url = SNAPSHOT_API || 'https://hub.snapshot.org/' @@ -52,12 +67,12 @@ export class SnapshotGraphql extends API { if (SNAPSHOT_API_KEY) this.defaultOptions.header('x-api-key', SNAPSHOT_API_KEY) } - async getStatus() { - const status = await this.fetch('/api/') + async getConfig() { + const snapshotConfig = await this.fetch('/api/') return { - ...status, - version: status.version.split('#')[0], + ...snapshotConfig, + version: snapshotConfig.version.split('#')[0], } } @@ -235,22 +250,6 @@ export class SnapshotGraphql extends API { } async getAllVotesBetweenDates(start: Date, end: Date): Promise { - const query = ` - query getVotes($space: String!, $start: Int!, $end: Int!, $first: Int!) { - votes(where: {space: $space, created_gte: $start, created_lt: $end}, orderBy: "created", orderDirection: asc, first: $first) { - id - voter - created - vp - choice - proposal { - id - choices - } - } - } - ` - let allResults: SnapshotVote[] = [] let hasNext = true let created = getQueryTimestamp(start.getTime()) @@ -268,7 +267,7 @@ export class SnapshotGraphql extends API { const result = await this.fetch( GRAPHQL_ENDPOINT, this.options().method('POST').json({ - query, + GET_VOTES_QUERY, variables, }) ) @@ -444,23 +443,31 @@ export class SnapshotGraphql extends API { return result?.data?.proposal } - async hasVoted(address: string) { - const query = `query HasVoted($space: String!, $address: String!, $created: Int!) { - votes( - where: { space: $space, voter: $address, created_gt: $created} - first: 1 - ) { - voter - } + async ping() { + const now = new Date() + const startTime = now.getTime() + try { + const startDate = getAMonthAgo(now).getTime() + const endDate = now.getTime() + const response = await this.fetch( + GRAPHQL_ENDPOINT, + this.options() + .method('POST') + .json({ + query: GET_VOTES_QUERY, + variables: { + space: SNAPSHOT_SPACE, + first: 10, + start: getQueryTimestamp(startDate), + end: getQueryTimestamp(endDate), + }, + }) + ) + const endTime = new Date().getTime() + const addressesSample = response?.data?.votes.map((vote: SnapshotVote) => vote.voter) + return { responseTime: endTime - startTime, addressesSample } + } catch (error) { + return { responseTime: -1, addressesSample: [] } // Return -1 to indicate API failures } - ` - const result = await this.fetch[] }>>( - GRAPHQL_ENDPOINT, - this.options() - .method('POST') - .json({ query, variables: { address, space: SNAPSHOT_SPACE, created: OLDEST_PROPOSAL_TIMESTAMP } }) - ) - - return result?.data?.votes.length > 0 } } diff --git a/src/clients/SnapshotSubgraph.ts b/src/clients/SnapshotSubgraph.ts index 029e29243..6a3d3df45 100644 --- a/src/clients/SnapshotSubgraph.ts +++ b/src/clients/SnapshotSubgraph.ts @@ -3,7 +3,7 @@ import fetch from 'isomorphic-fetch' import { SNAPSHOT_QUERY_ENDPOINT } from '../entities/Snapshot/constants' import { PICKED_BY_QUERY } from '../entities/Snapshot/queries' -import { Delegation } from './SnapshotGraphqlTypes' +import { Delegation } from './SnapshotTypes' import { inBatches, trimLastForwardSlash } from './utils' export type DelegationQueryResult = { diff --git a/src/clients/SnapshotGraphqlTypes.ts b/src/clients/SnapshotTypes.ts similarity index 86% rename from src/clients/SnapshotGraphqlTypes.ts rename to src/clients/SnapshotTypes.ts index d92403a0f..dd37c7817 100644 --- a/src/clients/SnapshotGraphqlTypes.ts +++ b/src/clients/SnapshotTypes.ts @@ -1,5 +1,5 @@ export type SnapshotQueryResponse = { data: T } -export type SnapshotStatus = { +export type SnapshotConfig = { name: string network: string version: string @@ -131,3 +131,13 @@ export enum StrategyOrder { L1Wearables, Rental, } + +export enum ServiceHealth { + Normal = 'normal', + Slow = 'slow', + Failing = 'failing', + Unknown = 'unknown', +} +export type ServiceStatus = { health: ServiceHealth; responseTime: number } +export type SnapshotStatus = { scoresStatus: ServiceStatus; graphQlStatus: ServiceStatus } +export const UNKNOWN_STATUS = { health: ServiceHealth.Unknown, responseTime: 0 } diff --git a/src/components/Context/BurgerMenuStatusContext.tsx b/src/components/Context/BurgerMenuStatusContext.tsx index bc45dff9a..086388f7f 100644 --- a/src/components/Context/BurgerMenuStatusContext.tsx +++ b/src/components/Context/BurgerMenuStatusContext.tsx @@ -11,6 +11,7 @@ type BurgerMenuStatus = { searching: boolean filtering: boolean translate?: string + snapshotStatusBarOpen: boolean } export type BurgerMenuStatusContextType = { @@ -28,6 +29,7 @@ const BurgerMenuStatusContextProvider = ({ children }: BurgerMenuStatusContextPr open: searchParams.searching || filtering, searching: searchParams.searching, filtering: filtering, + snapshotStatusBarOpen: false, }) return {children} } diff --git a/src/components/Debug/Snapshot.tsx b/src/components/Debug/Snapshot.tsx new file mode 100644 index 000000000..d5cce2180 --- /dev/null +++ b/src/components/Debug/Snapshot.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react' + +import { Button } from 'decentraland-ui/dist/components/Button/Button' +import { Field } from 'decentraland-ui/dist/components/Field/Field' + +import { Governance } from '../../clients/Governance' +import { SNAPSHOT_SPACE } from '../../entities/Snapshot/constants' +import Heading from '../Common/Typography/Heading' +import Label from '../Common/Typography/Label' +import Text from '../Common/Typography/Text' +import ErrorMessage from '../Error/ErrorMessage' +import { ContentSection } from '../Layout/ContentLayout' + +interface Props { + className?: string +} + +export default function Snapshot({ className }: Props) { + const [spaceName, setSpaceName] = useState(SNAPSHOT_SPACE) + const [snapshotConfig, setSnapshotConfig] = useState() + const [snapshotSpace, setSnapshotSpace] = useState() + const [errorMessage, setErrorMessage] = useState() + + async function handleFetchClick() { + setErrorMessage('') + try { + const { config: newConfig, space: newSpace } = await Governance.get().getSnapshotConfigAndSpace(spaceName) + setSnapshotConfig(newConfig) + setSnapshotSpace(newSpace) + setErrorMessage('') + } catch (e: any) { + setErrorMessage(e.message) + } + } + + return ( +
+ {'Snapshot'} + + +
+ setSpaceName(value)} /> + +
+
+ + {JSON.stringify(snapshotConfig)} + + {JSON.stringify(snapshotSpace)} + {!!errorMessage && } +
+ ) +} diff --git a/src/components/Debug/SnapshotStatus.css b/src/components/Debug/SnapshotStatus.css new file mode 100644 index 000000000..8240cf8a0 --- /dev/null +++ b/src/components/Debug/SnapshotStatus.css @@ -0,0 +1,41 @@ +.SnapshotStatus__TopBar { + width: 100%; + height: 0; + background: var(--black-700); + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + z-index: 21; + position: relative; + transition: all 0.75s ease 0s; + padding-left: 26px; +} + +.SnapshotStatus__TopBar--visible { + height: 50px; + padding-top: 4px; + gap: 24px; +} + +.SnapshotStatus__Text { + color: var(--white-900) !important; +} + +@media (min-width: 768px) { + .SnapshotStatus__TopBar { + padding-left: 32px; + align-items: flex-start; + } + + .SnapshotStatus__TopBar--visible { + height: 43px !important; + padding-top: 12px; + padding-left: 0; + gap: 0; + } + + .SnapshotStatus__Text { + margin-left: 6px; + } +} diff --git a/src/components/Debug/SnapshotStatus.tsx b/src/components/Debug/SnapshotStatus.tsx index 1d57d6d81..b9e1311a3 100644 --- a/src/components/Debug/SnapshotStatus.tsx +++ b/src/components/Debug/SnapshotStatus.tsx @@ -1,55 +1,49 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' -import { Button } from 'decentraland-ui/dist/components/Button/Button' -import { Field } from 'decentraland-ui/dist/components/Field/Field' +import classNames from 'classnames' import { Governance } from '../../clients/Governance' -import { SNAPSHOT_SPACE } from '../../entities/Snapshot/constants' -import Heading from '../Common/Typography/Heading' -import Label from '../Common/Typography/Label' -import Text from '../Common/Typography/Text' -import ErrorMessage from '../Error/ErrorMessage' -import { ContentSection } from '../Layout/ContentLayout' - -interface Props { - className?: string +import { ServiceHealth, SnapshotStatus as SnapshotServiceStatus } from '../../clients/SnapshotTypes' +import { useBurgerMenu } from '../../hooks/useBurgerMenu' +import useFormatMessage from '../../hooks/useFormatMessage' +import Markdown from '../Common/Typography/Markdown' +import WarningTriangle from '../Icon/WarningTriangle' + +import './SnapshotStatus.css' + +const PING_INTERVAL_IN_MS = 10000 // 10 seconds + +function logIfNotNormal(status: SnapshotServiceStatus) { + if (status.scoresStatus.health !== ServiceHealth.Normal || status.graphQlStatus.health !== ServiceHealth.Normal) { + console.log('Snapshot Status', status) + } } -export default function SnapshotStatus({ className }: Props) { - const [spaceName, setSpaceName] = useState(SNAPSHOT_SPACE) - const [snapshotStatus, setSnapshotStatus] = useState() - const [snapshotSpace, setSnapshotSpace] = useState() - const [errorMessage, setErrorMessage] = useState() - - async function handleFetchClick() { - setErrorMessage('') - try { - const { status: newStatus, space: newSpace } = await Governance.get().getSnapshotStatusAndSpace(spaceName) - setSnapshotStatus(newStatus) - setSnapshotSpace(newSpace) - setErrorMessage('') - } catch (e: any) { - setErrorMessage(e.message) - } +export default function SnapshotStatus() { + const t = useFormatMessage() + const [showTopBar, setShowTopBar] = useState(false) + const { setStatus } = useBurgerMenu() + + const updateServiceStatus = async () => { + const status = await Governance.get().getSnapshotStatus() + logIfNotNormal(status) + const show = + status.scoresStatus.health === ServiceHealth.Slow || status.scoresStatus.health === ServiceHealth.Failing + setShowTopBar(show) + setStatus((prev) => ({ ...prev, snapshotStatusBarOpen: show })) } + useEffect(() => { + const intervalId = setInterval(updateServiceStatus, PING_INTERVAL_IN_MS) + return () => clearInterval(intervalId) + }, []) + return ( -
- {'Snapshot'} - - -
- setSpaceName(value)} /> - -
-
- - {JSON.stringify(snapshotStatus)} - - {JSON.stringify(snapshotSpace)} - {!!errorMessage && } +
+ + + {t('page.debug.snapshot_status.label')} +
) } diff --git a/src/components/Delegation/DelegationCards.tsx b/src/components/Delegation/DelegationCards.tsx index 495f73fbf..9418d7aa8 100644 --- a/src/components/Delegation/DelegationCards.tsx +++ b/src/components/Delegation/DelegationCards.tsx @@ -2,7 +2,7 @@ import React, { useMemo, useState } from 'react' import Grid from 'semantic-ui-react/dist/commonjs/collections/Grid/Grid' -import { DelegationResult, DetailedScores, VpDistribution } from '../../clients/SnapshotGraphqlTypes' +import { DelegationResult, DetailedScores, VpDistribution } from '../../clients/SnapshotTypes' import useFormatMessage from '../../hooks/useFormatMessage' import { useSortingByKey } from '../../hooks/useSortingByKey' import FullWidthButton from '../Common/FullWidthButton' diff --git a/src/components/Icon/WarningTriangle.tsx b/src/components/Icon/WarningTriangle.tsx new file mode 100644 index 000000000..32797114c --- /dev/null +++ b/src/components/Icon/WarningTriangle.tsx @@ -0,0 +1,28 @@ +import React from 'react' + +function WarningTriangle({ className, size = '14' }: { size?: string; className?: string }) { + return ( + + + + + + + + + + + ) +} + +export default WarningTriangle diff --git a/src/components/Layout/BurgerMenu/BurgerMenuContent.css b/src/components/Layout/BurgerMenu/BurgerMenuContent.css index d2adfcb89..ec0432479 100644 --- a/src/components/Layout/BurgerMenu/BurgerMenuContent.css +++ b/src/components/Layout/BurgerMenu/BurgerMenuContent.css @@ -8,3 +8,7 @@ top: 0; padding: 0 16px; } + +.BurgerMenuContent--PushedDown { + top: 65px !important; +} diff --git a/src/components/Layout/BurgerMenu/BurgerMenuContent.tsx b/src/components/Layout/BurgerMenu/BurgerMenuContent.tsx index 413dd7a8c..f4bf77d7d 100644 --- a/src/components/Layout/BurgerMenu/BurgerMenuContent.tsx +++ b/src/components/Layout/BurgerMenu/BurgerMenuContent.tsx @@ -22,6 +22,7 @@ export type FilterStatus = { export type BurgerMenuContentProps = NavigationProps & { navigationOnly?: boolean + snapshotStatusBarOpen?: boolean } const filtersInitialStatus = { categoryOpen: true, statusOpen: false, timeFrameOpen: false } @@ -34,7 +35,7 @@ const TIMEFRAME_FILTER_HEIGHT = 212 const CLOSED_FILTER_HEIGHT = 56 const MOBILE_NAVIGATION_HEIGHT = 180 -function BurgerMenuContent({ navigationOnly, activeTab }: BurgerMenuContentProps) { +function BurgerMenuContent({ navigationOnly, activeTab, snapshotStatusBarOpen }: BurgerMenuContentProps) { const [footer, setFooter] = useState(null) const [filterStatus, setFilterStatus] = useState(filtersInitialStatus) const { status, setStatus } = useBurgerMenu() @@ -91,7 +92,12 @@ function BurgerMenuContent({ navigationOnly, activeTab }: BurgerMenuContentProps return (
{navigationOnly ? ( diff --git a/src/components/Layout/BurgerMenu/BurgerMenuLayout.tsx b/src/components/Layout/BurgerMenu/BurgerMenuLayout.tsx index fe6c65817..c7d37c605 100644 --- a/src/components/Layout/BurgerMenu/BurgerMenuLayout.tsx +++ b/src/components/Layout/BurgerMenu/BurgerMenuLayout.tsx @@ -14,16 +14,17 @@ interface Props { } function BurgerMenuLayout({ children, navigationOnly, activeTab }: Props) { - const burgerMenu = useBurgerMenu() - + const { status } = useBurgerMenu() + const { open, translate, snapshotStatusBarOpen } = status return ( <> - -
+ +
{children}
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 2a1d7d41e..72d738fef 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -9,6 +9,7 @@ import { Navbar, NavbarProps } from 'decentraland-ui/dist/components/Navbar/Navb import type { PageProps } from 'gatsby' import { isProjectPath } from '../../utils/locations' +import SnapshotStatus from '../Debug/SnapshotStatus' import WalletSelectorModal from '../Modal/WalletSelectorModal' import WrongNetworkModal from '../Modal/WrongNetworkModal' diff --git a/src/components/Modal/VotingPowerDelegationDetail/VotingPowerDistribution.tsx b/src/components/Modal/VotingPowerDelegationDetail/VotingPowerDistribution.tsx index 8c15fae36..19cd430be 100644 --- a/src/components/Modal/VotingPowerDelegationDetail/VotingPowerDistribution.tsx +++ b/src/components/Modal/VotingPowerDelegationDetail/VotingPowerDistribution.tsx @@ -3,7 +3,7 @@ import Skeleton from 'react-loading-skeleton' import classNames from 'classnames' -import { VpDistribution } from '../../../clients/SnapshotGraphqlTypes' +import { VpDistribution } from '../../../clients/SnapshotTypes' import useFormatMessage from '../../../hooks/useFormatMessage' import { EMPTY_DISTRIBUTION } from '../../../hooks/useVotingPowerDistribution' diff --git a/src/components/Modal/VotingPowerDelegationDetail/VotingPowerDistributionLabels.tsx b/src/components/Modal/VotingPowerDelegationDetail/VotingPowerDistributionLabels.tsx index 484536ee9..1babd3c78 100644 --- a/src/components/Modal/VotingPowerDelegationDetail/VotingPowerDistributionLabels.tsx +++ b/src/components/Modal/VotingPowerDelegationDetail/VotingPowerDistributionLabels.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { VpDistribution } from '../../../clients/SnapshotGraphqlTypes' +import { VpDistribution } from '../../../clients/SnapshotTypes' import { getFormattedPercentage } from '../../../helpers' import useFormatMessage from '../../../hooks/useFormatMessage' import { EMPTY_DISTRIBUTION } from '../../../hooks/useVotingPowerDistribution' diff --git a/src/components/Modal/VotingPowerDelegationModal/VotingPowerDelegationModal.tsx b/src/components/Modal/VotingPowerDelegationModal/VotingPowerDelegationModal.tsx index 0517a886a..fdb072ee7 100644 --- a/src/components/Modal/VotingPowerDelegationModal/VotingPowerDelegationModal.tsx +++ b/src/components/Modal/VotingPowerDelegationModal/VotingPowerDelegationModal.tsx @@ -3,7 +3,7 @@ import React from 'react' import { Close } from 'decentraland-ui/dist/components/Close/Close' import { Modal } from 'decentraland-ui/dist/components/Modal/Modal' -import { VpDistribution } from '../../../clients/SnapshotGraphqlTypes' +import { VpDistribution } from '../../../clients/SnapshotTypes' import VotingPowerDelegationDetail from '../VotingPowerDelegationDetail/VotingPowerDelegationDetail' import VotingPowerDelegationCandidatesList, { Candidate } from './VotingPowerDelegationCandidatesList' diff --git a/src/components/Profile/VpDelegationBox.tsx b/src/components/Profile/VpDelegationBox.tsx index f70332cb3..18d8a6d75 100644 --- a/src/components/Profile/VpDelegationBox.tsx +++ b/src/components/Profile/VpDelegationBox.tsx @@ -5,7 +5,7 @@ import { Button } from 'decentraland-ui/dist/components/Button/Button' import { Container } from 'decentraland-ui/dist/components/Container/Container' import Grid from 'semantic-ui-react/dist/commonjs/collections/Grid/Grid' -import { DelegationResult } from '../../clients/SnapshotGraphqlTypes' +import { DelegationResult } from '../../clients/SnapshotTypes' import { isSameAddress } from '../../entities/Snapshot/utils' import useFormatMessage from '../../hooks/useFormatMessage' import useVotingPowerDistribution from '../../hooks/useVotingPowerDistribution' diff --git a/src/components/Profile/VpDelegatorsBox.tsx b/src/components/Profile/VpDelegatorsBox.tsx index 24e802973..af23f9591 100644 --- a/src/components/Profile/VpDelegatorsBox.tsx +++ b/src/components/Profile/VpDelegatorsBox.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Container } from 'decentraland-ui/dist/components/Container/Container' -import { DelegationResult, DetailedScores } from '../../clients/SnapshotGraphqlTypes' +import { DelegationResult, DetailedScores } from '../../clients/SnapshotTypes' import { isSameAddress } from '../../entities/Snapshot/utils' import useFormatMessage from '../../hooks/useFormatMessage' import useVotingPowerInformation from '../../hooks/useVotingPowerInformation' diff --git a/src/components/User/UserStats.tsx b/src/components/User/UserStats.tsx index 47872d621..b78b8db08 100644 --- a/src/components/User/UserStats.tsx +++ b/src/components/User/UserStats.tsx @@ -5,7 +5,7 @@ import { Container } from 'decentraland-ui/dist/components/Container/Container' import { Loader } from 'decentraland-ui/dist/components/Loader/Loader' import { NotMobile, useMobileMediaQuery } from 'decentraland-ui/dist/components/Media/Media' -import { VpDistribution } from '../../clients/SnapshotGraphqlTypes' +import { VpDistribution } from '../../clients/SnapshotTypes' import { isSameAddress } from '../../entities/Snapshot/utils' import useFormatMessage from '../../hooks/useFormatMessage' import useGovernanceProfile from '../../hooks/useGovernanceProfile' diff --git a/src/components/User/UserVpStats.tsx b/src/components/User/UserVpStats.tsx index 4254e5891..3af439e18 100644 --- a/src/components/User/UserVpStats.tsx +++ b/src/components/User/UserVpStats.tsx @@ -1,6 +1,6 @@ import React from 'react' -import { VpDistribution } from '../../clients/SnapshotGraphqlTypes' +import { VpDistribution } from '../../clients/SnapshotTypes' import useFormatMessage from '../../hooks/useFormatMessage' import MobileSlider from '../Common/MobileSlider' diff --git a/src/constants.ts b/src/constants.ts index d5e24d4f4..ac3560ba9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,5 @@ +import isEthereumAddress from 'validator/lib/isEthereumAddress' + import { trimOtterspaceId } from './back/utils/contractInteractions' import { OTTERSPACE_DAO_RAFT_ID } from './entities/Snapshot/constants' import Candidates from './utils/delegates/candidates.json' @@ -32,3 +34,7 @@ export const LAND_OWNER_BADGE_SPEC_CID = process.env.LAND_OWNER_BADGE_SPEC_CID | export const TRIMMED_OTTERSPACE_RAFT_ID = trimOtterspaceId(OTTERSPACE_DAO_RAFT_ID) export const TOP_VOTERS_PER_MONTH = 3 export const TOP_VOTER_BADGE_IMG_URL = process.env.TOP_VOTER_BADGE_IMG_URL || '' +export const DEBUG_ADDRESSES = (env('DEBUG_ADDRESSES', '') || '') + .split(',') + .filter(isEthereumAddress) + .map((address) => address.toLowerCase()) diff --git a/src/entities/Debug/isDebugAddress.ts b/src/entities/Debug/isDebugAddress.ts index 0efb2f050..2e69c3988 100644 --- a/src/entities/Debug/isDebugAddress.ts +++ b/src/entities/Debug/isDebugAddress.ts @@ -1,11 +1,6 @@ import { magenta } from 'colors/safe' -import env from 'decentraland-gatsby/dist/utils/env' -import isEthereumAddress from 'validator/lib/isEthereumAddress' -export const DEBUG_ADDRESSES = (env('DEBUG_ADDRESSES', '') || '') - .split(',') - .filter(isEthereumAddress) - .map((address) => address.toLowerCase()) +import { DEBUG_ADDRESSES } from '../../constants' const debugAddresses = new Set(DEBUG_ADDRESSES) diff --git a/src/entities/Proposal/types.ts b/src/entities/Proposal/types.ts index b8387269d..9bd6cc879 100644 --- a/src/entities/Proposal/types.ts +++ b/src/entities/Proposal/types.ts @@ -4,7 +4,7 @@ import { SQLStatement } from 'decentraland-gatsby/dist/entities/Database/utils' import { CommitteeName } from '../../clients/DclData' -import { SnapshotProposal } from '../../clients/SnapshotGraphqlTypes' +import { SnapshotProposal } from '../../clients/SnapshotTypes' import { CategoryAssessmentQuestions, GrantRequestDueDiligence, diff --git a/src/entities/Snapshot/outcomeMatch.test.ts b/src/entities/Snapshot/outcomeMatch.test.ts index cce221516..807272114 100644 --- a/src/entities/Snapshot/outcomeMatch.test.ts +++ b/src/entities/Snapshot/outcomeMatch.test.ts @@ -1,4 +1,4 @@ -import { SnapshotVote } from '../../clients/SnapshotGraphqlTypes' +import { SnapshotVote } from '../../clients/SnapshotTypes' import { VOTES_EXAMPLE } from './outcomeMatchData' import { outcomeMatch } from './utils' diff --git a/src/entities/Snapshot/testData.ts b/src/entities/Snapshot/testData.ts index 978ebd5ee..65c575362 100644 --- a/src/entities/Snapshot/testData.ts +++ b/src/entities/Snapshot/testData.ts @@ -1,4 +1,4 @@ -import { SnapshotVote } from '../../clients/SnapshotGraphqlTypes' +import { SnapshotVote } from '../../clients/SnapshotTypes' export const VOTES_1: SnapshotVote[] = [ { diff --git a/src/entities/Snapshot/utils.test.ts b/src/entities/Snapshot/utils.test.ts index 3fcff5b19..c27e54338 100644 --- a/src/entities/Snapshot/utils.test.ts +++ b/src/entities/Snapshot/utils.test.ts @@ -1,4 +1,4 @@ -import { SnapshotVote } from '../../clients/SnapshotGraphqlTypes' +import { SnapshotVote } from '../../clients/SnapshotTypes' import { VOTES_1, VOTES_2, VOTES_3, VOTES_4, VOTES_5 } from './testData' import { MatchResult, calculateMatch } from './utils' diff --git a/src/entities/Snapshot/utils.ts b/src/entities/Snapshot/utils.ts index 59bdae85b..6227495f8 100644 --- a/src/entities/Snapshot/utils.ts +++ b/src/entities/Snapshot/utils.ts @@ -2,14 +2,14 @@ import logger from 'decentraland-gatsby/dist/entities/Development/logger' import { ethers } from 'ethers' import { SnapshotGraphql } from '../../clients/SnapshotGraphql' +import { SnapshotSubgraph } from '../../clients/SnapshotSubgraph' import { Delegation, DelegationResult, EMPTY_DELEGATION, SnapshotProposal, SnapshotVote, -} from '../../clients/SnapshotGraphqlTypes' -import { SnapshotSubgraph } from '../../clients/SnapshotSubgraph' +} from '../../clients/SnapshotTypes' import { SNAPSHOT_SPACE } from './constants' import { getDelegatedQuery } from './queries' diff --git a/src/entities/Votes/types.ts b/src/entities/Votes/types.ts index 070a68b61..4a1d305bc 100644 --- a/src/entities/Votes/types.ts +++ b/src/entities/Votes/types.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { SnapshotVote } from '../../clients/SnapshotGraphqlTypes' +import { SnapshotVote } from '../../clients/SnapshotTypes' import { ProposalStatus, ProposalType } from '../Proposal/types' export type VoteAttributes = { diff --git a/src/entities/Votes/utils.ts b/src/entities/Votes/utils.ts index da1c2a8ab..c187134ee 100644 --- a/src/entities/Votes/utils.ts +++ b/src/entities/Votes/utils.ts @@ -1,6 +1,6 @@ import isUUID from 'validator/lib/isUUID' -import { SnapshotVote } from '../../clients/SnapshotGraphqlTypes' +import { SnapshotVote } from '../../clients/SnapshotTypes' import { ChoiceColor, Vote } from './types' diff --git a/src/hooks/useDelegation.test.ts b/src/hooks/useDelegation.test.ts index 3852db789..4f52e01a2 100644 --- a/src/hooks/useDelegation.test.ts +++ b/src/hooks/useDelegation.test.ts @@ -1,4 +1,4 @@ -import { Delegation } from '../clients/SnapshotGraphqlTypes' +import { Delegation } from '../clients/SnapshotTypes' import { SNAPSHOT_SPACE } from '../entities/Snapshot/constants' import { filterDelegationFrom, filterDelegationTo } from '../entities/Snapshot/utils' diff --git a/src/hooks/useDelegation.ts b/src/hooks/useDelegation.ts index 293ebc796..21a182066 100644 --- a/src/hooks/useDelegation.ts +++ b/src/hooks/useDelegation.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' -import { EMPTY_DELEGATION } from '../clients/SnapshotGraphqlTypes' +import { EMPTY_DELEGATION } from '../clients/SnapshotTypes' import { SNAPSHOT_SPACE } from '../entities/Snapshot/constants' import { getDelegations } from '../entities/Snapshot/utils' diff --git a/src/hooks/useDelegationOnProposal.ts b/src/hooks/useDelegationOnProposal.ts index ee87642eb..cd2a033eb 100644 --- a/src/hooks/useDelegationOnProposal.ts +++ b/src/hooks/useDelegationOnProposal.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' -import { EMPTY_DELEGATION } from '../clients/SnapshotGraphqlTypes' +import { EMPTY_DELEGATION } from '../clients/SnapshotTypes' import { ProposalAttributes } from '../entities/Proposal/types' import { getDelegations } from '../entities/Snapshot/utils' diff --git a/src/hooks/useParticipatingVP.ts b/src/hooks/useParticipatingVP.ts index 3a55e05be..8f1631152 100644 --- a/src/hooks/useParticipatingVP.ts +++ b/src/hooks/useParticipatingVP.ts @@ -3,7 +3,7 @@ import { useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import { Governance } from '../clients/Governance' -import { SnapshotProposal } from '../clients/SnapshotGraphqlTypes' +import { SnapshotProposal } from '../clients/SnapshotTypes' import { groupProposalsByMonth, median } from '../entities/Snapshot/utils' import { DEFAULT_QUERY_STALE_TIME } from './constants' diff --git a/src/hooks/useVotingPowerDistribution.ts b/src/hooks/useVotingPowerDistribution.ts index f5bd2d5ad..c73b9522b 100644 --- a/src/hooks/useVotingPowerDistribution.ts +++ b/src/hooks/useVotingPowerDistribution.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query' import { Governance } from '../clients/Governance' -import { VpDistribution } from '../clients/SnapshotGraphqlTypes' +import { VpDistribution } from '../clients/SnapshotTypes' import { DEFAULT_QUERY_STALE_TIME } from './constants' diff --git a/src/hooks/useVotingPowerOnProposal.ts b/src/hooks/useVotingPowerOnProposal.ts index c52d606af..505dd4a6f 100644 --- a/src/hooks/useVotingPowerOnProposal.ts +++ b/src/hooks/useVotingPowerOnProposal.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query' import { Governance } from '../clients/Governance' -import { SnapshotVote, StrategyOrder, VpDistribution } from '../clients/SnapshotGraphqlTypes' +import { SnapshotVote, StrategyOrder, VpDistribution } from '../clients/SnapshotTypes' import { ProposalAttributes } from '../entities/Proposal/types' import { isSameAddress } from '../entities/Snapshot/utils' import { MINIMUM_VP_REQUIRED_TO_VOTE } from '../entities/Votes/constants' diff --git a/src/hooks/useVotingStats.ts b/src/hooks/useVotingStats.ts index d2fd36057..40c8b865a 100644 --- a/src/hooks/useVotingStats.ts +++ b/src/hooks/useVotingStats.ts @@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query' import { Governance } from '../clients/Governance' import { getQueryTimestamp } from '../clients/SnapshotGraphql' -import { SnapshotProposal, SnapshotVote } from '../clients/SnapshotGraphqlTypes' +import { SnapshotProposal, SnapshotVote } from '../clients/SnapshotTypes' import { calculateMatch, getChecksumAddress, outcomeMatch } from '../entities/Snapshot/utils' import { getFormattedPercentage } from '../helpers' import Time from '../utils/date/Time' diff --git a/src/intl/en.json b/src/intl/en.json index 6ddcd5a00..4a1710e87 100644 --- a/src/intl/en.json +++ b/src/intl/en.json @@ -733,7 +733,10 @@ "debug": { "title": "Debug", "description": "A tab where anything is possible", - "error_label": "There was an error." + "error_label": "There was an error.", + "snapshot_status": { + "label": "**Snapshot is unstable.** Voting and creating proposals may be unavailable." + } }, "home": { "title": "Decentraland DAO", diff --git a/src/pages/debug.tsx b/src/pages/debug.tsx index 02ab4b75a..a2df07607 100644 --- a/src/pages/debug.tsx +++ b/src/pages/debug.tsx @@ -10,7 +10,7 @@ import BudgetsUpdate from '../components/Debug/BudgetsUpdate' import EnvStatus from '../components/Debug/EnvStatus' import ErrorReporting from '../components/Debug/ErrorReporting' import HttpStatus from '../components/Debug/HttpStatus' -import SnapshotStatus from '../components/Debug/SnapshotStatus' +import Snapshot from '../components/Debug/Snapshot' import TriggerFunction from '../components/Debug/TriggerFunction' import LogIn from '../components/Layout/LogIn' import Navigation, { NavigationTab } from '../components/Layout/Navigation' @@ -34,7 +34,7 @@ export default function DebugPage() { - + diff --git a/src/server.ts b/src/server.ts index 37a8cff69..0dd09715d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,6 +16,7 @@ import swaggerUi from 'swagger-ui-express' import YAML from 'yaml' import { giveTopVoterBadges, runAirdropJobs } from './back/jobs/BadgeAirdrop' +import { pingSnapshot } from './back/jobs/PingSnapshot' import badges from './back/routes/badges' import bid from './back/routes/bid' import budget from './back/routes/budget' @@ -44,6 +45,7 @@ const jobs = manager() jobs.cron('@eachMinute', finishProposal) jobs.cron('@eachMinute', activateProposals) jobs.cron('@eachMinute', publishBids) +jobs.cron('@eachMinute', pingSnapshot) jobs.cron('@daily', updateGovernanceBudgets) jobs.cron('@daily', runAirdropJobs) jobs.cron('@monthly', giveTopVoterBadges) diff --git a/src/services/CacheService.ts b/src/services/CacheService.ts new file mode 100644 index 000000000..d73179929 --- /dev/null +++ b/src/services/CacheService.ts @@ -0,0 +1,27 @@ +import NodeCache from 'node-cache' + +class CacheService { + private static instance: CacheService + private cache: NodeCache + + private constructor() { + this.cache = new NodeCache() + } + + public static getInstance(): CacheService { + if (!CacheService.instance) { + CacheService.instance = new CacheService() + } + return CacheService.instance + } + + set(key: string, data: any, ttlSeconds?: number) { + ttlSeconds ? this.cache.set(key, data, ttlSeconds) : this.cache.set(key, data) + } + + public get(key: string): T | undefined { + return this.cache.get(key) + } +} + +export default CacheService.getInstance() diff --git a/src/services/ProposalService.ts b/src/services/ProposalService.ts index 80a6a73e9..8d7577914 100644 --- a/src/services/ProposalService.ts +++ b/src/services/ProposalService.ts @@ -4,7 +4,7 @@ import { v1 as uuid } from 'uuid' import { VoteService } from '../back/services/vote' import { Discourse, DiscourseComment, DiscoursePost } from '../clients/Discourse' -import { SnapshotProposalContent } from '../clients/SnapshotGraphqlTypes' +import { SnapshotProposalContent } from '../clients/SnapshotTypes' import CoauthorModel from '../entities/Coauthor/model' import isDAOCommittee from '../entities/Committee/isDAOCommittee' import ProposalModel from '../entities/Proposal/model' diff --git a/src/services/SnapshotService.ts b/src/services/SnapshotService.ts index 2dea6c32d..341151045 100644 --- a/src/services/SnapshotService.ts +++ b/src/services/SnapshotService.ts @@ -3,20 +3,76 @@ import isNumber from 'lodash/isNumber' import { SnapshotApi, SnapshotReceipt } from '../clients/SnapshotApi' import { SnapshotGraphql } from '../clients/SnapshotGraphql' -import { DetailedScores, SnapshotProposal, SnapshotVote, VpDistribution } from '../clients/SnapshotGraphqlTypes' +import { + DetailedScores, + ServiceHealth, + ServiceStatus, + SnapshotProposal, + SnapshotStatus, + SnapshotVote, + UNKNOWN_STATUS, + VpDistribution, +} from '../clients/SnapshotTypes' 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 { ErrorCategory } from '../utils/errorCategories' +import CacheService from './CacheService' +import { ErrorService } from './ErrorService' import { ProposalInCreation, ProposalLifespan } from './ProposalService' import RpcService from './RpcService' const DELEGATION_STRATEGY_NAME = 'delegation' +const SLOW_RESPONSE_TIME_THRESHOLD_IN_MS = 8000 // 8 seconds +const SNAPSHOT_STATUS_CACHE_KEY = 'SNAPSHOT_STATUS' export class SnapshotService { + public static async getStatus(): Promise { + const cachedStatus = CacheService.get(SNAPSHOT_STATUS_CACHE_KEY) + if (cachedStatus) { + return cachedStatus + } + return { scoresStatus: UNKNOWN_STATUS, graphQlStatus: UNKNOWN_STATUS } + } + + public static async ping() { + try { + const { status: graphQlStatus, addressesSample } = await this.pingGraphQl() + const scoresStatus = await this.pingScores(addressesSample) + const snapshotStatus = { scoresStatus, graphQlStatus } + CacheService.set(SNAPSHOT_STATUS_CACHE_KEY, snapshotStatus) + logger.log('Snapshot status:', snapshotStatus) + } catch (error) { + ErrorService.report('Unable to determine snapshot status', { error, category: ErrorCategory.Snapshot }) + } + } + + private static async pingScores(addressesSample: string[]): Promise { + let scoresHealth = ServiceHealth.Normal + const responseTime = await SnapshotApi.get().ping(addressesSample) + if (responseTime === -1) { + scoresHealth = ServiceHealth.Failing + } else if (responseTime > SLOW_RESPONSE_TIME_THRESHOLD_IN_MS) { + scoresHealth = ServiceHealth.Slow + } + return { health: scoresHealth, responseTime } + } + + private static async pingGraphQl(): Promise<{ status: ServiceStatus; addressesSample: string[] }> { + let health = ServiceHealth.Normal + const { responseTime, addressesSample } = await SnapshotGraphql.get().ping() + if (responseTime === -1) { + health = ServiceHealth.Failing + } else if (responseTime > SLOW_RESPONSE_TIME_THRESHOLD_IN_MS) { + health = ServiceHealth.Slow + } + return { status: { health, responseTime }, addressesSample } + } + static async createProposal( proposalInCreation: ProposalInCreation, proposalId: string, @@ -87,17 +143,17 @@ export class SnapshotService { }) } - static async getStatusAndSpace(spaceName = SNAPSHOT_SPACE) { - const [status, space] = await Promise.all([ - await SnapshotGraphql.get().getStatus(), + static async getConfig(spaceName = SNAPSHOT_SPACE) { + const [config, space] = await Promise.all([ + await SnapshotGraphql.get().getConfig(), await SnapshotGraphql.get().getSpace(spaceName), ]) if (!space) { throw new Error(`Couldn't find snapshot space ${spaceName}. \nSnapshot response: ${JSON.stringify(space)} - \nSnapshot status: ${JSON.stringify(status)}`) + \nSnapshot config: ${JSON.stringify(config)}`) } - return { status, space } + return { config, space } } static async getAddressesVotes(addresses: string[], first?: number, skip?: number) {