diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 39616c77e8..e34b0fe12e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,126 +23,4 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 18 - - run: yarn --immutable --immutable-cache --check-cache - changed-workspaces: - runs-on: ubuntu-latest - outputs: - projects: ${{ steps.dry-run.outputs.projects }} - projects-web: ${{ steps.dry-run.outputs.projects-web }} - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - run: corepack enable - - uses: actions/setup-node@v3 - with: - node-version: 18 - - run: yarn --immutable --immutable-cache - - name: Set needs.changed-workspaces.outputs.{projects,projects-web} - id: dry-run - run: | - echo yarn build:ci --dry='json' --filter='[${{ github.event.pull_request.base.sha }}]' | tee -a $GITHUB_STEP_SUMMARY - export DRY_BUILD_JSON=$( - yarn build:ci --dry='json' --filter='[${{ github.event.pull_request.base.sha }}]' | jq '{packages: .packages}' - ) - node <> $GITHUB_STEP_SUMMARY - yarn ts-check:ci --filter=${{ matrix.project }} | tee -a $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - name: lint - run: | - echo '```' >> $GITHUB_STEP_SUMMARY - yarn lint:ci --filter=${{ matrix.project }} | tee -a $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - name: build - if: ${{ needs.changed-workspaces.outputs.projects-web != '[]' }} - run: | - echo '```' >> $GITHUB_STEP_SUMMARY - yarn build:ci --filter=${{ matrix.project }} | tee -a $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - jest-test: - if: ${{ success() && needs.changed-workspaces.outputs.projects != '[]' }} - needs: changed-workspaces - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - shardIndex: [1, 2, 3, 4, 5] - shardTotal: [5] - steps: - - uses: actions/checkout@v3 - - run: corepack enable - - uses: actions/setup-node@v3 - with: - node-version: 18 - - run: yarn --immutable --immutable-cache - - name: test - run: | - echo '```' >> $GITHUB_STEP_SUMMARY - yarn test:ci --filter=${{ join(fromJSON(needs.changed-workspaces.outputs.projects), ' --filter=') }} -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --coverage | tee -a $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - e2e-playwright: - if: ${{ success() && needs.changed-workspaces.outputs.projects-web != '[]' }} - needs: changed-workspaces - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - project: [web] - browser: - - chromium - # - firefox - # - webkit - # - mobile-chrome - - mobile-safari - shardIndex: [1, 2, 3, 4, 5] - shardTotal: [5] - container: - image: mcr.microsoft.com/playwright:v1.33.0-focal - steps: - - uses: actions/checkout@v3 - - run: corepack enable - - uses: actions/setup-node@v3 - with: - node-version: 18 - - run: yarn --immutable --immutable-cache - - name: Playwright testing - run: | - echo '```' >> $GITHUB_STEP_SUMMARY - yarn playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} | tee -a $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - env: - CI: 1 - DEBUG: pw:webserver - PROJECT_NAME: ${{ matrix.project }} - PLAYWRIGHT_HTML_REPORT: playwright-report/${{ matrix.browser }}/${{ matrix.shardIndex }} - - uses: actions/upload-artifact@v3 - if: ${{ failure() }} - with: - name: trace.playwright.dev - path: playwright-report/ - retention-days: 7 + - run: echo Skipped diff --git a/apps/web-namada/.codecov.yml b/apps/web-namada/.codecov.yml new file mode 100644 index 0000000000..a990bf8809 --- /dev/null +++ b/apps/web-namada/.codecov.yml @@ -0,0 +1,17 @@ +# https://docs.codecov.io/docs/commit-status +coverage: + status: + project: + default: + # basic + target: 0 + threshold: 0% + base: 0% + # advanced + branches: [] + if_no_uploads: error + if_not_found: success + if_ci_failed: error + only_pulls: false + flags: [] + paths: [] diff --git a/apps/web-namada/.eslintrc.yml b/apps/web-namada/.eslintrc.yml new file mode 100644 index 0000000000..fc134c47f1 --- /dev/null +++ b/apps/web-namada/.eslintrc.yml @@ -0,0 +1,9 @@ +root: true +extends: + - custom +ignorePatterns: + - '**/node_modules/*' + - '**/out/*' + - '**/.next/*' + - '**/dist/*' + - '**/src/graphql/*' diff --git a/apps/web-namada/CHANGELOG.md b/apps/web-namada/CHANGELOG.md new file mode 100644 index 0000000000..79e701b844 --- /dev/null +++ b/apps/web-namada/CHANGELOG.md @@ -0,0 +1 @@ +# Unreleased diff --git a/apps/web-namada/codegen.yml b/apps/web-namada/codegen.yml new file mode 100644 index 0000000000..2609c55885 --- /dev/null +++ b/apps/web-namada/codegen.yml @@ -0,0 +1,13 @@ +overwrite: true +generates: + ./src/graphql/types/general_types.ts: + documents: + - 'src/graphql/general/*' + schema: http://154.91.1.75:8080/v1/graphql + config: + # omitOperationSuffix: true + skipTypeNameForRoot: true + plugins: + - "typescript" + - "typescript-operations" + - "typescript-react-apollo" # To generate custom hooks per query diff --git a/apps/web-namada/jest.config.ts b/apps/web-namada/jest.config.ts new file mode 100644 index 0000000000..d0d9cb919a --- /dev/null +++ b/apps/web-namada/jest.config.ts @@ -0,0 +1,32 @@ +import configFromPreset from 'jest-presets/jest/node/jest-preset'; +import nextJest from 'next/jest'; +import { pathsToModuleNameMapper } from 'ts-jest'; +import tsconfig from './tsconfig.json'; + +/* Creating a jest configuration for nextjs. */ +const createJestConfig = nextJest({ + dir: './', +})(configFromPreset); + +const exportFunc = async () => { + // Create Next.js jest configuration + const configFromNext = await createJestConfig(); + Object.keys(configFromNext.moduleNameMapper).forEach((regExp) => { + if (new RegExp(regExp).test('_.svg')) { + configFromNext.moduleNameMapper[regExp] = 'shared-utils/__mocks__/svg.js'; + } + }); + // moduleNameMapper overrided by nextjs, so we need to add it here. + const finalConfig = { + ...configFromNext, + moduleNameMapper: { + ...configFromNext.moduleNameMapper, + ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { + prefix: '/src/', + }), + }, + }; + return finalConfig; +}; + +export default exportFunc; diff --git a/apps/web-namada/jest.setup.ts b/apps/web-namada/jest.setup.ts new file mode 100644 index 0000000000..301ebcb836 --- /dev/null +++ b/apps/web-namada/jest.setup.ts @@ -0,0 +1,16 @@ +import mockApollo from '@/tests/mocks/mockApollo'; +import mockChainConfig from '@/tests/mocks/mockChainConfig'; +import mockDayJs from '@/tests/mocks/mockDayJs'; +import mockDynamicComponent from '@/tests/mocks/mockDynamicComponent'; +import mockI18Next from '@/tests/mocks/mockI18Next'; +import mockProfiles from '@/tests/mocks/mockProfiles'; +import '@testing-library/jest-dom/extend-expect'; +import 'jest-localstorage-mock'; + +jest.setTimeout(30000); +mockI18Next(); +mockApollo(); +mockChainConfig(); +mockDayJs(); +mockDynamicComponent(); +mockProfiles(); diff --git a/apps/web-namada/next-env.d.ts b/apps/web-namada/next-env.d.ts new file mode 100644 index 0000000000..4f11a03dc6 --- /dev/null +++ b/apps/web-namada/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/web-namada/next-i18next.config.js b/apps/web-namada/next-i18next.config.js new file mode 100644 index 0000000000..ebf68ae324 --- /dev/null +++ b/apps/web-namada/next-i18next.config.js @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { resolve } = require('path'); + +module.exports = { + i18n: { + defaultLocale: 'en', + locales: ['en', 'zht', 'zhs', 'it', 'pl'], + }, + localeDetection: false, + localePath: resolve('../../packages/ui/public/locales'), +}; diff --git a/apps/web-namada/next-sitemap.config.js b/apps/web-namada/next-sitemap.config.js new file mode 100644 index 0000000000..f1d46928c5 --- /dev/null +++ b/apps/web-namada/next-sitemap.config.js @@ -0,0 +1,5 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { readFileSync } = require('fs'); +const getSitemap = require('shared-utils/configs/sitemap'); + +module.exports = getSitemap(JSON.parse(readFileSync('./package.json', 'utf8')).name); diff --git a/apps/web-namada/next.config.js b/apps/web-namada/next.config.js new file mode 100644 index 0000000000..d9dff8f572 --- /dev/null +++ b/apps/web-namada/next.config.js @@ -0,0 +1,16 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { readFileSync } = require('fs'); +const { i18n } = require('./next-i18next.config'); +const getNextConfig = require('../../packages/shared-utils/configs/next'); + +const nextConfig = getNextConfig(JSON.parse(readFileSync('./package.json', 'utf8')).name); +nextConfig.i18n = i18n; + +nextConfig.rewrites = async () => [ + { + source: '/gql', + destination: 'http://154.91.1.75:8080/v1/graphql', + }, +]; + +module.exports = nextConfig; diff --git a/apps/web-namada/package.json b/apps/web-namada/package.json new file mode 100644 index 0000000000..23fec938fd --- /dev/null +++ b/apps/web-namada/package.json @@ -0,0 +1,138 @@ +{ + "name": "web-namada", + "version": "2.16.3", + "license": "Apache-2.0", + "private": true, + "scripts": { + "dev": "RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED=false next dev", + "build": "next build && next-sitemap", + "clean": "rm -rf .next .swc .turbo coverage node_modules", + "start": "next start", + "ts-check": "pnpify tsc --noemit", + "lint": "next lint", + "test": "pnpify jest --passWithNoTests --ci --no-watchman --runInBand", + "graphql:codegen": "pnpify graphql-codegen" + }, + "dependencies": { + "@apollo/client": "^3.7.14", + "@cosmjs/encoding": "^0.30.1", + "@cosmjs/launchpad": "^0.27.1", + "@cosmjs/stargate": "^0.29.5", + "@emotion/react": "^11.11.0", + "@emotion/server": "^11.11.0", + "@emotion/styled": "^11.11.0", + "@keplr-wallet/types": "^0.11.59", + "@keplr-wallet/wc-client": "^0.11.59", + "@mui/icons-material": "^5.11.16", + "@mui/material": "^5.12.3", + "@socialgouv/matomo-next": "^1.6.1", + "@walletconnect/client": "^1.8.0", + "@walletconnect/encoding": "^1.0.2", + "@yarnpkg/pnpify": "^4.0.0-rc.43", + "apollo-link-rest": "^0.9.0", + "bech32": "^2.0.0", + "big.js": "^6.2.1", + "color": "^4.2.3", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.7", + "framer-motion": "^10.12.8", + "graphql": "^16.6.0", + "graphql-ws": "^5.12.1", + "i18next": "^22.4.15", + "jdenticon": "^3.2.0", + "js-yaml": "^4.1.0", + "lightweight-charts": "^4.0.1", + "markdown-to-jsx": "^7.2.0", + "next": "^13.4.1", + "next-i18next": "^13.2.2", + "next-seo": "^6.0.0", + "next-sitemap": "^4.1.3", + "numeral": "^2.0.6", + "qrcode.react": "^3.1.0", + "qs": "^6.11.1", + "ramda": "^0.29.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^12.2.2", + "react-share": "^4.4.1", + "react-toastify": "^9.1.2", + "react-virtualized-auto-sizer": "^1.0.15", + "react-window": "^1.8.9", + "react-window-infinite-loader": "^1.0.9", + "recharts": "^2.5.0", + "recoil": "^0.7.7", + "shared-utils": "workspace:*", + "subscriptions-transport-ws": "^0.11.0", + "tsconfig": "workspace:*", + "tslib": "^2.5.0", + "tss-react": "^4.8.3", + "typanion": "^3.12.1", + "ui": "workspace:*", + "usehooks-ts": "^2.9.1", + "ws": "^8.13.0", + "xss": "^1.0.14", + "zod": "^3.21.4" + }, + "devDependencies": { + "@emotion/cache": "^11.11.0", + "@emotion/jest": "^11.11.0", + "@graphql-codegen/cli": "^3.3.1", + "@graphql-codegen/client-preset": "^3.0.1", + "@graphql-codegen/fragment-matcher": "^4.0.1", + "@graphql-codegen/typescript": "^3.0.4", + "@graphql-codegen/typescript-operations": "^3.0.4", + "@graphql-codegen/typescript-react-apollo": "^3.3.7", + "@graphql-tools/mock": "^8.7.20", + "@graphql-tools/schema": "^9.0.19", + "@jest/globals": "^29.5.0", + "@next/eslint-plugin-next": "^13.4.1", + "@svgr/webpack": "^7.0.0", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^14.0.0", + "@types/big.js": "^6.1.6", + "@types/color": "^3.0.3", + "@types/eslint": "^8.37.0", + "@types/esprima": "^4.0.3", + "@types/jest": "^29.5.1", + "@types/js-yaml": "^4.0.5", + "@types/node": "^18.16.5", + "@types/numeral": "^2.0.2", + "@types/qs": "^6.9.7", + "@types/ramda": "^0.29.1", + "@types/react": "^18.2.6", + "@types/react-dom": "^18.2.4", + "@types/react-test-renderer": "^18.0.0", + "@types/react-virtualized-auto-sizer": "^1.0.1", + "@types/react-window": "^1.8.5", + "@types/react-window-infinite-loader": "^1.0.6", + "@typescript-eslint/eslint-plugin": "^5.59.2", + "@typescript-eslint/parser": "^5.59.2", + "csstype": "^3.1.2", + "dotenv": "^16.0.3", + "eslint": "^8.40.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-custom": "workspace:*", + "eslint-config-next": "^13.4.1", + "eslint-config-prettier": "^8.8.0", + "eslint-config-turbo": "^1.9.3", + "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-turbo": "^1.9.3", + "esprima": "^4.0.1", + "graphql-tag": "^2.12.6", + "jest": "^29.5.0", + "jest-cli": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "jest-localstorage-mock": "^2.4.26", + "jest-presets": "workspace:*", + "jest-transform-stub": "^2.0.0", + "jest-watch-typeahead": "^2.2.2", + "react-test-renderer": "^18.2.0", + "ts-jest": "^29.1.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + } +} diff --git a/apps/web-namada/public/fonts/HindMadurai-Regular.woff2 b/apps/web-namada/public/fonts/HindMadurai-Regular.woff2 new file mode 100644 index 0000000000..64b2f86e1e Binary files /dev/null and b/apps/web-namada/public/fonts/HindMadurai-Regular.woff2 differ diff --git a/apps/web-namada/public/icons/android-chrome-192x192.png b/apps/web-namada/public/icons/android-chrome-192x192.png new file mode 100644 index 0000000000..0919ccf167 Binary files /dev/null and b/apps/web-namada/public/icons/android-chrome-192x192.png differ diff --git a/apps/web-namada/public/icons/android-chrome-512x512.png b/apps/web-namada/public/icons/android-chrome-512x512.png new file mode 100644 index 0000000000..119aaee7d7 Binary files /dev/null and b/apps/web-namada/public/icons/android-chrome-512x512.png differ diff --git a/apps/web-namada/public/icons/apple-touch-icon.png b/apps/web-namada/public/icons/apple-touch-icon.png new file mode 100644 index 0000000000..b964efd4ea Binary files /dev/null and b/apps/web-namada/public/icons/apple-touch-icon.png differ diff --git a/apps/web-namada/public/icons/browserconfig.xml b/apps/web-namada/public/icons/browserconfig.xml new file mode 100644 index 0000000000..9ebcb517e9 --- /dev/null +++ b/apps/web-namada/public/icons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/apps/web-namada/public/icons/favicon-16x16.png b/apps/web-namada/public/icons/favicon-16x16.png new file mode 100644 index 0000000000..148a4bc5d1 Binary files /dev/null and b/apps/web-namada/public/icons/favicon-16x16.png differ diff --git a/apps/web-namada/public/icons/favicon-32x32.png b/apps/web-namada/public/icons/favicon-32x32.png new file mode 100644 index 0000000000..9165e9624e Binary files /dev/null and b/apps/web-namada/public/icons/favicon-32x32.png differ diff --git a/apps/web-namada/public/icons/favicon.ico b/apps/web-namada/public/icons/favicon.ico new file mode 100644 index 0000000000..d1cfa921ae Binary files /dev/null and b/apps/web-namada/public/icons/favicon.ico differ diff --git a/apps/web-namada/public/icons/mstile-150x150.png b/apps/web-namada/public/icons/mstile-150x150.png new file mode 100644 index 0000000000..b728d6b7d2 Binary files /dev/null and b/apps/web-namada/public/icons/mstile-150x150.png differ diff --git a/apps/web-namada/public/icons/safari-pinned-tab.svg b/apps/web-namada/public/icons/safari-pinned-tab.svg new file mode 100644 index 0000000000..a155a03bb5 --- /dev/null +++ b/apps/web-namada/public/icons/safari-pinned-tab.svg @@ -0,0 +1,61 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + + diff --git a/apps/web-namada/public/icons/site.webmanifest b/apps/web-namada/public/icons/site.webmanifest new file mode 100644 index 0000000000..c5d1b226ad --- /dev/null +++ b/apps/web-namada/public/icons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "https://explorer.desmos.network/images/icons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "https://explorer.desmos.network/images/icons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/apps/web-namada/public/images/default_cover_pattern.png b/apps/web-namada/public/images/default_cover_pattern.png new file mode 100644 index 0000000000..bf8f8bc3d1 Binary files /dev/null and b/apps/web-namada/public/images/default_cover_pattern.png differ diff --git a/apps/web-namada/public/images/icon.svg b/apps/web-namada/public/images/icon.svg new file mode 100644 index 0000000000..605a109998 --- /dev/null +++ b/apps/web-namada/public/images/icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web-namada/public/images/logo.svg b/apps/web-namada/public/images/logo.svg new file mode 100644 index 0000000000..b4b4d6e308 --- /dev/null +++ b/apps/web-namada/public/images/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-namada/src/chain.json b/apps/web-namada/src/chain.json new file mode 100644 index 0000000000..d1776dd6ea --- /dev/null +++ b/apps/web-namada/src/chain.json @@ -0,0 +1,162 @@ +{ + "chainName": "namada", + "title": "Namada Block Explorer", + "extra": { + "profile": true, + "graphqlWs": true + }, + "previewImage": "https://s3.bigdipper.live/crescent.png", + "themes": { + "default": "dark", + "themeList": [ + "dark", + "deuteranopia", + "tritanopia" + ], + "dark": { + "primary": { + "main": "#FFC780", + "contrastText": "#FFFFFF" + }, + "background": { + "default": "#1F0E0B", + "paper": "#2E150F" + }, + "divider": "#4D4441", + "text": { + "primary": "#E6E6E6", + "secondary": "#C4C4C4" + }, + "custom": { + "general": { + "background": "#1F0E0B", + "surfaceOne": "#2E150F", + "surfaceTwo": "#333333", + "icon": "#999999" + }, + "fonts": { + "fontOne": "#E6E6E6", + "fontTwo": "#C4C4C4", + "fontThree": "#818181", + "fontFour": "#999999", + "fontFive": "#FFFFFF", + "highlight": "#6AA6FF" + }, + "primaryData": { + "one": "#FFC780", + "two": "#ECA54C", + "three": "#D17A0C", + "four": "#D1530C" + }, + "results": { + "pass": "#1EC490", + "fail": "#FD3B4C" + }, + "tokenomics": { + "one": "#ED512F", + "two": "#FFC780", + "three": "#5FBE15" + }, + "condition": { + "zero": "#E8E8E8", + "one": "#1EC490", + "two": "#FF9338", + "three": "#FF608A" + }, + "charts": { + "zero": "#E8E8E8", + "one": "#1EC490", + "two": "#EA5356", + "three": "#285ED5", + "four": "#FFC780", + "five": "#DE51A6" + }, + "tags": { + "zero": "#E8E8E8", + "one": "#2460FA", + "two": "#2BA897", + "three": "#E79726", + "four": "#F17053", + "five": "#DA4B4B", + "six": "#9438DC", + "seven": "#1A869D", + "eight": "#2C9950", + "nine": "#B49F37", + "ten": "#E9A852", + "eleven": "#E94687", + "twelve": "#C15EC4", + "thirteen": "#C388D9", + "fourteen": "#46AEE9", + "fifteen": "#58BC91", + "sixteen": "#90BC58", + "seventeen": "#E99E8E", + "eighteen": "#F0A479", + "nineteen": "#D37763", + "twenty": "#D9C788" + }, + "wallet": { + "background": "#5E5E5E", + "backgroundTwo": "#212123", + "surfaceOne": "#5E5C5C", + "surfaceTwo": "#D9D9D9", + "surfaceThree": "#4D4D4D", + "surfaceFour": "#414141", + "surfaceFive": "#777777", + "divider": "#34383E", + "textPrimary": "#000000", + "textSecondary": "#DDDDDD" + } + } + }, + "light": { + "primary": {}, + "background": {}, + "text": {}, + "custom": { + "general": { + "icon": "#999999" + }, + "fonts": {}, + "primaryData": {}, + "results": {}, + "tokenomics": {}, + "condition": {}, + "charts": {}, + "tags": {}, + "wallet": {} + } + } + }, + "chains": [ + { + "network": "namada-testnet", + "chainType": "Testnet", + "genesis": { + "time": "2022-04-13T00:00:00", + "height": 1 + }, + "prefix": { + "consensus": "crevalcons", + "validator": "crevaloper", + "account": "tnam" + }, + "primaryTokenUnit": "unam", + "votingPowerTokenUnit": "unam", + "tokenUnits": { + "unam": { + "display": "nam", + "exponent": 6 + } + }, + "endpoints": { + "graphql": "/namada/gql", + "graphqlWebsocket": "wss://gql.crescent.forbole.com/v1/graphql", + "publicRpcWebsocket": "wss://rpc.crescent.forbole.com/websocket" + }, + "marketing": { + "matomoURL": "https://analytics.bigdipper.live", + "matomoSiteID": "8" + } + } + ] +} diff --git a/apps/web-namada/src/components/index.ts b/apps/web-namada/src/components/index.ts new file mode 100644 index 0000000000..d8efbf952a --- /dev/null +++ b/apps/web-namada/src/components/index.ts @@ -0,0 +1 @@ +export { default as LiquidStakingExplanation } from '@/components/liquid_staking_explanation'; diff --git a/apps/web-namada/src/components/liquid_staking_explanation/index.tsx b/apps/web-namada/src/components/liquid_staking_explanation/index.tsx new file mode 100644 index 0000000000..87f472505e --- /dev/null +++ b/apps/web-namada/src/components/liquid_staking_explanation/index.tsx @@ -0,0 +1,17 @@ +import Typography from '@mui/material/Typography'; +import AppTrans from '@/components/AppTrans'; +import useStyles from '@/components/liquid_staking_explanation/styles'; + +const LiquidStakingExplanation = () => { + const { classes } = useStyles(); + + return ( +
+ + ]} /> + +
+ ); +}; + +export default LiquidStakingExplanation; diff --git a/apps/web-namada/src/components/liquid_staking_explanation/styles.ts b/apps/web-namada/src/components/liquid_staking_explanation/styles.ts new file mode 100644 index 0000000000..920476b7cf --- /dev/null +++ b/apps/web-namada/src/components/liquid_staking_explanation/styles.ts @@ -0,0 +1,9 @@ +import { makeStyles } from 'tss-react/mui'; + +const useStyles = makeStyles()(() => ({ + root: { + height: '100%', + }, +})); + +export default useStyles; diff --git a/apps/web-namada/src/graphql/general/block_details.graphql b/apps/web-namada/src/graphql/general/block_details.graphql new file mode 100644 index 0000000000..f9d9dfb3d9 --- /dev/null +++ b/apps/web-namada/src/graphql/general/block_details.graphql @@ -0,0 +1,9 @@ +query BlockDetails($height: bigint) { + block(limit: 1, where: {height: {_eq: $height}}) { + height + hash + timestamp + txs: num_txs + proposerAddress: proposer_address + } +} diff --git a/apps/web-namada/src/graphql/general/blocks.graphql b/apps/web-namada/src/graphql/general/blocks.graphql new file mode 100644 index 0000000000..e029817072 --- /dev/null +++ b/apps/web-namada/src/graphql/general/blocks.graphql @@ -0,0 +1,9 @@ +query Blocks($limit: Int = 7, $offset: Int = 0) { + blocks: block(limit: $limit, offset: $offset, order_by: {height: desc}) { + height + txs: num_txs + hash + timestamp + proposerAddress: proposer_address + } +} diff --git a/apps/web-namada/src/graphql/types/general_types.ts b/apps/web-namada/src/graphql/types/general_types.ts new file mode 100644 index 0000000000..b98d321805 --- /dev/null +++ b/apps/web-namada/src/graphql/types/general_types.ts @@ -0,0 +1,1269 @@ +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +const defaultOptions = {} as const; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + bigint: any; + jsonb: any; + timestamp: any; +}; + +/** Boolean expression to compare columns of type "Boolean". All fields are combined with logical 'AND'. */ +export type Boolean_Comparison_Exp = { + _eq?: InputMaybe; + _gt?: InputMaybe; + _gte?: InputMaybe; + _in?: InputMaybe>; + _is_null?: InputMaybe; + _lt?: InputMaybe; + _lte?: InputMaybe; + _neq?: InputMaybe; + _nin?: InputMaybe>; +}; + +/** Boolean expression to compare columns of type "Int". All fields are combined with logical 'AND'. */ +export type Int_Comparison_Exp = { + _eq?: InputMaybe; + _gt?: InputMaybe; + _gte?: InputMaybe; + _in?: InputMaybe>; + _is_null?: InputMaybe; + _lt?: InputMaybe; + _lte?: InputMaybe; + _neq?: InputMaybe; + _nin?: InputMaybe>; +}; + +/** Boolean expression to compare columns of type "String". All fields are combined with logical 'AND'. */ +export type String_Comparison_Exp = { + _eq?: InputMaybe; + _gt?: InputMaybe; + _gte?: InputMaybe; + /** does the column match the given case-insensitive pattern */ + _ilike?: InputMaybe; + _in?: InputMaybe>; + /** does the column match the given POSIX regular expression, case insensitive */ + _iregex?: InputMaybe; + _is_null?: InputMaybe; + /** does the column match the given pattern */ + _like?: InputMaybe; + _lt?: InputMaybe; + _lte?: InputMaybe; + _neq?: InputMaybe; + /** does the column NOT match the given case-insensitive pattern */ + _nilike?: InputMaybe; + _nin?: InputMaybe>; + /** does the column NOT match the given POSIX regular expression, case insensitive */ + _niregex?: InputMaybe; + /** does the column NOT match the given pattern */ + _nlike?: InputMaybe; + /** does the column NOT match the given POSIX regular expression, case sensitive */ + _nregex?: InputMaybe; + /** does the column NOT match the given SQL regular expression */ + _nsimilar?: InputMaybe; + /** does the column match the given POSIX regular expression, case sensitive */ + _regex?: InputMaybe; + /** does the column match the given SQL regular expression */ + _similar?: InputMaybe; +}; + +/** Boolean expression to compare columns of type "bigint". All fields are combined with logical 'AND'. */ +export type Bigint_Comparison_Exp = { + _eq?: InputMaybe; + _gt?: InputMaybe; + _gte?: InputMaybe; + _in?: InputMaybe>; + _is_null?: InputMaybe; + _lt?: InputMaybe; + _lte?: InputMaybe; + _neq?: InputMaybe; + _nin?: InputMaybe>; +}; + +/** columns and relationships of "block" */ +export type Block = { + __typename?: 'block'; + hash: Scalars['String']; + height: Scalars['bigint']; + num_txs?: Maybe; + /** An array relationship */ + pre_commits: Array; + /** An aggregate relationship */ + pre_commits_aggregate: Pre_Commit_Aggregate; + proposer_address?: Maybe; + timestamp: Scalars['timestamp']; + total_gas?: Maybe; + /** An array relationship */ + transactions: Array; + /** An object relationship */ + validator?: Maybe; +}; + + +/** columns and relationships of "block" */ +export type BlockPre_CommitsArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +/** columns and relationships of "block" */ +export type BlockPre_Commits_AggregateArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +/** columns and relationships of "block" */ +export type BlockTransactionsArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + +/** order by aggregate values of table "block" */ +export type Block_Aggregate_Order_By = { + avg?: InputMaybe; + count?: InputMaybe; + max?: InputMaybe; + min?: InputMaybe; + stddev?: InputMaybe; + stddev_pop?: InputMaybe; + stddev_samp?: InputMaybe; + sum?: InputMaybe; + var_pop?: InputMaybe; + var_samp?: InputMaybe; + variance?: InputMaybe; +}; + +/** order by avg() on columns of table "block" */ +export type Block_Avg_Order_By = { + height?: InputMaybe; + num_txs?: InputMaybe; + total_gas?: InputMaybe; +}; + +/** Boolean expression to filter rows from the table "block". All fields are combined with a logical 'AND'. */ +export type Block_Bool_Exp = { + _and?: InputMaybe>; + _not?: InputMaybe; + _or?: InputMaybe>; + hash?: InputMaybe; + height?: InputMaybe; + num_txs?: InputMaybe; + pre_commits?: InputMaybe; + pre_commits_aggregate?: InputMaybe; + proposer_address?: InputMaybe; + timestamp?: InputMaybe; + total_gas?: InputMaybe; + transactions?: InputMaybe; + validator?: InputMaybe; +}; + +/** order by max() on columns of table "block" */ +export type Block_Max_Order_By = { + hash?: InputMaybe; + height?: InputMaybe; + num_txs?: InputMaybe; + proposer_address?: InputMaybe; + timestamp?: InputMaybe; + total_gas?: InputMaybe; +}; + +/** order by min() on columns of table "block" */ +export type Block_Min_Order_By = { + hash?: InputMaybe; + height?: InputMaybe; + num_txs?: InputMaybe; + proposer_address?: InputMaybe; + timestamp?: InputMaybe; + total_gas?: InputMaybe; +}; + +/** Ordering options when selecting data from "block". */ +export type Block_Order_By = { + hash?: InputMaybe; + height?: InputMaybe; + num_txs?: InputMaybe; + pre_commits_aggregate?: InputMaybe; + proposer_address?: InputMaybe; + timestamp?: InputMaybe; + total_gas?: InputMaybe; + transactions_aggregate?: InputMaybe; + validator?: InputMaybe; +}; + +/** select columns of table "block" */ +export enum Block_Select_Column { + /** column name */ + Hash = 'hash', + /** column name */ + Height = 'height', + /** column name */ + NumTxs = 'num_txs', + /** column name */ + ProposerAddress = 'proposer_address', + /** column name */ + Timestamp = 'timestamp', + /** column name */ + TotalGas = 'total_gas' +} + +/** order by stddev() on columns of table "block" */ +export type Block_Stddev_Order_By = { + height?: InputMaybe; + num_txs?: InputMaybe; + total_gas?: InputMaybe; +}; + +/** order by stddev_pop() on columns of table "block" */ +export type Block_Stddev_Pop_Order_By = { + height?: InputMaybe; + num_txs?: InputMaybe; + total_gas?: InputMaybe; +}; + +/** order by stddev_samp() on columns of table "block" */ +export type Block_Stddev_Samp_Order_By = { + height?: InputMaybe; + num_txs?: InputMaybe; + total_gas?: InputMaybe; +}; + +/** Streaming cursor of the table "block" */ +export type Block_Stream_Cursor_Input = { + /** Stream column input with initial value */ + initial_value: Block_Stream_Cursor_Value_Input; + /** cursor ordering */ + ordering?: InputMaybe; +}; + +/** Initial value of the column from where the streaming should start */ +export type Block_Stream_Cursor_Value_Input = { + hash?: InputMaybe; + height?: InputMaybe; + num_txs?: InputMaybe; + proposer_address?: InputMaybe; + timestamp?: InputMaybe; + total_gas?: InputMaybe; +}; + +/** order by sum() on columns of table "block" */ +export type Block_Sum_Order_By = { + height?: InputMaybe; + num_txs?: InputMaybe; + total_gas?: InputMaybe; +}; + +/** order by var_pop() on columns of table "block" */ +export type Block_Var_Pop_Order_By = { + height?: InputMaybe; + num_txs?: InputMaybe; + total_gas?: InputMaybe; +}; + +/** order by var_samp() on columns of table "block" */ +export type Block_Var_Samp_Order_By = { + height?: InputMaybe; + num_txs?: InputMaybe; + total_gas?: InputMaybe; +}; + +/** order by variance() on columns of table "block" */ +export type Block_Variance_Order_By = { + height?: InputMaybe; + num_txs?: InputMaybe; + total_gas?: InputMaybe; +}; + +/** ordering argument of a cursor */ +export enum Cursor_Ordering { + /** ascending ordering of the cursor */ + Asc = 'ASC', + /** descending ordering of the cursor */ + Desc = 'DESC' +} + +export type Jsonb_Cast_Exp = { + String?: InputMaybe; +}; + +/** Boolean expression to compare columns of type "jsonb". All fields are combined with logical 'AND'. */ +export type Jsonb_Comparison_Exp = { + _cast?: InputMaybe; + /** is the column contained in the given json value */ + _contained_in?: InputMaybe; + /** does the column contain the given json value at the top level */ + _contains?: InputMaybe; + _eq?: InputMaybe; + _gt?: InputMaybe; + _gte?: InputMaybe; + /** does the string exist as a top-level key in the column */ + _has_key?: InputMaybe; + /** do all of these strings exist as top-level keys in the column */ + _has_keys_all?: InputMaybe>; + /** do any of these strings exist as top-level keys in the column */ + _has_keys_any?: InputMaybe>; + _in?: InputMaybe>; + _is_null?: InputMaybe; + _lt?: InputMaybe; + _lte?: InputMaybe; + _neq?: InputMaybe; + _nin?: InputMaybe>; +}; + +/** columns and relationships of "message" */ +export type Message = { + __typename?: 'message'; + height: Scalars['bigint']; + /** An object relationship */ + transaction?: Maybe; + transaction_hash: Scalars['String']; + type: Scalars['String']; + value: Scalars['jsonb']; +}; + + +/** columns and relationships of "message" */ +export type MessageValueArgs = { + path?: InputMaybe; +}; + +/** Boolean expression to filter rows from the table "message". All fields are combined with a logical 'AND'. */ +export type Message_Bool_Exp = { + _and?: InputMaybe>; + _not?: InputMaybe; + _or?: InputMaybe>; + height?: InputMaybe; + transaction?: InputMaybe; + transaction_hash?: InputMaybe; + type?: InputMaybe; + value?: InputMaybe; +}; + +/** Ordering options when selecting data from "message". */ +export type Message_Order_By = { + height?: InputMaybe; + transaction?: InputMaybe; + transaction_hash?: InputMaybe; + type?: InputMaybe; + value?: InputMaybe; +}; + +/** select columns of table "message" */ +export enum Message_Select_Column { + /** column name */ + Height = 'height', + /** column name */ + TransactionHash = 'transaction_hash', + /** column name */ + Type = 'type', + /** column name */ + Value = 'value' +} + +/** Streaming cursor of the table "message" */ +export type Message_Stream_Cursor_Input = { + /** Stream column input with initial value */ + initial_value: Message_Stream_Cursor_Value_Input; + /** cursor ordering */ + ordering?: InputMaybe; +}; + +/** Initial value of the column from where the streaming should start */ +export type Message_Stream_Cursor_Value_Input = { + height?: InputMaybe; + transaction_hash?: InputMaybe; + type?: InputMaybe; + value?: InputMaybe; +}; + +/** column ordering options */ +export enum Order_By { + /** in ascending order, nulls last */ + Asc = 'asc', + /** in ascending order, nulls first */ + AscNullsFirst = 'asc_nulls_first', + /** in ascending order, nulls last */ + AscNullsLast = 'asc_nulls_last', + /** in descending order, nulls first */ + Desc = 'desc', + /** in descending order, nulls first */ + DescNullsFirst = 'desc_nulls_first', + /** in descending order, nulls last */ + DescNullsLast = 'desc_nulls_last' +} + +/** columns and relationships of "pre_commit" */ +export type Pre_Commit = { + __typename?: 'pre_commit'; + height: Scalars['bigint']; + proposer_priority: Scalars['bigint']; + timestamp: Scalars['timestamp']; + /** An object relationship */ + validator?: Maybe; + validator_address: Scalars['String']; + voting_power: Scalars['bigint']; +}; + +/** aggregated selection of "pre_commit" */ +export type Pre_Commit_Aggregate = { + __typename?: 'pre_commit_aggregate'; + aggregate?: Maybe; + nodes: Array; +}; + +export type Pre_Commit_Aggregate_Bool_Exp = { + count?: InputMaybe; +}; + +export type Pre_Commit_Aggregate_Bool_Exp_Count = { + arguments?: InputMaybe>; + distinct?: InputMaybe; + filter?: InputMaybe; + predicate: Int_Comparison_Exp; +}; + +/** aggregate fields of "pre_commit" */ +export type Pre_Commit_Aggregate_Fields = { + __typename?: 'pre_commit_aggregate_fields'; + avg?: Maybe; + count: Scalars['Int']; + max?: Maybe; + min?: Maybe; + stddev?: Maybe; + stddev_pop?: Maybe; + stddev_samp?: Maybe; + sum?: Maybe; + var_pop?: Maybe; + var_samp?: Maybe; + variance?: Maybe; +}; + + +/** aggregate fields of "pre_commit" */ +export type Pre_Commit_Aggregate_FieldsCountArgs = { + columns?: InputMaybe>; + distinct?: InputMaybe; +}; + +/** order by aggregate values of table "pre_commit" */ +export type Pre_Commit_Aggregate_Order_By = { + avg?: InputMaybe; + count?: InputMaybe; + max?: InputMaybe; + min?: InputMaybe; + stddev?: InputMaybe; + stddev_pop?: InputMaybe; + stddev_samp?: InputMaybe; + sum?: InputMaybe; + var_pop?: InputMaybe; + var_samp?: InputMaybe; + variance?: InputMaybe; +}; + +/** aggregate avg on columns */ +export type Pre_Commit_Avg_Fields = { + __typename?: 'pre_commit_avg_fields'; + height?: Maybe; + proposer_priority?: Maybe; + voting_power?: Maybe; +}; + +/** order by avg() on columns of table "pre_commit" */ +export type Pre_Commit_Avg_Order_By = { + height?: InputMaybe; + proposer_priority?: InputMaybe; + voting_power?: InputMaybe; +}; + +/** Boolean expression to filter rows from the table "pre_commit". All fields are combined with a logical 'AND'. */ +export type Pre_Commit_Bool_Exp = { + _and?: InputMaybe>; + _not?: InputMaybe; + _or?: InputMaybe>; + height?: InputMaybe; + proposer_priority?: InputMaybe; + timestamp?: InputMaybe; + validator?: InputMaybe; + validator_address?: InputMaybe; + voting_power?: InputMaybe; +}; + +/** aggregate max on columns */ +export type Pre_Commit_Max_Fields = { + __typename?: 'pre_commit_max_fields'; + height?: Maybe; + proposer_priority?: Maybe; + timestamp?: Maybe; + validator_address?: Maybe; + voting_power?: Maybe; +}; + +/** order by max() on columns of table "pre_commit" */ +export type Pre_Commit_Max_Order_By = { + height?: InputMaybe; + proposer_priority?: InputMaybe; + timestamp?: InputMaybe; + validator_address?: InputMaybe; + voting_power?: InputMaybe; +}; + +/** aggregate min on columns */ +export type Pre_Commit_Min_Fields = { + __typename?: 'pre_commit_min_fields'; + height?: Maybe; + proposer_priority?: Maybe; + timestamp?: Maybe; + validator_address?: Maybe; + voting_power?: Maybe; +}; + +/** order by min() on columns of table "pre_commit" */ +export type Pre_Commit_Min_Order_By = { + height?: InputMaybe; + proposer_priority?: InputMaybe; + timestamp?: InputMaybe; + validator_address?: InputMaybe; + voting_power?: InputMaybe; +}; + +/** Ordering options when selecting data from "pre_commit". */ +export type Pre_Commit_Order_By = { + height?: InputMaybe; + proposer_priority?: InputMaybe; + timestamp?: InputMaybe; + validator?: InputMaybe; + validator_address?: InputMaybe; + voting_power?: InputMaybe; +}; + +/** select columns of table "pre_commit" */ +export enum Pre_Commit_Select_Column { + /** column name */ + Height = 'height', + /** column name */ + ProposerPriority = 'proposer_priority', + /** column name */ + Timestamp = 'timestamp', + /** column name */ + ValidatorAddress = 'validator_address', + /** column name */ + VotingPower = 'voting_power' +} + +/** aggregate stddev on columns */ +export type Pre_Commit_Stddev_Fields = { + __typename?: 'pre_commit_stddev_fields'; + height?: Maybe; + proposer_priority?: Maybe; + voting_power?: Maybe; +}; + +/** order by stddev() on columns of table "pre_commit" */ +export type Pre_Commit_Stddev_Order_By = { + height?: InputMaybe; + proposer_priority?: InputMaybe; + voting_power?: InputMaybe; +}; + +/** aggregate stddev_pop on columns */ +export type Pre_Commit_Stddev_Pop_Fields = { + __typename?: 'pre_commit_stddev_pop_fields'; + height?: Maybe; + proposer_priority?: Maybe; + voting_power?: Maybe; +}; + +/** order by stddev_pop() on columns of table "pre_commit" */ +export type Pre_Commit_Stddev_Pop_Order_By = { + height?: InputMaybe; + proposer_priority?: InputMaybe; + voting_power?: InputMaybe; +}; + +/** aggregate stddev_samp on columns */ +export type Pre_Commit_Stddev_Samp_Fields = { + __typename?: 'pre_commit_stddev_samp_fields'; + height?: Maybe; + proposer_priority?: Maybe; + voting_power?: Maybe; +}; + +/** order by stddev_samp() on columns of table "pre_commit" */ +export type Pre_Commit_Stddev_Samp_Order_By = { + height?: InputMaybe; + proposer_priority?: InputMaybe; + voting_power?: InputMaybe; +}; + +/** Streaming cursor of the table "pre_commit" */ +export type Pre_Commit_Stream_Cursor_Input = { + /** Stream column input with initial value */ + initial_value: Pre_Commit_Stream_Cursor_Value_Input; + /** cursor ordering */ + ordering?: InputMaybe; +}; + +/** Initial value of the column from where the streaming should start */ +export type Pre_Commit_Stream_Cursor_Value_Input = { + height?: InputMaybe; + proposer_priority?: InputMaybe; + timestamp?: InputMaybe; + validator_address?: InputMaybe; + voting_power?: InputMaybe; +}; + +/** aggregate sum on columns */ +export type Pre_Commit_Sum_Fields = { + __typename?: 'pre_commit_sum_fields'; + height?: Maybe; + proposer_priority?: Maybe; + voting_power?: Maybe; +}; + +/** order by sum() on columns of table "pre_commit" */ +export type Pre_Commit_Sum_Order_By = { + height?: InputMaybe; + proposer_priority?: InputMaybe; + voting_power?: InputMaybe; +}; + +/** aggregate var_pop on columns */ +export type Pre_Commit_Var_Pop_Fields = { + __typename?: 'pre_commit_var_pop_fields'; + height?: Maybe; + proposer_priority?: Maybe; + voting_power?: Maybe; +}; + +/** order by var_pop() on columns of table "pre_commit" */ +export type Pre_Commit_Var_Pop_Order_By = { + height?: InputMaybe; + proposer_priority?: InputMaybe; + voting_power?: InputMaybe; +}; + +/** aggregate var_samp on columns */ +export type Pre_Commit_Var_Samp_Fields = { + __typename?: 'pre_commit_var_samp_fields'; + height?: Maybe; + proposer_priority?: Maybe; + voting_power?: Maybe; +}; + +/** order by var_samp() on columns of table "pre_commit" */ +export type Pre_Commit_Var_Samp_Order_By = { + height?: InputMaybe; + proposer_priority?: InputMaybe; + voting_power?: InputMaybe; +}; + +/** aggregate variance on columns */ +export type Pre_Commit_Variance_Fields = { + __typename?: 'pre_commit_variance_fields'; + height?: Maybe; + proposer_priority?: Maybe; + voting_power?: Maybe; +}; + +/** order by variance() on columns of table "pre_commit" */ +export type Pre_Commit_Variance_Order_By = { + height?: InputMaybe; + proposer_priority?: InputMaybe; + voting_power?: InputMaybe; +}; + +export type Query_Root = { + __typename?: 'query_root'; + /** fetch data from the table: "block" */ + block: Array; + /** fetch data from the table: "block" using primary key columns */ + block_by_pk?: Maybe; + /** fetch data from the table: "message" */ + message: Array; + /** fetch data from the table: "pre_commit" */ + pre_commit: Array; + /** fetch aggregated fields from the table: "pre_commit" */ + pre_commit_aggregate: Pre_Commit_Aggregate; + /** fetch data from the table: "transaction" */ + transaction: Array; + /** fetch data from the table: "validator" */ + validator: Array; + /** fetch data from the table: "validator" using primary key columns */ + validator_by_pk?: Maybe; +}; + + +export type Query_RootBlockArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Query_RootBlock_By_PkArgs = { + height: Scalars['bigint']; +}; + + +export type Query_RootMessageArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Query_RootPre_CommitArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Query_RootPre_Commit_AggregateArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Query_RootTransactionArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Query_RootValidatorArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Query_RootValidator_By_PkArgs = { + consensus_address: Scalars['String']; +}; + +export type Subscription_Root = { + __typename?: 'subscription_root'; + /** fetch data from the table: "block" */ + block: Array; + /** fetch data from the table: "block" using primary key columns */ + block_by_pk?: Maybe; + /** fetch data from the table in a streaming manner: "block" */ + block_stream: Array; + /** fetch data from the table: "message" */ + message: Array; + /** fetch data from the table in a streaming manner: "message" */ + message_stream: Array; + /** fetch data from the table: "pre_commit" */ + pre_commit: Array; + /** fetch aggregated fields from the table: "pre_commit" */ + pre_commit_aggregate: Pre_Commit_Aggregate; + /** fetch data from the table in a streaming manner: "pre_commit" */ + pre_commit_stream: Array; + /** fetch data from the table: "transaction" */ + transaction: Array; + /** fetch data from the table in a streaming manner: "transaction" */ + transaction_stream: Array; + /** fetch data from the table: "validator" */ + validator: Array; + /** fetch data from the table: "validator" using primary key columns */ + validator_by_pk?: Maybe; + /** fetch data from the table in a streaming manner: "validator" */ + validator_stream: Array; +}; + + +export type Subscription_RootBlockArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Subscription_RootBlock_By_PkArgs = { + height: Scalars['bigint']; +}; + + +export type Subscription_RootBlock_StreamArgs = { + batch_size: Scalars['Int']; + cursor: Array>; + where?: InputMaybe; +}; + + +export type Subscription_RootMessageArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Subscription_RootMessage_StreamArgs = { + batch_size: Scalars['Int']; + cursor: Array>; + where?: InputMaybe; +}; + + +export type Subscription_RootPre_CommitArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Subscription_RootPre_Commit_AggregateArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Subscription_RootPre_Commit_StreamArgs = { + batch_size: Scalars['Int']; + cursor: Array>; + where?: InputMaybe; +}; + + +export type Subscription_RootTransactionArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Subscription_RootTransaction_StreamArgs = { + batch_size: Scalars['Int']; + cursor: Array>; + where?: InputMaybe; +}; + + +export type Subscription_RootValidatorArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +export type Subscription_RootValidator_By_PkArgs = { + consensus_address: Scalars['String']; +}; + + +export type Subscription_RootValidator_StreamArgs = { + batch_size: Scalars['Int']; + cursor: Array>; + where?: InputMaybe; +}; + +/** Boolean expression to compare columns of type "timestamp". All fields are combined with logical 'AND'. */ +export type Timestamp_Comparison_Exp = { + _eq?: InputMaybe; + _gt?: InputMaybe; + _gte?: InputMaybe; + _in?: InputMaybe>; + _is_null?: InputMaybe; + _lt?: InputMaybe; + _lte?: InputMaybe; + _neq?: InputMaybe; + _nin?: InputMaybe>; +}; + +/** columns and relationships of "transaction" */ +export type Transaction = { + __typename?: 'transaction'; + /** An object relationship */ + block?: Maybe; + gas_used?: Maybe; + gas_wanted?: Maybe; + hash: Scalars['String']; + height: Scalars['bigint']; + memo?: Maybe; + raw_log?: Maybe; + success: Scalars['Boolean']; +}; + +/** order by aggregate values of table "transaction" */ +export type Transaction_Aggregate_Order_By = { + avg?: InputMaybe; + count?: InputMaybe; + max?: InputMaybe; + min?: InputMaybe; + stddev?: InputMaybe; + stddev_pop?: InputMaybe; + stddev_samp?: InputMaybe; + sum?: InputMaybe; + var_pop?: InputMaybe; + var_samp?: InputMaybe; + variance?: InputMaybe; +}; + +/** order by avg() on columns of table "transaction" */ +export type Transaction_Avg_Order_By = { + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + height?: InputMaybe; +}; + +/** Boolean expression to filter rows from the table "transaction". All fields are combined with a logical 'AND'. */ +export type Transaction_Bool_Exp = { + _and?: InputMaybe>; + _not?: InputMaybe; + _or?: InputMaybe>; + block?: InputMaybe; + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + hash?: InputMaybe; + height?: InputMaybe; + memo?: InputMaybe; + raw_log?: InputMaybe; + success?: InputMaybe; +}; + +/** order by max() on columns of table "transaction" */ +export type Transaction_Max_Order_By = { + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + hash?: InputMaybe; + height?: InputMaybe; + memo?: InputMaybe; + raw_log?: InputMaybe; +}; + +/** order by min() on columns of table "transaction" */ +export type Transaction_Min_Order_By = { + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + hash?: InputMaybe; + height?: InputMaybe; + memo?: InputMaybe; + raw_log?: InputMaybe; +}; + +/** Ordering options when selecting data from "transaction". */ +export type Transaction_Order_By = { + block?: InputMaybe; + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + hash?: InputMaybe; + height?: InputMaybe; + memo?: InputMaybe; + raw_log?: InputMaybe; + success?: InputMaybe; +}; + +/** select columns of table "transaction" */ +export enum Transaction_Select_Column { + /** column name */ + GasUsed = 'gas_used', + /** column name */ + GasWanted = 'gas_wanted', + /** column name */ + Hash = 'hash', + /** column name */ + Height = 'height', + /** column name */ + Memo = 'memo', + /** column name */ + RawLog = 'raw_log', + /** column name */ + Success = 'success' +} + +/** order by stddev() on columns of table "transaction" */ +export type Transaction_Stddev_Order_By = { + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + height?: InputMaybe; +}; + +/** order by stddev_pop() on columns of table "transaction" */ +export type Transaction_Stddev_Pop_Order_By = { + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + height?: InputMaybe; +}; + +/** order by stddev_samp() on columns of table "transaction" */ +export type Transaction_Stddev_Samp_Order_By = { + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + height?: InputMaybe; +}; + +/** Streaming cursor of the table "transaction" */ +export type Transaction_Stream_Cursor_Input = { + /** Stream column input with initial value */ + initial_value: Transaction_Stream_Cursor_Value_Input; + /** cursor ordering */ + ordering?: InputMaybe; +}; + +/** Initial value of the column from where the streaming should start */ +export type Transaction_Stream_Cursor_Value_Input = { + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + hash?: InputMaybe; + height?: InputMaybe; + memo?: InputMaybe; + raw_log?: InputMaybe; + success?: InputMaybe; +}; + +/** order by sum() on columns of table "transaction" */ +export type Transaction_Sum_Order_By = { + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + height?: InputMaybe; +}; + +/** order by var_pop() on columns of table "transaction" */ +export type Transaction_Var_Pop_Order_By = { + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + height?: InputMaybe; +}; + +/** order by var_samp() on columns of table "transaction" */ +export type Transaction_Var_Samp_Order_By = { + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + height?: InputMaybe; +}; + +/** order by variance() on columns of table "transaction" */ +export type Transaction_Variance_Order_By = { + gas_used?: InputMaybe; + gas_wanted?: InputMaybe; + height?: InputMaybe; +}; + +/** columns and relationships of "validator" */ +export type Validator = { + __typename?: 'validator'; + /** An array relationship */ + blocks: Array; + consensus_address: Scalars['String']; + consensus_pubkey: Scalars['String']; + /** An array relationship */ + pre_commits: Array; + /** An aggregate relationship */ + pre_commits_aggregate: Pre_Commit_Aggregate; +}; + + +/** columns and relationships of "validator" */ +export type ValidatorBlocksArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +/** columns and relationships of "validator" */ +export type ValidatorPre_CommitsArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + + +/** columns and relationships of "validator" */ +export type ValidatorPre_Commits_AggregateArgs = { + distinct_on?: InputMaybe>; + limit?: InputMaybe; + offset?: InputMaybe; + order_by?: InputMaybe>; + where?: InputMaybe; +}; + +/** Boolean expression to filter rows from the table "validator". All fields are combined with a logical 'AND'. */ +export type Validator_Bool_Exp = { + _and?: InputMaybe>; + _not?: InputMaybe; + _or?: InputMaybe>; + blocks?: InputMaybe; + consensus_address?: InputMaybe; + consensus_pubkey?: InputMaybe; + pre_commits?: InputMaybe; + pre_commits_aggregate?: InputMaybe; +}; + +/** Ordering options when selecting data from "validator". */ +export type Validator_Order_By = { + blocks_aggregate?: InputMaybe; + consensus_address?: InputMaybe; + consensus_pubkey?: InputMaybe; + pre_commits_aggregate?: InputMaybe; +}; + +/** select columns of table "validator" */ +export enum Validator_Select_Column { + /** column name */ + ConsensusAddress = 'consensus_address', + /** column name */ + ConsensusPubkey = 'consensus_pubkey' +} + +/** Streaming cursor of the table "validator" */ +export type Validator_Stream_Cursor_Input = { + /** Stream column input with initial value */ + initial_value: Validator_Stream_Cursor_Value_Input; + /** cursor ordering */ + ordering?: InputMaybe; +}; + +/** Initial value of the column from where the streaming should start */ +export type Validator_Stream_Cursor_Value_Input = { + consensus_address?: InputMaybe; + consensus_pubkey?: InputMaybe; +}; + +export type BlockDetailsQueryVariables = Exact<{ + height?: InputMaybe; +}>; + + +export type BlockDetailsQuery = { block: Array<{ __typename?: 'block', height: any, hash: string, timestamp: any, txs?: number | null, proposerAddress?: string | null }> }; + +export type BlocksQueryVariables = Exact<{ + limit?: InputMaybe; + offset?: InputMaybe; +}>; + + +export type BlocksQuery = { blocks: Array<{ __typename?: 'block', height: any, hash: string, timestamp: any, txs?: number | null, proposerAddress?: string | null }> }; + + +export const BlockDetailsDocument = gql` + query BlockDetails($height: bigint) { + block(limit: 1, where: {height: {_eq: $height}}) { + height + hash + timestamp + txs: num_txs + proposerAddress: proposer_address + } +} + `; + +/** + * __useBlockDetailsQuery__ + * + * To run a query within a React component, call `useBlockDetailsQuery` and pass it any options that fit your needs. + * When your component renders, `useBlockDetailsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useBlockDetailsQuery({ + * variables: { + * height: // value for 'height' + * }, + * }); + */ +export function useBlockDetailsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(BlockDetailsDocument, options); + } +export function useBlockDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(BlockDetailsDocument, options); + } +export type BlockDetailsQueryHookResult = ReturnType; +export type BlockDetailsLazyQueryHookResult = ReturnType; +export type BlockDetailsQueryResult = Apollo.QueryResult; +export const BlocksDocument = gql` + query Blocks($limit: Int = 7, $offset: Int = 0) { + blocks: block(limit: $limit, offset: $offset, order_by: {height: desc}) { + height + txs: num_txs + hash + timestamp + proposerAddress: proposer_address + } +} + `; + +/** + * __useBlocksQuery__ + * + * To run a query within a React component, call `useBlocksQuery` and pass it any options that fit your needs. + * When your component renders, `useBlocksQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useBlocksQuery({ + * variables: { + * limit: // value for 'limit' + * offset: // value for 'offset' + * }, + * }); + */ +export function useBlocksQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(BlocksDocument, options); + } +export function useBlocksLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(BlocksDocument, options); + } +export type BlocksQueryHookResult = ReturnType; +export type BlocksLazyQueryHookResult = ReturnType; +export type BlocksQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/web-namada/src/pages/404.tsx b/apps/web-namada/src/pages/404.tsx new file mode 100644 index 0000000000..76cf2cc94d --- /dev/null +++ b/apps/web-namada/src/pages/404.tsx @@ -0,0 +1,10 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import NotFound from '@/screens/404'; +import type { NextPage } from 'next'; +import nextI18NextConfig from '../../next-i18next.config'; + +const Custom404: NextPage = () => ; + +export const getStaticProps = withGetStaticProps(nextI18NextConfig); + +export default Custom404; diff --git a/apps/web-namada/src/pages/[dtag].tsx b/apps/web-namada/src/pages/[dtag].tsx new file mode 100644 index 0000000000..cb8769bdd0 --- /dev/null +++ b/apps/web-namada/src/pages/[dtag].tsx @@ -0,0 +1,11 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import ProfileDetails from '@/screens/profile_details'; +import type { NextPage } from 'next'; +import nextI18NextConfig from '../../next-i18next.config'; + +const ProfileDetailsPage: NextPage = () => ; + +export const getStaticPaths = () => ({ paths: [], fallback: 'blocking' }); +export const getStaticProps = withGetStaticProps(nextI18NextConfig, 'profiles', 'accounts'); + +export default ProfileDetailsPage; diff --git a/apps/web-namada/src/pages/_app.tsx b/apps/web-namada/src/pages/_app.tsx new file mode 100644 index 0000000000..254596e760 --- /dev/null +++ b/apps/web-namada/src/pages/_app.tsx @@ -0,0 +1,7 @@ +import MyApp from '@/screens/app'; +import { appWithTranslation } from 'next-i18next'; +import 'react-toastify/dist/ReactToastify.css'; +import 'shared-utils/assets/styles/global.css'; +import nextI18NextConfig from '../../next-i18next.config'; + +export default appWithTranslation(MyApp, nextI18NextConfig); diff --git a/apps/web-namada/src/pages/_document.tsx b/apps/web-namada/src/pages/_document.tsx new file mode 100644 index 0000000000..174b41f650 --- /dev/null +++ b/apps/web-namada/src/pages/_document.tsx @@ -0,0 +1,12 @@ +import Document from 'next/document'; +import DocumentComponent, { getInitialProps, DocumentComponentProps } from 'ui/pages/_document'; + +class MyDocument extends Document { + render() { + return ; + } +} + +MyDocument.getInitialProps = getInitialProps; + +export default MyDocument; diff --git a/apps/web-namada/src/pages/_error.tsx b/apps/web-namada/src/pages/_error.tsx new file mode 100644 index 0000000000..2cac4132ab --- /dev/null +++ b/apps/web-namada/src/pages/_error.tsx @@ -0,0 +1,8 @@ +import type { NextPage } from 'next'; +import ErrorPage, { getInitialProps } from 'ui/pages/_error'; + +const MyError: NextPage = () => ; + +MyError.getInitialProps = getInitialProps; + +export default MyError; diff --git a/apps/web-namada/src/pages/accounts/[address].tsx b/apps/web-namada/src/pages/accounts/[address].tsx new file mode 100644 index 0000000000..2eb26b1ac2 --- /dev/null +++ b/apps/web-namada/src/pages/accounts/[address].tsx @@ -0,0 +1,18 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import type { NextPage } from 'next'; +import AccountDetails from '@/screens/account_details'; +import nextI18NextConfig from '../../../next-i18next.config'; + +const AccountDetailsPage: NextPage = () => ; + +export const getStaticPaths = () => ({ paths: [], fallback: 'blocking' }); +export const getStaticProps = withGetStaticProps( + nextI18NextConfig, + 'accounts', + 'transactions', + 'validators', + 'message_labels', + 'message_contents' +); + +export default AccountDetailsPage; diff --git a/apps/web-namada/src/pages/blocks/[height].tsx b/apps/web-namada/src/pages/blocks/[height].tsx new file mode 100644 index 0000000000..269c546dd5 --- /dev/null +++ b/apps/web-namada/src/pages/blocks/[height].tsx @@ -0,0 +1,17 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import type { NextPage } from 'next'; +import BlockDetails from '@/screens/block_details'; +import nextI18NextConfig from '../../../next-i18next.config'; + +const BlockDetailsPage: NextPage = () => ; + +export const getStaticPaths = () => ({ paths: [], fallback: 'blocking' }); +export const getStaticProps = withGetStaticProps( + nextI18NextConfig, + 'blocks', + 'transactions', + 'message_labels', + 'message_contents' +); + +export default BlockDetailsPage; diff --git a/apps/web-namada/src/pages/blocks/index.tsx b/apps/web-namada/src/pages/blocks/index.tsx new file mode 100644 index 0000000000..73a2e27d4c --- /dev/null +++ b/apps/web-namada/src/pages/blocks/index.tsx @@ -0,0 +1,16 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import type { NextPage } from 'next'; +import Blocks from '@/screens/blocks'; +import nextI18NextConfig from '../../../next-i18next.config'; + +const BlocksPage: NextPage = () => ; + +export const getStaticProps = withGetStaticProps( + nextI18NextConfig, + 'blocks', + 'transactions', + 'message_labels', + 'message_contents' +); + +export default BlocksPage; diff --git a/apps/web-namada/src/pages/index.tsx b/apps/web-namada/src/pages/index.tsx new file mode 100644 index 0000000000..7e8b5638e2 --- /dev/null +++ b/apps/web-namada/src/pages/index.tsx @@ -0,0 +1,15 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import type { NextPage } from 'next'; +import Home from '@/screens/home'; +import nextI18NextConfig from '../../next-i18next.config'; + +const HomePage: NextPage = () => ; + +export const getStaticProps = withGetStaticProps( + nextI18NextConfig, + 'home', + 'blocks', + 'transactions' +); + +export default HomePage; diff --git a/apps/web-namada/src/pages/params/index.tsx b/apps/web-namada/src/pages/params/index.tsx new file mode 100644 index 0000000000..36c5df67e5 --- /dev/null +++ b/apps/web-namada/src/pages/params/index.tsx @@ -0,0 +1,10 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import Params from '@/screens/params'; +import type { NextPage } from 'next'; +import nextI18NextConfig from '../../../next-i18next.config'; + +const ParamsPage: NextPage = () => ; + +export const getStaticProps = withGetStaticProps(nextI18NextConfig, 'params'); + +export default ParamsPage; diff --git a/apps/web-namada/src/pages/proposals/[id].tsx b/apps/web-namada/src/pages/proposals/[id].tsx new file mode 100644 index 0000000000..bd370d9b35 --- /dev/null +++ b/apps/web-namada/src/pages/proposals/[id].tsx @@ -0,0 +1,11 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import type { NextPage } from 'next'; +import ProposalDetails from '@/screens/proposal_details'; +import nextI18NextConfig from '../../../next-i18next.config'; + +const TokenDetailsPage: NextPage = () => ; + +export const getStaticPaths = () => ({ paths: [], fallback: 'blocking' }); +export const getStaticProps = withGetStaticProps(nextI18NextConfig, 'proposals'); + +export default TokenDetailsPage; diff --git a/apps/web-namada/src/pages/proposals/index.tsx b/apps/web-namada/src/pages/proposals/index.tsx new file mode 100644 index 0000000000..d4ff03ed63 --- /dev/null +++ b/apps/web-namada/src/pages/proposals/index.tsx @@ -0,0 +1,10 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import type { NextPage } from 'next'; +import Tokens from '@/screens/proposals'; +import nextI18NextConfig from '../../../next-i18next.config'; + +const TokensPage: NextPage = () => ; + +export const getStaticProps = withGetStaticProps(nextI18NextConfig, 'proposals'); + +export default TokensPage; diff --git a/apps/web-namada/src/pages/server-sitemap.xml/index.tsx b/apps/web-namada/src/pages/server-sitemap.xml/index.tsx new file mode 100644 index 0000000000..9d27abd6ad --- /dev/null +++ b/apps/web-namada/src/pages/server-sitemap.xml/index.tsx @@ -0,0 +1,8 @@ +import ServerSitemap, { getServerSideProps } from 'ui/pages/server-sitemap.xml'; + +// This function is called by Next.js before rendering the page. It returns +// a list of URLs that should be included in the sitemap. +export { getServerSideProps }; + +// Next.js calls this function to render the page. +export default ServerSitemap; diff --git a/apps/web-namada/src/pages/transactions/[tx].tsx b/apps/web-namada/src/pages/transactions/[tx].tsx new file mode 100644 index 0000000000..500faa8fa1 --- /dev/null +++ b/apps/web-namada/src/pages/transactions/[tx].tsx @@ -0,0 +1,16 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import type { NextPage } from 'next'; +import TransactionDetails from '@/screens/transaction_details'; +import nextI18NextConfig from '../../../next-i18next.config'; + +const TransactionDetailsPage: NextPage = () => ; + +export const getStaticPaths = () => ({ paths: [], fallback: 'blocking' }); +export const getStaticProps = withGetStaticProps( + nextI18NextConfig, + 'transactions', + 'message_labels', + 'message_contents' +); + +export default TransactionDetailsPage; diff --git a/apps/web-namada/src/pages/transactions/index.tsx b/apps/web-namada/src/pages/transactions/index.tsx new file mode 100644 index 0000000000..b68e2676eb --- /dev/null +++ b/apps/web-namada/src/pages/transactions/index.tsx @@ -0,0 +1,15 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import type { NextPage } from 'next'; +import Transactions from '@/screens/transactions'; +import nextI18NextConfig from '../../../next-i18next.config'; + +const TransactionsPage: NextPage = () => ; + +export const getStaticProps = withGetStaticProps( + nextI18NextConfig, + 'transactions', + 'message_labels', + 'message_contents' +); + +export default TransactionsPage; diff --git a/apps/web-namada/src/pages/validators/[address].tsx b/apps/web-namada/src/pages/validators/[address].tsx new file mode 100644 index 0000000000..657b818c10 --- /dev/null +++ b/apps/web-namada/src/pages/validators/[address].tsx @@ -0,0 +1,18 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import ValidatorDetails from '@/screens/validator_details'; +import type { NextPage } from 'next'; +import nextI18NextConfig from '../../../next-i18next.config'; + +const ValidatorDetailsPage: NextPage = () => ; + +export const getStaticPaths = () => ({ paths: [], fallback: 'blocking' }); +export const getStaticProps = withGetStaticProps( + nextI18NextConfig, + 'validators', + 'transactions', + 'accounts', + 'message_labels', + 'message_contents' +); + +export default ValidatorDetailsPage; diff --git a/apps/web-namada/src/pages/validators/index.tsx b/apps/web-namada/src/pages/validators/index.tsx new file mode 100644 index 0000000000..7ce3ee119a --- /dev/null +++ b/apps/web-namada/src/pages/validators/index.tsx @@ -0,0 +1,17 @@ +import withGetStaticProps from '@/pages/withGetStaticProps'; +import Validators from '@/screens/validators'; +import type { NextPage } from 'next'; +import nextI18NextConfig from '../../../next-i18next.config'; + +const ValidatorsPage: NextPage = () => ; + +export const getStaticProps = withGetStaticProps( + nextI18NextConfig, + 'validators', + 'transactions', + 'accounts', + 'message_labels', + 'message_contents' +); + +export default ValidatorsPage; diff --git a/apps/web-namada/src/recoil/market/hooks.ts b/apps/web-namada/src/recoil/market/hooks.ts new file mode 100644 index 0000000000..b751bec19d --- /dev/null +++ b/apps/web-namada/src/recoil/market/hooks.ts @@ -0,0 +1,73 @@ +import Big from 'big.js'; +import numeral from 'numeral'; +import { SetterOrUpdater, useRecoilState } from 'recoil'; +import chainConfig from '@/chainConfig'; +import { MarketDataQuery, useMarketDataQuery } from '@/graphql/types/general_types'; +import { writeMarket } from '@/recoil/market/selectors'; +import type { AtomState } from '@/recoil/market/types'; +import { formatToken } from '@/utils/format_token'; +import getCurrentInflationAmount from '@/utils/get_current_inflation'; +import { getDenom } from '@/utils/get_denom'; + +const { primaryTokenUnit, tokenUnits } = chainConfig(); + +export function useMarketRecoil() { + const [market, setMarket] = useRecoilState(writeMarket) as [ + AtomState, + SetterOrUpdater, + ]; + + useMarketDataQuery?.({ + variables: { + denom: tokenUnits?.[primaryTokenUnit]?.display, + }, + onCompleted: (data) => { + if (data) { + setMarket(formatUseChainIdQuery(data)); + } + }, + }); + + function formatUseChainIdQuery(data: MarketDataQuery): AtomState { + let { communityPool, price, marketCap } = market; + + if (data?.tokenPrice?.length) { + price = numeral(numeral(data?.tokenPrice[0]?.price).format('0.[00]', Math.floor)).value(); + marketCap = data.tokenPrice[0]?.marketCap; + } + + const [communityPoolCoin] = + (data?.communityPool?.[0]?.coins as MsgCoin[])?.filter((x) => x.denom === primaryTokenUnit) ?? + []; + const inflation = data?.inflation?.[0]?.value ?? 0; + + const rawSupplyAmount = getDenom(data?.supply?.[0]?.coins, primaryTokenUnit).amount; + const supply = formatToken(rawSupplyAmount, primaryTokenUnit); + + if (communityPoolCoin) { + communityPool = formatToken(communityPoolCoin.amount, communityPoolCoin.denom); + } + + // Get the annual inflation amount of current inflation shedule + const inflationSchedules = data?.mintParams?.[0]?.params?.inflation_schedules ?? []; + const inflationAmount = getCurrentInflationAmount(inflationSchedules); + + const bondedTokens = data?.bondedTokens?.[0]?.bonded_tokens ?? 1; + + // The annual token provision distributed to staking is 6.25% => this number is obtained from cummunity telegram + const stakingDistribution = 0.0625; + + const apr = bondedTokens + ? Big(inflationAmount)?.times(stakingDistribution).div(bondedTokens).toNumber() + : 0; + + return { + price, + supply, + marketCap, + inflation, + communityPool, + apr, + }; + } +} diff --git a/apps/web-namada/src/screens/block_details/hooks.ts b/apps/web-namada/src/screens/block_details/hooks.ts new file mode 100644 index 0000000000..8d72571076 --- /dev/null +++ b/apps/web-namada/src/screens/block_details/hooks.ts @@ -0,0 +1,133 @@ +import { useRouter } from 'next/router'; +import numeral from 'numeral'; +import * as R from 'ramda'; +import { useCallback, useEffect, useState } from 'react'; +import { convertMsgsToModels } from '@/components/msg/utils'; +import { BlockDetailsQuery, useBlockDetailsQuery } from '@/graphql/types/general_types'; +import type { BlockDetailState } from '@/screens/block_details/types'; +import { convertMsgType } from '@/utils/convert_msg_type'; + +export const useBlockDetails = () => { + const router = useRouter(); + const [state, setState] = useState({ + loading: true, + exists: true, + overview: { + height: 0, + hash: '', + txs: 0, + timestamp: '', + proposer: '', + }, + signatures: [], + transactions: [], + }); + + const handleSetState = useCallback( + (stateChange: (prevState: BlockDetailState) => BlockDetailState) => { + setState((prevState) => { + const newState = stateChange(prevState); + return R.equals(prevState, newState) ? prevState : newState; + }); + }, + [] + ); + + // ========================== + // Fetch Data + // ========================== + useBlockDetailsQuery?.({ + variables: { + height: numeral(router.query.height).value(), + // signatureHeight: (numeral(router.query.height).value() ?? 0) + 1, + }, + onCompleted: (data) => { + handleSetState((prevState) => ({ ...prevState, ...formatRaws(data) })); + }, + }) || {}; + + useEffect(() => { + // reset every call + handleSetState((prevState) => ({ + ...prevState, + loading: true, + exists: true, + })); + }, [handleSetState]); + + return { + state, + }; +}; + +// ========================== +// Overview +// ========================== +const formatOverview = (data: BlockDetailsQuery) => { + const proposerAddress = + data?.block?.[0]?.validator?.validatorInfo?.operatorAddress ?? + data?.block?.[0]?.proposerAddress ?? + ''; + const overview = { + height: data.block?.[0].height, + hash: data.block?.[0].hash, + txs: data.block?.[0].txs ?? 0, + timestamp: data.block?.[0].timestamp, + proposer: proposerAddress, + }; + return overview; +}; + +// ========================== +// Signatures +// ========================== +const formatSignatures = (data: BlockDetailsQuery) => { + const signatures = (data.preCommits || []) + .filter((x) => x?.validator?.validatorInfo) + .map((x) => x?.validator?.validatorInfo?.operatorAddress ?? ''); + return signatures; +}; + +// ========================== +// Transactions +// ========================== +const formatTransactions = (data: BlockDetailsQuery, stateChange: Partial) => { + const transactions = (data.transaction || []).map((x) => { + const messages = convertMsgsToModels(x); + const msgType = messages.map((eachMsg) => { + const eachMsgType = eachMsg?.type ?? 'none type'; + return eachMsgType ?? ''; + }); + const convertedMsgType = convertMsgType(msgType); + return { + type: convertedMsgType, + height: x.height, + hash: x.hash, + success: x.success, + timestamp: stateChange.overview?.timestamp ?? '', + messages: { + count: x.messages.length, + items: messages, + }, + }; + }); + + return transactions; +}; + +function formatRaws(data: BlockDetailsQuery) { + const stateChange: Partial = { + loading: false, + }; + + if (!data.block?.length) { + stateChange.exists = false; + return stateChange; + } + + stateChange.overview = formatOverview(data); + stateChange.signatures = formatSignatures(data); + stateChange.transactions = formatTransactions(data, stateChange); + + return stateChange; +} diff --git a/apps/web-namada/src/screens/blocks/hooks.ts b/apps/web-namada/src/screens/blocks/hooks.ts new file mode 100644 index 0000000000..8a23bdf249 --- /dev/null +++ b/apps/web-namada/src/screens/blocks/hooks.ts @@ -0,0 +1,140 @@ +import * as R from 'ramda'; +import { useCallback, useState } from 'react'; +import { + BlocksListenerSubscription, + useBlocksListenerSubscription, + useBlocksQuery, +} from '@/graphql/types/general_types'; +import type { BlocksState, BlockType } from '@/screens/blocks/types'; + +// This is a bandaid as it can get extremely +// expensive if there is too much data +/** + * Helps remove any possible duplication + * and sorts by height in case it bugs out + */ +const uniqueAndSort = R.pipe( + R.uniqBy((r: BlockType) => r?.height), + R.sort(R.descend((r) => r?.height)) +); + +const formatBlocks = (data: BlocksListenerSubscription): BlockType[] => { + let formattedData = data.blocks; + if (data.blocks.length === 51) { + formattedData = data.blocks.slice(0, 51); + } + return ( + formattedData?.map((x) => { + const proposerAddress = + x?.validator?.validatorInfo?.operatorAddress ?? x.proposerAddress ?? ''; + console.log('debug: hooks.ts: proposerAddress', proposerAddress); + return { + height: x.height, + txs: x.txs ?? 0, + hash: x.hash, + timestamp: x.timestamp, + proposer: proposerAddress, + }; + }) ?? [] + ); +}; + +export const useBlocks = () => { + const [state, setState] = useState({ + loading: true, + exists: true, + items: [], + hasNextPage: false, + isNextPageLoading: true, + }); + + const handleSetState = useCallback((stateChange: (prevState: BlocksState) => BlocksState) => { + setState((prevState) => { + const newState = stateChange(prevState); + return R.equals(prevState, newState) ? prevState : newState; + }); + }, []); + + // ================================ + // block subscription + // ================================ + useBlocksListenerSubscription?.({ + variables: { + limit: 1, + offset: 0, + }, + onData: (data) => { + const newItems = uniqueAndSort([ + ...(data.data.data ? formatBlocks(data.data.data) : []), + ...state.items, + ]); + handleSetState((prevState) => ({ + ...prevState, + loading: false, + items: newItems, + })); + }, + }); + + // ================================ + // block query + // ================================ + const LIMIT = 51; + const blockQuery = + useBlocksQuery?.({ + variables: { + limit: LIMIT, + offset: 1, + }, + onCompleted: (data) => { + const itemsLength = data.blocks.length; + const newItems = uniqueAndSort([...state.items, ...formatBlocks(data)]); + handleSetState((prevState) => ({ + ...prevState, + loading: false, + items: newItems, + hasNextPage: itemsLength === 51, + isNextPageLoading: false, + })); + }, + onError: () => { + handleSetState((prevState) => ({ ...prevState, loading: false })); + }, + }) || {}; + + const loadNextPage = async () => { + handleSetState((prevState) => ({ ...prevState, isNextPageLoading: true })); + // refetch query + await blockQuery + .fetchMore({ + variables: { + offset: state.items.length, + limit: LIMIT, + }, + }) + .then(({ data }) => { + const itemsLength = data.blocks.length; + const newItems = uniqueAndSort([...state.items, ...formatBlocks(data)]); + + // set new state + handleSetState((prevState) => ({ + ...prevState, + items: newItems, + isNextPageLoading: false, + hasNextPage: itemsLength === 51, + })); + }); + }; + + const itemCount = state.hasNextPage ? state.items.length + 1 : state.items.length; + const loadMoreItems = state.isNextPageLoading ? () => null : loadNextPage; + const isItemLoaded = (index: number) => !state.hasNextPage || index < state.items.length; + + return { + state, + loadNextPage, + itemCount, + loadMoreItems, + isItemLoaded, + }; +}; diff --git a/apps/web-namada/src/screens/validator_details/components/validator_overview/index.tsx b/apps/web-namada/src/screens/validator_details/components/validator_overview/index.tsx new file mode 100644 index 0000000000..cb6b2ad699 --- /dev/null +++ b/apps/web-namada/src/screens/validator_details/components/validator_overview/index.tsx @@ -0,0 +1,208 @@ +import Box from '@/components/box'; +import ConditionExplanation from '@/components/condition_explanation'; +import InfoPopover from '@/components/info_popover'; +import Tag from '@/components/tag'; +import { useAddress } from '@/screens/validator_details/components/validator_overview/hooks'; +import useStyles from '@/screens/validator_details/components/validator_overview/styles'; +import { getCondition } from '@/screens/validator_details/components/validator_overview/utils'; +import { useDisplayStyles } from '@/styles/useSharedStyles'; +import { getMiddleEllipsis } from '@/utils/get_middle_ellipsis'; +import { getValidatorStatus } from '@/utils/get_validator_status'; +import { ACCOUNT_DETAILS } from '@/utils/go_to_page'; +import Divider from '@mui/material/Divider'; +import Typography from '@mui/material/Typography'; +import Big from 'big.js'; +import useAppTranslation from '@/hooks/useAppTranslation'; +import Link from 'next/link'; +import numeral from 'numeral'; +import { FC } from 'react'; +import CopyIcon from 'shared-utils/assets/icon-copy.svg'; +import Loading from '@/components/loading'; +import type { OverviewType, StatusType } from '@/screens/validator_details/types'; +import LiquidStakingFalseIcon from 'shared-utils/assets/liquid-staking-false.svg'; +import LiquidStakingTrueIcon from 'shared-utils/assets/liquid-staking-true.svg'; +import LiquidStakingExplanation from '@/components/liquid_staking_explanation'; + +type ValidatorOverviewProps = { + className?: string; + status: StatusType; + overview: OverviewType; + loading: boolean; +}; + +const ValidatorOverview: FC = ({ + status, + overview, + className, + loading, +}) => { + const { classes, cx } = useStyles(); + const display = useDisplayStyles().classes; + const { t } = useAppTranslation('validators'); + const { handleCopyToClipboard } = useAddress(t); + const statusTheme = getValidatorStatus(status.status, status.jailed, status.tombstoned); + const condition = getCondition(status.condition, status.status); + const { liquidStaking } = status; + + const statusItems = [ + { + key: ( + + {t('status')} + + ), + value: ( + + ), + }, + { + key: ( + + {t('liquidStaking')} + } /> + + ), + value: ( + + {liquidStaking === 'Yes' ? : } + + ), + }, + { + key: ( + + {t('commission')} + + ), + value: ( + + {`${numeral(status.commission * 100).format('0.00')}%`} + + ), + }, + { + key: ( + + {t('condition')} + } /> + + ), + value: + status.status === 3 ? ( +
+ + + {t('missedBlockCounter', { + amount: numeral(status.missedBlockCounter).format('0,0'), + })} + + + {t('signedBlockWindow', { + amount: numeral(status.signedBlockWindow).format('0,0'), + })} + + + } + display={ + + {t(condition)} + + } + /> +
+ ) : ( + + {t(condition)} + + ), + }, + { + key: ( + + {t('maxRate')} + + ), + value: ( + + {Big(status.maxRate)?.times(100).toFixed(2)}% + + ), + }, + ]; + + return ( + + {loading ? ( + + ) : ( + <> +
+
+ + {t('operatorAddress')} + +
+ handleCopyToClipboard(overview.operatorAddress)} + className={classes.actionIcons} + /> + + {overview.operatorAddress} + + {getMiddleEllipsis(overview.operatorAddress, { + beginning: 15, + ending: 5, + })} + + +
+
+ +
+ + {t('selfDelegateAddress')} + +
+ handleCopyToClipboard(overview.selfDelegateAddress)} + /> + + {overview.selfDelegateAddress} + + {getMiddleEllipsis(overview.selfDelegateAddress, { + beginning: 15, + ending: 5, + })} + + +
+
+
+ +
+ {statusItems.map((x) => ( +
+ {x.key} + {x.value} +
+ ))} +
+ + )} +
+ ); +}; + +export default ValidatorOverview; diff --git a/apps/web-namada/src/screens/validator_details/components/validator_overview/styles.ts b/apps/web-namada/src/screens/validator_details/components/validator_overview/styles.ts new file mode 100644 index 0000000000..1d9650a5db --- /dev/null +++ b/apps/web-namada/src/screens/validator_details/components/validator_overview/styles.ts @@ -0,0 +1,123 @@ +import { makeStyles } from 'tss-react/mui'; + +const useStyles = makeStyles()((theme) => ({ + addressRoot: { + [theme.breakpoints.up('md')]: { + display: 'grid', + gridTemplateColumns: 'repeat(2,1fr)', + }, + }, + actionIcons: { + '&:hover': { + cursor: 'pointer', + }, + }, + icons: { + '& svg': { + width: theme.spacing(4.5), + height: theme.spacing(4.5), + }, + }, + item: { + padding: theme.spacing(2, 0), + color: theme.palette.custom.fonts.fontTwo, + '&:first-of-type': { + paddingTop: 0, + }, + '&:last-child': { + paddingBottom: 0, + }, + '&:not(:last-child)': { + borderBottom: `solid 1px ${theme.palette.divider}`, + }, + '& .label': { + marginBottom: theme.spacing(1), + }, + '& .detail': { + '&.MuiTypography-body1': { + wordWrap: 'break-word', + }, + }, + '& a': { + color: theme.palette.custom.fonts.highlight, + }, + [theme.breakpoints.up('md')]: { + padding: 0, + '&:not(:last-child)': { + borderBottom: 'none', + }, + '& .label': { + marginBottom: 0, + }, + }, + }, + copyText: { + '& .detail': { + display: 'flex', + alignItems: 'center', + flexDirection: 'row-reverse', + justifyContent: 'flex-end', + '& svg': { + width: '1rem', + marginLeft: theme.spacing(1), + }, + }, + }, + statusRoot: { + display: 'grid', + gridTemplateColumns: 'repeat(1, 1fr)', + gap: theme.spacing(2), + [theme.breakpoints.up('md')]: { + gridTemplateColumns: 'repeat(2, 1fr)', + }, + [theme.breakpoints.up('lg')]: { + display: 'grid', + gridTemplateColumns: 'repeat(5, 1fr)', + }, + }, + statusItem: { + '& .label': { + marginBottom: theme.spacing(1), + color: theme.palette.custom.fonts.fontThree, + '&.condition': { + display: 'flex', + alignItems: 'center', + }, + '&.liquidStaking': { + display: 'flex', + alignItems: 'center', + }, + }, + '& .condition__body': { + justifySelf: 'flex-start', + }, + '& p.value': { + color: theme.palette.custom.fonts.fontTwo, + '&.good': { + color: theme.palette.custom.condition.one, + }, + '&.moderate': { + color: theme.palette.custom.condition.two, + }, + '&.bad': { + color: theme.palette.custom.condition.three, + }, + '&.condition': { + color: theme.palette.custom.condition.zero, + }, + }, + '& a': { + color: theme.palette.custom.fonts.highlight, + }, + }, + statusTag: { + '& .MuiTypography-body1': { + lineHeight: 1, + }, + }, + divider: { + margin: theme.spacing(3, 0), + }, +})); + +export default useStyles; diff --git a/apps/web-namada/src/screens/validator_details/hooks.ts b/apps/web-namada/src/screens/validator_details/hooks.ts new file mode 100644 index 0000000000..0312626805 --- /dev/null +++ b/apps/web-namada/src/screens/validator_details/hooks.ts @@ -0,0 +1,263 @@ +import { useRouter } from 'next/router'; +import * as R from 'ramda'; +import { useCallback, useEffect, useState } from 'react'; +import chainConfig from '@/chainConfig'; +import { + useValidatorVotingPowersQuery, + ValidatorVotingPowersQuery, + useValidatorInfoQuery, + ValidatorInfoQuery, + ValidatorAddressQuery, + useValidatorAddressQuery, +} from '@/graphql/types/general_types'; +import { useDesmosProfile } from '@/hooks/use_desmos_profile'; +import { SlashingParams } from '@/models'; +import { + StatusType, + ValidatorVPState, + ValidatorProfileState, + ValidatorOverviewState, +} from '@/screens/validator_details/types'; +import { formatToken } from '@/utils/format_token'; +import { getValidatorCondition } from '@/utils/get_validator_condition'; + +const { extra, votingPowerTokenUnit } = chainConfig(); + +const initialTokenDenom: TokenUnit = { + value: '0', + displayDenom: '', + baseDenom: '', + exponent: 0, +}; + +const initialVotingPowerState: ValidatorVPState = { + validatorVPExists: true, + votingPower: { + height: 0, + overall: initialTokenDenom, + self: 0, + validatorStatus: 0, + }, +}; + +const initialValidatorOverviewState: ValidatorOverviewState = { + exists: true, + overview: { + validator: '', + operatorAddress: '', + selfDelegateAddress: '', + description: '', + website: '', + }, + status: { + status: 0, + jailed: false, + tombstoned: false, + condition: 0, + liquidStaking: 'N/A', + commission: 0, + missedBlockCounter: 0, + signedBlockWindow: 0, + maxRate: '0', + }, +}; + +const initialValidatorProfileState: ValidatorProfileState = { + exists: true, + desmosProfile: null, + operatorAddress: '', + selfDelegateAddress: '', +}; + +export const useValidatorVotingPowerDetails = () => { + const router = useRouter(); + const [state, setState] = useState(initialVotingPowerState); + + const handleSetState = useCallback( + (stateChange: (prevState: ValidatorVPState) => ValidatorVPState) => { + setState((prevState) => { + const newState = stateChange(prevState); + return R.equals(prevState, newState) ? prevState : newState; + }); + }, + [] + ); + + // ========================== + // Fetch Data + // ========================== + const { loading } = useValidatorVotingPowersQuery({ + variables: { + address: router.query.address as string, + }, + onCompleted: (data) => { + handleSetState((prevState) => ({ ...prevState, ...formatValidatorVotingPower(data) })); + }, + }); + + return { state, loading }; +}; + +export const useValidatorOverviewDetails = () => { + const router = useRouter(); + const [state, setState] = useState(initialValidatorOverviewState); + + const handleSetState = useCallback( + (stateChange: (prevState: ValidatorOverviewState) => ValidatorOverviewState) => { + setState((prevState) => { + const newState = stateChange(prevState); + return R.equals(prevState, newState) ? prevState : newState; + }); + }, + [] + ); + + // ========================== + // Fetch Data + // ========================== + const { loading } = useValidatorInfoQuery({ + variables: { + address: router.query.address as string, + }, + onCompleted: (data) => { + handleSetState((prevState) => ({ ...prevState, ...formatValidatorOverview(data) })); + }, + }); + + return { state, loading }; +}; + +export const useValidatorProfileDetails = () => { + const [state, setState] = useState(initialValidatorProfileState); + const router = useRouter(); + + const handleSetState = useCallback( + (stateChange: (prevState: ValidatorProfileState) => ValidatorProfileState) => { + setState((prevState) => { + const newState = stateChange(prevState); + return R.equals(prevState, newState) ? prevState : newState; + }); + }, + [] + ); + + // ========================== + // Fetch Data + // ========================== + const { loading } = useValidatorAddressQuery({ + variables: { + address: router.query.address as string, + }, + onCompleted: (data) => { + handleSetState((prevState) => ({ ...prevState, ...formatValidatorAddress(data) })); + }, + }); + + // ========================== + // Desmos Profile + // ========================== + const { data: dataDesmosProfile, loading: loadingDesmosProfile } = useDesmosProfile({ + addresses: [state.selfDelegateAddress], + skip: !extra.profile || !state.selfDelegateAddress, + }); + useEffect( + () => + setState((prevState) => ({ + ...prevState, + desmosProfile: dataDesmosProfile?.[0], + loading: loadingDesmosProfile, + })), + [dataDesmosProfile, loadingDesmosProfile] + ); + return { state, loading }; +}; + +function formatValidatorAddress(data: ValidatorAddressQuery): Partial { + const stateChange: Partial = {}; + if (!data.validator.length) { + stateChange.exists = false; + return stateChange; + } + + const operatorAddress = data?.validator?.[0]?.validatorInfo?.operatorAddress ?? ''; + const selfDelegateAddress = data?.validator?.[0]?.validatorInfo?.selfDelegateAddress ?? ''; + + stateChange.operatorAddress = operatorAddress; + stateChange.selfDelegateAddress = selfDelegateAddress; + return stateChange; +} + +function formatValidatorVotingPower(data: ValidatorVotingPowersQuery): Partial { + const stateChange: Partial = {}; + if (!data.validator.length) { + stateChange.validatorVPExists = false; + return stateChange; + } + + const selfVotingPower = + (data.validator[0]?.validatorVotingPowers?.[0]?.votingPower ?? 0) / + 10 ** (extra.votingPowerExponent ?? 0); + + const votingPower = { + self: selfVotingPower, + overall: formatToken(data?.stakingPool?.[0]?.bonded ?? 0, votingPowerTokenUnit), + height: data.validator[0]?.validatorVotingPowers?.[0]?.height ?? 0, + validatorStatus: data.validator[0]?.validatorStatuses[0]?.status, + }; + + stateChange.votingPower = votingPower; + stateChange.validatorVPExists = true; + + return stateChange; +} + +function formatValidatorOverview(data: ValidatorInfoQuery): Partial { + const stateChange: Partial = {}; + if (!data.validator.length) { + stateChange.exists = false; + return stateChange; + } + + const operatorAddress = data?.validator?.[0]?.validatorInfo?.operatorAddress ?? ''; + const selfDelegateAddress = data?.validator?.[0]?.validatorInfo?.selfDelegateAddress ?? ''; + const overview = { + validator: operatorAddress, + operatorAddress, + selfDelegateAddress, + description: data.validator[0]?.validatorDescriptions?.[0]?.details ?? '', + website: data.validator[0]?.validatorDescriptions?.[0]?.website ?? '', + }; + + const slashingParams = SlashingParams.fromJson(data?.slashingParams?.[0]?.params ?? {}); + const missedBlockCounter = + data.validator[0]?.validatorSigningInfos?.[0]?.missedBlocksCounter ?? 0; + const { signedBlockWindow } = slashingParams; + const condition = getValidatorCondition(signedBlockWindow, missedBlockCounter); + + const liquidStakingReturn = data.validator[0]?.validatorLiquidStaking?.[0]?.liquidStaking; + let liquidStaking = 'N/A'; + if (liquidStakingReturn !== undefined) { + if (liquidStakingReturn) { + liquidStaking = 'Yes'; + } else { + liquidStaking = 'No'; + } + } + + const status: StatusType = { + status: data.validator[0]?.validatorStatuses?.[0]?.status ?? 3, + jailed: data.validator[0]?.validatorStatuses?.[0]?.jailed ?? false, + tombstoned: data.validator[0]?.validatorSigningInfos?.[0]?.tombstoned ?? false, + liquidStaking, + commission: data.validator[0]?.validatorCommissions?.[0]?.commission ?? 0, + condition, + missedBlockCounter, + signedBlockWindow, + maxRate: data?.validator?.[0]?.validatorInfo?.maxRate ?? '0', + }; + + stateChange.exists = true; + stateChange.overview = overview; + stateChange.status = status; + return stateChange; +} diff --git a/apps/web-namada/src/screens/validator_details/index.tsx b/apps/web-namada/src/screens/validator_details/index.tsx new file mode 100644 index 0000000000..1fd9c6513b --- /dev/null +++ b/apps/web-namada/src/screens/validator_details/index.tsx @@ -0,0 +1,80 @@ +import { NextSeo } from 'next-seo'; +import useAppTranslation from '@/hooks/useAppTranslation'; +import DesmosProfile from '@/components/desmos_profile'; +import Layout from '@/components/layout'; +import Blocks from '@/screens/validator_details/components/blocks'; +import Profile from '@/screens/validator_details/components/profile'; +import Staking from '@/screens/validator_details/components/staking'; +import Transactions from '@/screens/validator_details/components/transactions'; +import ValidatorOverview from '@/screens/validator_details/components/validator_overview'; +import VotingPower from '@/screens/validator_details/components/voting_power'; +import { + useValidatorProfileDetails, + useValidatorOverviewDetails, + useValidatorVotingPowerDetails, +} from '@/screens/validator_details/hooks'; +import useStyles from '@/screens/validator_details/styles'; +import LoadAndExist from '@/components/load_and_exist'; + +const ValidatorDetails = () => { + const { t } = useAppTranslation('validators'); + const { classes } = useStyles(); + const { state, loading } = useValidatorProfileDetails(); + const { exists, desmosProfile, operatorAddress } = state; + const { state: validatorOverviewState, loading: validatorOverviewLoading } = + useValidatorOverviewDetails(); + const { state: validatorVPState, loading: validatorVPLoading } = useValidatorVotingPowerDetails(); + + return ( + <> + + + +
+ + {exists && desmosProfile ? ( + + ) : ( + + )} + {!loading ? ( + <> + + + + ) : null} + {!loading && !validatorOverviewLoading && !validatorVPLoading ? ( + <> + + + + + ) : null} + +
+
+
+ + ); +}; + +export default ValidatorDetails; diff --git a/apps/web-namada/src/screens/validator_details/types.ts b/apps/web-namada/src/screens/validator_details/types.ts new file mode 100644 index 0000000000..45b7d15962 --- /dev/null +++ b/apps/web-namada/src/screens/validator_details/types.ts @@ -0,0 +1,45 @@ +export interface OverviewType { + validator: string; + operatorAddress: string; + selfDelegateAddress: string; + description: string; + website: string; +} + +export interface StatusType { + status: number; + inActiveSet?: string; + jailed: boolean; + tombstoned: boolean; + condition: number; + liquidStaking: string; + commission: number; + signedBlockWindow: number; + missedBlockCounter: number; + maxRate: string; +} + +export interface VotingPowerType { + height: number; + overall: TokenUnit; + self: number; + validatorStatus: number; +} + +export interface ValidatorVPState { + validatorVPExists: boolean; + votingPower: VotingPowerType; +} + +export interface ValidatorOverviewState { + exists: boolean; + overview: OverviewType; + status: StatusType; +} + +export interface ValidatorProfileState { + exists: boolean; + desmosProfile: DesmosProfile | null; + operatorAddress: string; + selfDelegateAddress: string; +} diff --git a/apps/web-namada/src/screens/validators/components/list/__snapshots__/index.test.tsx.snap b/apps/web-namada/src/screens/validators/components/list/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..c980aa811a --- /dev/null +++ b/apps/web-namada/src/screens/validators/components/list/__snapshots__/index.test.tsx.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`screen: Validators/List matches snapshot 1`] = ` +.emotion-0 { + min-height: 500px; + height: 50vh; +} + +@media (min-width:1280px) { + .emotion-0 { + min-height: 65vh; + } +} + +@media (max-width:1279.95px) { + .emotion-1 { + display: none; + } +} + +@media (min-width:1280px) { + .emotion-2 { + display: none; + } +} + +.emotion-2.emotion-2 { + height: 100%; +} + +
+
+
+
+
+
+
+
+
+`; diff --git a/apps/web-namada/src/screens/validators/components/list/components/desktop/index.tsx b/apps/web-namada/src/screens/validators/components/list/components/desktop/index.tsx new file mode 100644 index 0000000000..51e55f5fb2 --- /dev/null +++ b/apps/web-namada/src/screens/validators/components/list/components/desktop/index.tsx @@ -0,0 +1,246 @@ +import Typography from '@mui/material/Typography'; +import useAppTranslation from '@/hooks/useAppTranslation'; +import numeral from 'numeral'; +import { ComponentProps, CSSProperties, FC, LegacyRef, ReactNode } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { VariableSizeGrid as Grid } from 'react-window'; +import LiquidStakingFalseIcon from 'shared-utils/assets/liquid-staking-false.svg'; +import LiquidStakingTitleIcon from 'shared-utils/assets/liquid-staking-title.svg'; +import LiquidStakingTrueIcon from 'shared-utils/assets/liquid-staking-true.svg'; +import AvatarName from '@/components/avatar_name'; +import InfoPopover from '@/components/info_popover'; +import LiquidStakingExplanation from '@/components/liquid_staking_explanation'; +import SortArrows from '@/components/sort_arrows'; +import { useGrid } from '@/hooks/use_react_window'; +import Condition from '@/screens/validators/components/list/components/condition'; +import useStyles from '@/screens/validators/components/list/components/desktop/styles'; +import { fetchColumns } from '@/screens/validators/components/list/components/desktop/utils'; +import VotingPower from '@/screens/validators/components/list/components/voting_power'; +import VotingPowerExplanation from '@/screens/validators/components/list/components/voting_power_explanation'; +import type { ItemType } from '@/screens/validators/components/list/types'; +import { getValidatorConditionClass } from '@/utils/get_validator_condition'; +import { getValidatorStatus } from '@/utils/get_validator_status'; + +type GridColumnProps = { + column: ReturnType[number]; + sortKey: string; + sortDirection: 'desc' | 'asc'; + handleSort: (key: string) => void; + style: CSSProperties; +}; + +const GridColumn: FC = ({ column, sortKey, sortDirection, handleSort, style }) => { + const { t } = useAppTranslation('validators'); + const { classes, cx } = useStyles(); + + const { key, align, component, sort, sortKey: sortingKey } = column; + let formattedComponent = component; + + if (key === 'votingPower') { + formattedComponent = ( + + {t('votingPower')} + } /> + {!!sort && } + + ); + } + + if (key === 'liquidStaking') { + formattedComponent = ( + + + } /> + {!!sort && } + + ); + } + + return ( +
(sort ? handleSort(sortingKey ?? '') : null)} + role="button" + tabIndex={0} + aria-label={t(key) ?? undefined} + > + {formattedComponent || ( + + {t(key)} + {!!sort && } + + )} +
+ ); +}; + +type GridRowProps = { + column: string; + style: CSSProperties; + rowIndex: number; + align?: ComponentProps['align']; + item: ItemType; + search: string; + i: number; +}; + +const GridRow: FC = ({ column, style, rowIndex, align, item, search, i }) => { + const { classes, cx } = useStyles(); + const { name, address, imageUrl } = item.validator; + const { t } = useAppTranslation('validators'); + + if (search) { + const formattedSearch = search.toLowerCase().replace(/ /g, ''); + if ( + !name.toLowerCase().replace(/ /g, '').includes(formattedSearch) && + !address.toLowerCase().includes(formattedSearch) + ) { + return null; + } + } + + const status = getValidatorStatus(item.status, item.jailed, item.tombstoned); + const condition = item.status === 3 ? getValidatorConditionClass(item.condition) : undefined; + const percentDisplay = + item.status === 3 ? `${numeral(item.votingPowerPercent.toFixed(6)).format('0.[00]')}%` : '0%'; + const votingPower = numeral(item.votingPower).format('0,0'); + + let formatItem: ReactNode = null; + switch (column) { + case 'idx': + formatItem = `#${i + 1}`; + break; + case 'validator': + formatItem = ; + break; + case 'votingPower': + formatItem = ( + + ); + break; + case 'commission': + formatItem = `${numeral(item.commission).format('0.[00]')}%`; + break; + case 'status': + formatItem = ( + + {t(status.status)} + + ); + break; + case 'condition': + formatItem = ; + break; + case 'liquidStaking': + formatItem = + item.liquidStaking === 'Yes' ? : ; + break; + default: + break; + } + + return ( +
+ + {formatItem} + +
+ ); +}; + +type DesktopProps = { + className?: string; + sortDirection: 'desc' | 'asc'; + sortKey: string; + handleSort: (key: string) => void; + items: ItemType[]; + search: string; +}; + +const Desktop: FC = (props) => { + const { t } = useAppTranslation('validators'); + const { classes, cx } = useStyles(); + const columns = fetchColumns(t); + const { gridRef, columnRef, onResize, getColumnWidth, getRowHeight } = useGrid(columns); + + return ( +
+ + {({ height, width }) => ( + <> + {/* ======================================= */} + {/* Table Header */} + {/* ======================================= */} + } + columnCount={columns.length} + columnWidth={(index) => getColumnWidth(width ?? 0, index)} + height={50} + rowCount={1} + rowHeight={() => 50} + width={width ?? 0} + > + {({ columnIndex, style }) => ( + + )} + + {/* ======================================= */} + {/* Table Body */} + {/* ======================================= */} + } + columnCount={columns.length} + columnWidth={(index) => getColumnWidth(width ?? 0, index)} + height={(height ?? 0) - 50} + rowCount={props.items.length} + rowHeight={getRowHeight} + width={width ?? 0} + className="scrollbar" + > + {({ columnIndex, rowIndex, style }) => { + const { key, align } = columns[columnIndex]; + const item = props.items[rowIndex]; + if (!item?.validator) return null; + return ( + + ); + }} + + + )} + +
+ ); +}; + +export default Desktop; diff --git a/apps/web-namada/src/screens/validators/components/list/components/desktop/utils.tsx b/apps/web-namada/src/screens/validators/components/list/components/desktop/utils.tsx new file mode 100644 index 0000000000..ca3fd087dc --- /dev/null +++ b/apps/web-namada/src/screens/validators/components/list/components/desktop/utils.tsx @@ -0,0 +1,63 @@ +import Typography from '@mui/material/Typography'; +import type { TFunction } from '@/hooks/useAppTranslation'; +import { ReactNode } from 'react'; +import InfoPopover from '@/components/info_popover'; +import ConditionExplanation from '@/components/condition_explanation'; + +export const fetchColumns = ( + t: TFunction +): { + key: string; + align?: 'left' | 'center' | 'right' | 'justify' | 'inherit'; + width: number; + component?: ReactNode; + sortKey?: string; + sort?: boolean; +}[] => [ + { + key: 'idx', + width: 5, + }, + { + key: 'validator', + sortKey: 'validator.name', + width: 25, + sort: true, + }, + { + key: 'votingPower', + sortKey: 'votingPower', + width: 25, + sort: true, + }, + { + key: 'commission', + sortKey: 'commission', + align: 'right', + width: 15, + sort: true, + }, + { + key: 'status', + align: 'center', + width: 12, + }, + { + key: 'condition', + align: 'center', + width: 10, + component: ( + + {t('condition')} + } /> + + ), + }, + { + key: 'liquidStaking', + sortKey: 'liquidStaking', + align: 'left', + width: 8, + sort: true, + }, +]; diff --git a/apps/web-namada/src/screens/validators/components/list/components/mobile/component/single_validator/index.tsx b/apps/web-namada/src/screens/validators/components/list/components/mobile/component/single_validator/index.tsx new file mode 100644 index 0000000000..ad7f666c90 --- /dev/null +++ b/apps/web-namada/src/screens/validators/components/list/components/mobile/component/single_validator/index.tsx @@ -0,0 +1,74 @@ +import Typography from '@mui/material/Typography'; +import useAppTranslation from '@/hooks/useAppTranslation'; +import { FC, ReactNode } from 'react'; +import LiquidStakingFalseIcon from 'shared-utils/assets/liquid-staking-false.svg'; +import LiquidStakingTrueIcon from 'shared-utils/assets/liquid-staking-true.svg'; +import useStyles from '@/screens/validators/components/list/components/mobile/component/single_validator/styles'; + +type SingleValidatorProps = { + className?: string; + validator: ReactNode; + commission: string; + votingPower: ReactNode; + status: { + status: string; + theme: string; + }; + liquidStaking: string; +}; + +const SingleValidator: FC = ({ + className, + validator, + commission, + votingPower, + status, + liquidStaking, +}) => { + const { t } = useAppTranslation('validators'); + const { classes, cx } = useStyles(); + return ( +
+
+ + {t('validator')} + + {validator} +
+
+ + {t('votingPower')} + + {votingPower} +
+
+
+ + {t('status')} + + + {t(status.status)} + +
+
+ + {t('liquidStaking')} + + + {liquidStaking === 'Yes' ? : } + +
+
+ + {t('commission')} + + + {commission} + +
+
+
+ ); +}; + +export default SingleValidator; diff --git a/apps/web-namada/src/screens/validators/components/list/components/mobile/index.tsx b/apps/web-namada/src/screens/validators/components/list/components/mobile/index.tsx new file mode 100644 index 0000000000..6855595f78 --- /dev/null +++ b/apps/web-namada/src/screens/validators/components/list/components/mobile/index.tsx @@ -0,0 +1,108 @@ +import Divider from '@mui/material/Divider'; +import numeral from 'numeral'; +import { FC, LegacyRef } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { ListChildComponentProps, VariableSizeList as List } from 'react-window'; +import AvatarName from '@/components/avatar_name'; +import { useList, useListRow } from '@/hooks/use_react_window'; +import Condition from '@/screens/validators/components/list/components/condition'; +import SingleValidator from '@/screens/validators/components/list/components/mobile/component/single_validator'; +import VotingPower from '@/screens/validators/components/list/components/voting_power'; +import type { ItemType } from '@/screens/validators/components/list/types'; +import { getValidatorConditionClass } from '@/utils/get_validator_condition'; +import { getValidatorStatus } from '@/utils/get_validator_status'; + +type ListItemProps = Pick & { + setRowHeight: Parameters[1]; + item: ItemType; + search: string; + i: number; + isLast: boolean; +}; + +const ListItem: FC = ({ index, style, setRowHeight, item, search, i, isLast }) => { + const { rowRef } = useListRow(index, setRowHeight); + const { name, address, imageUrl } = item.validator; + + if (search) { + const formattedSearch = search.toLowerCase().replace(/ /g, ''); + if ( + !name.toLowerCase().replace(/ /g, '').includes(formattedSearch) && + !address.toLowerCase().includes(formattedSearch) + ) { + return null; + } + } + + const status = getValidatorStatus(item.status, item.jailed, item.tombstoned); + const condition = item.status === 3 ? getValidatorConditionClass(item.condition) : undefined; + const percentDisplay = + item.status === 3 ? `${numeral(item.votingPowerPercent.toFixed(6)).format('0.[00]')}%` : '0%'; + const votingPower = numeral(item.votingPower).format('0,0'); + const selectedItem = { + idx: `#${i + 1}`, + validator: , + commission: `${numeral(item.commission).format('0.[00]')}%`, + condition: , + votingPower: ( + + ), + status, + liquidStaking: item.liquidStaking, + }; + return ( +
+
+ + {!isLast && } +
+
+ ); +}; + +type MobileProps = { + className?: string; + items: ItemType[]; + search: string; +}; + +const Mobile: FC = ({ className, items, search }) => { + const { listRef, getRowHeight, setRowHeight } = useList(); + + return ( +
+ + {({ height, width }) => ( + } + width={width ?? 0} + > + {({ index, style }) => ( + + )} + + )} + +
+ ); +}; + +export default Mobile; diff --git a/apps/web-namada/src/screens/validators/components/list/hooks.ts b/apps/web-namada/src/screens/validators/components/list/hooks.ts new file mode 100644 index 0000000000..d752ae31d4 --- /dev/null +++ b/apps/web-namada/src/screens/validators/components/list/hooks.ts @@ -0,0 +1,203 @@ +import Big from 'big.js'; +import numeral from 'numeral'; +import * as R from 'ramda'; +import { SyntheticEvent, useCallback, useState } from 'react'; +import chainConfig from '@/chainConfig'; +import { useValidatorsQuery, ValidatorsQuery } from '@/graphql/types/general_types'; +import { SlashingParams } from '@/models'; +import type { + ItemType, + ValidatorsState, + ValidatorType, +} from '@/screens/validators/components/list/types'; +import { formatToken } from '@/utils/format_token'; +import { getValidatorCondition } from '@/utils/get_validator_condition'; + +const { votingPowerTokenUnit } = chainConfig(); + +// ========================== +// Parse data +// ========================== +const formatValidators = (data: ValidatorsQuery) => { + const slashingParams = SlashingParams.fromJson(data?.slashingParams?.[0]?.params ?? {}); + const votingPowerOverall = + numeral( + formatToken(data?.stakingPool?.[0]?.bondedTokens ?? 0, votingPowerTokenUnit).value + ).value() ?? 0; + + const { signedBlockWindow } = slashingParams; + + let formattedItems: ValidatorType[] = data.validator + .filter((x) => x.validatorInfo) + .map((x) => { + const votingPower = x?.validatorVotingPowers?.[0]?.votingPower ?? 0; + const votingPowerPercent = numeral((votingPower / (votingPowerOverall ?? 0)) * 100).value(); + + const missedBlockCounter = x?.validatorSigningInfos?.[0]?.missedBlocksCounter ?? 0; + const condition = getValidatorCondition(signedBlockWindow, missedBlockCounter); + + const liquidStakingReturn = x?.validatorLiquidStaking?.[0]?.liquidStaking; + let liquidStaking = 'N/A'; + if (liquidStakingReturn !== undefined) { + if (liquidStakingReturn) { + liquidStaking = 'Yes'; + } else { + liquidStaking = 'No'; + } + } + + return { + validator: x.validatorInfo?.operatorAddress ?? '', + votingPower: votingPower ?? 0, + votingPowerPercent: votingPowerPercent ?? 0, + liquidStaking, + commission: (x?.validatorCommissions?.[0]?.commission ?? 0) * 100, + condition, + status: x?.validatorStatuses?.[0]?.status ?? 0, + jailed: x?.validatorStatuses?.[0]?.jailed ?? false, + tombstoned: x?.validatorSigningInfos?.[0]?.tombstoned ?? false, + }; + }); + + // get the top 34% validators + formattedItems = formattedItems.sort((a, b) => (a.votingPower > b.votingPower ? -1 : 1)); + + // add key to indicate they are part of top 34% + let cumulativeVotingPower = Big(0); + let reached = false; + formattedItems.forEach((x) => { + if (x.status === 3) { + const totalVp = cumulativeVotingPower.add(x.votingPowerPercent); + if (totalVp.lte(34) && !reached) { + x.topVotingPower = true; + } + + if (totalVp.gt(34) && !reached) { + x.topVotingPower = true; + reached = true; + } + + cumulativeVotingPower = totalVp; + } + }); + + return { + votingPowerOverall, + items: formattedItems, + }; +}; + +export const useValidators = () => { + const [search, setSearch] = useState(''); + const [state, setState] = useState({ + loading: true, + exists: true, + items: [], + votingPowerOverall: 0, + tab: 0, + sortKey: 'validator.name', + sortDirection: 'asc', + }); + + const handleSetState = useCallback( + (stateChange: (prevState: ValidatorsState) => ValidatorsState) => { + setState((prevState) => { + const newState = stateChange(prevState); + return R.equals(prevState, newState) ? prevState : newState; + }); + }, + [] + ); + + // ========================== + // Fetch Data + // ========================== + useValidatorsQuery?.({ + onCompleted: (data) => { + handleSetState((prevState) => ({ + ...prevState, + loading: false, + ...formatValidators(data), + })); + }, + onError: () => handleSetState((prevState) => ({ ...prevState, loading: false })), + }); + + const handleTabChange = useCallback( + (_event: SyntheticEvent, newValue: number) => { + setState((prevState) => ({ + ...prevState, + tab: newValue, + })); + }, + [] + ); + + const handleSort = useCallback( + (key: string) => { + if (key === state.sortKey) { + setState((prevState) => ({ + ...prevState, + sortDirection: prevState.sortDirection === 'asc' ? 'desc' : 'asc', + })); + } else { + setState((prevState) => ({ + ...prevState, + sortKey: key, + sortDirection: 'asc', // new key so we start the sort by asc + })); + } + }, + [state.sortKey] + ); + + const sortItems = useCallback( + (items: ItemType[]) => { + let sorted: ItemType[] = R.clone(items); + + if (state.tab === 0) { + sorted = sorted.filter((x) => x.status === 3); + } + + if (state.tab === 1) { + sorted = sorted.filter((x) => x.status !== 3); + } + + if (state.sortKey && state.sortDirection) { + sorted.sort((a, b) => { + let compareA = R.pathOr('', [...state.sortKey.split('.')], a); + let compareB = R.pathOr('', [...state.sortKey.split('.')], b); + + if (typeof compareA === 'string' && typeof compareB === 'string') { + compareA = compareA.toLowerCase(); + compareB = compareB.toLowerCase(); + } + + if (compareA < compareB) { + return state.sortDirection === 'asc' ? -1 : 1; + } + if (compareA > compareB) { + return state.sortDirection === 'asc' ? 1 : -1; + } + return 0; + }); + } + + return sorted; + }, + [state.sortDirection, state.sortKey, state.tab] + ); + + const handleSearch = useCallback((value: string) => { + setSearch(value); + }, []); + + return { + state, + handleTabChange, + handleSort, + handleSearch, + sortItems, + search, + }; +}; diff --git a/apps/web-namada/src/screens/validators/components/list/index.test.tsx b/apps/web-namada/src/screens/validators/components/list/index.test.tsx new file mode 100644 index 0000000000..dbb7cb4641 --- /dev/null +++ b/apps/web-namada/src/screens/validators/components/list/index.test.tsx @@ -0,0 +1,136 @@ +import { ValidatorsDocument } from '@/graphql/types/general_types'; +import List from '@/screens/validators/components/list'; +import { mockClient } from '@/tests/mocks/mockApollo'; +import MockTheme from '@/tests/mocks/MockTheme'; +import wait from '@/tests/utils/wait'; +import { ApolloProvider } from '@apollo/client'; +import { MockedProvider } from '@apollo/client/testing'; +import renderer from 'react-test-renderer'; + +// ================================== +// mocks +// ================================== +jest.mock( + '@/screens/validators/components/list/components/mobile', + () => (props: JSX.IntrinsicElements['div']) =>
+); +jest.mock( + '@/screens/validators/components/list/components/desktop', + () => (props: JSX.IntrinsicElements['div']) =>
+); +jest.mock( + '@/screens/validators/components/list/components/tabs', + () => (props: JSX.IntrinsicElements['div']) =>
+); +jest.mock('@/components/box', () => (props: JSX.IntrinsicElements['div']) => ( +
+)); +jest.mock('@/components/no_data', () => (props: JSX.IntrinsicElements['div']) => ( +
+)); +jest.mock('@/components/load_and_exist', () => (props: JSX.IntrinsicElements['div']) => ( +
+)); + +const mockValidatorsDocument = jest.fn().mockReturnValue({ + data: { + stakingParams: [ + { + params: {}, + }, + ], + stakingPool: [ + { + bondedTokens: 3932987528498, + }, + ], + validator: [ + { + validatorStatuses: [ + { + status: 3, + jailed: false, + height: 437042, + }, + ], + validatorSigningInfos: [ + { + missedBlocksCounter: 1, + tombstoned: false, + }, + ], + validatorInfo: { + operatorAddress: 'desmosvaloper1njm853h4h9vh8xge3v9mf7nnukzhgt6z6dwsr3', + selfDelegateAddress: 'desmos1njm853h4h9vh8xge3v9mf7nnukzhgt6zyqxyfr', + }, + validatorVotingPowers: [ + { + votingPower: 22, + }, + ], + validatorLiquidStaking: [ + { + liquidStaking: 'Y', + }, + ], + validatorCommissions: [ + { + commission: 0.1, + }, + ], + delegations: [ + { + amount: { + denom: 'udaric', + amount: '1704000', + }, + delegatorAddress: 'desmos12mu43g2qvdddfc3tpgr3pekkrdx23jgv36jlta', + }, + { + amount: { + denom: 'udaric', + amount: '21000000', + }, + delegatorAddress: 'desmos1njm853h4h9vh8xge3v9mf7nnukzhgt6zyqxyfr', + }, + ], + }, + ], + slashingParams: [ + { + params: {}, + }, + ], + }, +}); + +// ================================== +// unit tests +// ================================== +describe('screen: Validators/List', () => { + it('matches snapshot', async () => { + let component: renderer.ReactTestRenderer | undefined; + + renderer.act(() => { + component = renderer.create( + + + + + + + + ); + }); + await wait(renderer.act); + + const tree = component?.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); +}); diff --git a/apps/web-namada/src/screens/validators/components/list/types.ts b/apps/web-namada/src/screens/validators/components/list/types.ts new file mode 100644 index 0000000000..893cbf951a --- /dev/null +++ b/apps/web-namada/src/screens/validators/components/list/types.ts @@ -0,0 +1,24 @@ +export interface ValidatorType { + validator: string; + votingPower: number; + votingPowerPercent: number; + liquidStaking: string; + commission: number; + condition: number; + status: number; + jailed: boolean; + tombstoned: boolean; + topVotingPower?: boolean; // top 34% VP +} + +export interface ValidatorsState { + loading: boolean; + exists: boolean; + tab: number; + sortKey: string; + sortDirection: 'asc' | 'desc'; + votingPowerOverall: number; + items: ValidatorType[]; +} + +export type ItemType = Override; diff --git a/apps/web-namada/src/utils/get_current_inflation.ts b/apps/web-namada/src/utils/get_current_inflation.ts new file mode 100644 index 0000000000..c1edf66acc --- /dev/null +++ b/apps/web-namada/src/utils/get_current_inflation.ts @@ -0,0 +1,20 @@ +/** + * Helper Function to get inflation amount from a list + */ +const getCurrentInflationAmount = ( + // eslint-disable-next-line camelcase + list: { amount: string; end_time: Date; start_time: Date }[] = [] +) => { + const today = new Date(); + // Get the current inflation schedule from mint params "inflation_schedules" array + const [currentInflationSchedule] = list.filter( + (x) => today >= new Date(x.start_time) && today <= new Date(x.end_time) + ); + let result = 0; + if (currentInflationSchedule) { + result = parseFloat(currentInflationSchedule?.amount ?? '0'); + } + return result; +}; + +export default getCurrentInflationAmount; diff --git a/apps/web-namada/tsconfig.json b/apps/web-namada/tsconfig.json new file mode 100644 index 0000000000..d02c130bbc --- /dev/null +++ b/apps/web-namada/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../packages/tsconfig/nextjs.json", + "compilerOptions": { + "rootDirs": ["../../packages/ui", "."], + "baseUrl": "./src/", + "paths": { + "@/*": ["./*", "../../../packages/ui/src/*"], + "@/public/*": ["../public/*"], + "ui/*": ["../../../packages/ui/src/*"] + } + }, + "include": ["*.d.ts", "src/**/*.ts", "src/**/*.tsx", "../../packages/ui/*.d.ts"] +} diff --git a/package.json b/package.json index 77f6f60f0e..23ae3938ca 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "turbo": "^1.9.3", "typescript": "^5.0.4" }, - "packageManager": "yarn@3.5.0", + "packageManager": "yarn@4.1.0", "engines": { "yarn": ">=3.5", "node": ">=18.17.1" diff --git a/packages/shared-utils/assets/icons/namada-light.png b/packages/shared-utils/assets/icons/namada-light.png new file mode 100644 index 0000000000..68775c1073 Binary files /dev/null and b/packages/shared-utils/assets/icons/namada-light.png differ diff --git a/packages/ui/src/components/ChainIcon/index.tsx b/packages/ui/src/components/ChainIcon/index.tsx index 35b44bf3c7..4e7d0b3f25 100644 --- a/packages/ui/src/components/ChainIcon/index.tsx +++ b/packages/ui/src/components/ChainIcon/index.tsx @@ -34,6 +34,8 @@ import shentuIconLight from 'shared-utils/assets/icons/shentu-light.svg?url'; import sifchainIconLight from 'shared-utils/assets/icons/sifchain-light.svg?url'; import solanaIconDark from 'shared-utils/assets/icons/solana-dark.svg?url'; import solanaIconLight from 'shared-utils/assets/icons/solana-light.svg?url'; +import namadaIconLight from 'shared-utils/assets/icons/namada-light.png?url'; +import namadaIconDark from 'shared-utils/assets/icons/namada-light.png?url'; import strideIconDark from 'shared-utils/assets/icons/stride-dark.svg?url'; import wormholeIconDark from 'shared-utils/assets/icons/wormhole.svg?url'; import celestiaIconDark from 'shared-utils/assets/icons/celestia-both.svg?url'; @@ -240,6 +242,15 @@ const ChainIcon = ({ [iconDark, iconLight] = type === 'icon' ? [solanaIconDark, solanaIconLight] : [solanaLogoLight, solanaLogoLight]; break; + case 'namada': { + // @TODO + const namadaLogoLight = namadaIconLight; + const namadaLogoDark = namadaIconDark; + + [iconDark, iconLight] = + type === 'icon' ? [namadaIconDark, namadaIconLight] : [namadaLogoDark, namadaLogoLight]; + break; + } case 'stride': [iconDark, iconLight] = type === 'icon' ? [strideIconDark, strideIconDark] : [strideLogoDark, strideLogoLight]; diff --git a/packages/ui/src/hooks/useBigDipperNetworks/index.ts b/packages/ui/src/hooks/useBigDipperNetworks/index.ts index fe89240c3c..87c16e0996 100644 --- a/packages/ui/src/hooks/useBigDipperNetworks/index.ts +++ b/packages/ui/src/hooks/useBigDipperNetworks/index.ts @@ -71,7 +71,7 @@ function useBigDipperNetworks(skipChainId = false) { }, [isCompleted, data]); // Fetch the chain ID using a GraphQL query - const chainIdQuery = useChainIdQuery({ skip: skipChainId }); + const chainIdQuery = useChainIdQuery?.({ skip: skipChainId }) || {}; // Refetch the query if there's an error and loading is completed const shouldRefetchChainId = !!chainIdQuery.error && !chainIdQuery.loading; diff --git a/packages/ui/src/recoil/market/hooks.ts b/packages/ui/src/recoil/market/hooks.ts index e8e404eaf9..db7ee6215f 100644 --- a/packages/ui/src/recoil/market/hooks.ts +++ b/packages/ui/src/recoil/market/hooks.ts @@ -16,10 +16,10 @@ const { primaryTokenUnit, tokenUnits } = chainConfig(); export function useMarketRecoil() { const [market, setMarket] = useRecoilState(writeMarket) as [ AtomState, - SetterOrUpdater + SetterOrUpdater, ]; - useMarketDataQuery({ + useMarketDataQuery?.({ variables: { denom: tokenUnits?.[primaryTokenUnit]?.display, }, diff --git a/packages/ui/src/recoil/validators/hooks.ts b/packages/ui/src/recoil/validators/hooks.ts index f8c56904d3..d50118d08b 100644 --- a/packages/ui/src/recoil/validators/hooks.ts +++ b/packages/ui/src/recoil/validators/hooks.ts @@ -7,7 +7,7 @@ import type { AtomState as ProfileAtomState } from '@/recoil/profiles/types'; import { atomFamilyState as validatorAtomState } from '@/recoil/validators/atom'; export const useValidatorRecoil = () => { - const { loading: loadingValidator, data } = useValidatorAddressesQuery(); + const { loading: loadingValidator, data } = useValidatorAddressesQuery?.() || {}; const setValidatorAtomState = useRecoilCallback( ({ set }) => (consensusAddress: string, newState: ValidatorAtomState) => diff --git a/packages/ui/src/screens/account_details/components/staking/hooks.ts b/packages/ui/src/screens/account_details/components/staking/hooks.ts index 336652a929..f04f7c66b8 100644 --- a/packages/ui/src/screens/account_details/components/staking/hooks.ts +++ b/packages/ui/src/screens/account_details/components/staking/hooks.ts @@ -113,7 +113,7 @@ export const useStaking = ( // ========================== // Fetch Data // ========================== - useValidatorsQuery({ + useValidatorsQuery?.({ onCompleted: (data) => { formatValidators(data); }, @@ -150,20 +150,20 @@ export const useStaking = ( loading: delegationsLoading, error: delegationsError, refetch: delegationsRefetch, - } = useAccountDelegationsQuery({ + } = useAccountDelegationsQuery?.({ variables: { address, limit: ROWS_PER_PAGE, offset: delegationsPage * ROWS_PER_PAGE, }, - }); + }) || {}; useEffect(() => { if (delegationsLoading) return; if (delegationsError) { delegationsRefetch({ pagination: false }); } }, [delegationsError, delegationsLoading, delegationsRefetch]); - useAccountDelegationsQuery({ + useAccountDelegationsQuery?.({ variables: { address, limit: ROWS_PER_PAGE, @@ -176,7 +176,7 @@ export const useStaking = ( data: dData, error: dError, refetch: dRefetch, - } = useAccountDelegationsQuery({ + } = useAccountDelegationsQuery?.({ variables: { address, limit: 0, @@ -184,7 +184,7 @@ export const useStaking = ( pagination: true, }, skip: delegationsPagination !== undefined, - }); + }) || {}; useEffect(() => { if (dError) { dRefetch(); @@ -201,20 +201,20 @@ export const useStaking = ( loading: redelegationsLoading, error: redelegationsError, refetch: redelegationsRefetch, - } = useAccountRedelegationsQuery({ + } = useAccountRedelegationsQuery?.({ variables: { address, limit: ROWS_PER_PAGE, offset: redelegationsPage * ROWS_PER_PAGE, }, - }); + }) || {}; useEffect(() => { if (redelegationsLoading) return; if (redelegationsError) { redelegationsRefetch({ pagination: false }); } }, [redelegationsError, redelegationsLoading, redelegationsRefetch]); - useAccountRedelegationsQuery({ + useAccountRedelegationsQuery?.({ variables: { address, limit: ROWS_PER_PAGE, @@ -227,7 +227,7 @@ export const useStaking = ( data: rData, error: rError, refetch: rRefetch, - } = useAccountRedelegationsQuery({ + } = useAccountRedelegationsQuery?.({ variables: { address, limit: 0, @@ -235,7 +235,7 @@ export const useStaking = ( pagination: true, }, skip: redelegationsPagination !== undefined, - }); + }) || {}; useEffect(() => { if (rError) { rRefetch(); @@ -252,20 +252,20 @@ export const useStaking = ( loading: undelegationsLoading, error: undelegationsError, refetch: undelegationsRefetch, - } = useAccountUndelegationsQuery({ + } = useAccountUndelegationsQuery?.({ variables: { address, limit: ROWS_PER_PAGE, offset: unbondingsPage * ROWS_PER_PAGE, }, - }); + }) || {}; useEffect(() => { if (undelegationsLoading) return; if (undelegationsError) { undelegationsRefetch({ pagination: false }); } }, [undelegationsError, undelegationsLoading, undelegationsRefetch]); - useAccountUndelegationsQuery({ + useAccountUndelegationsQuery?.({ variables: { address, limit: ROWS_PER_PAGE, @@ -278,7 +278,7 @@ export const useStaking = ( data: uData, error: uError, refetch: uRefetch, - } = useAccountUndelegationsQuery({ + } = useAccountUndelegationsQuery?.({ variables: { address, limit: 0, @@ -286,7 +286,7 @@ export const useStaking = ( pagination: true, }, skip: undelegationsPagination !== undefined, - }); + }) || {}; useEffect(() => { if (uError) { uRefetch(); diff --git a/packages/ui/src/screens/account_details/components/transactions/hooks.ts b/packages/ui/src/screens/account_details/components/transactions/hooks.ts index 7a529a89e0..20f84db5d9 100644 --- a/packages/ui/src/screens/account_details/components/transactions/hooks.ts +++ b/packages/ui/src/screens/account_details/components/transactions/hooks.ts @@ -74,25 +74,26 @@ export function useTransactions() { }); }; - const { fetchMore } = useGetMessagesByAddressQuery({ - variables: { - limit: LIMIT + 1, // to check if more exist - offset: 0, - address: `{${router?.query?.address ?? ''}}`, - }, - onCompleted: (data) => { - const itemsLength = data.messagesByAddress.length; - const newItems = R.uniq([...state.data, ...formatTransactions(data)]); - const stateChange: TransactionState = { - data: newItems, - hasNextPage: itemsLength === 51, - isNextPageLoading: false, - offsetCount: state.offsetCount + LIMIT, - }; + const { fetchMore } = + useGetMessagesByAddressQuery?.({ + variables: { + limit: LIMIT + 1, // to check if more exist + offset: 0, + address: `{${router?.query?.address ?? ''}}`, + }, + onCompleted: (data) => { + const itemsLength = data.messagesByAddress.length; + const newItems = R.uniq([...state.data, ...formatTransactions(data)]); + const stateChange: TransactionState = { + data: newItems, + hasNextPage: itemsLength === 51, + isNextPageLoading: false, + offsetCount: state.offsetCount + LIMIT, + }; - handleSetState((prevState) => ({ ...prevState, ...stateChange })); - }, - }); + handleSetState((prevState) => ({ ...prevState, ...stateChange })); + }, + }) || {}; const loadNextPage = async () => { handleSetState((prevState) => ({ ...prevState, isNextPageLoading: true })); diff --git a/packages/ui/src/screens/account_details/utils.tsx b/packages/ui/src/screens/account_details/utils.tsx index feb4fa12aa..acab62b471 100644 --- a/packages/ui/src/screens/account_details/utils.tsx +++ b/packages/ui/src/screens/account_details/utils.tsx @@ -26,12 +26,13 @@ export const useCommission = (address?: string) => { }), [] ); - const { data, error, refetch } = useAccountCommissionQuery({ - variables: { - validatorAddress, - }, - skip: !address, - }); + const { data, error, refetch } = + useAccountCommissionQuery?.({ + variables: { + validatorAddress, + }, + skip: !address, + }) || {}; useEffect(() => { if (error) refetch(); }, [error, refetch]); @@ -47,12 +48,13 @@ export const useAccountWithdrawalAddress = (address?: string) => { }), [address] ); - const { data, error, refetch } = useAccountWithdrawalAddressQuery({ - variables: { - address: address ?? '', - }, - skip: !address, - }); + const { data, error, refetch } = + useAccountWithdrawalAddressQuery?.({ + variables: { + address: address ?? '', + }, + skip: !address, + }) || {}; useEffect(() => { if (error) refetch(); }, [error, refetch]); @@ -68,12 +70,13 @@ export const useAvailableBalances = (address?: string) => { }), [] ); - const { data, error, refetch } = useAccountBalancesQuery({ - variables: { - address: address ?? '', - }, - skip: !address, - }); + const { data, error, refetch } = + useAccountBalancesQuery?.({ + variables: { + address: address ?? '', + }, + skip: !address, + }) || {}; useEffect(() => { if (error) refetch(); }, [error, refetch]); @@ -89,12 +92,13 @@ export const useDelegationBalance = (address?: string) => { }), [] ); - const { data, error, refetch } = useAccountDelegationBalanceQuery({ - variables: { - address: address ?? '', - }, - skip: !address, - }); + const { data, error, refetch } = + useAccountDelegationBalanceQuery?.({ + variables: { + address: address ?? '', + }, + skip: !address, + }) || {}; useEffect(() => { if (error) refetch(); }, [error, refetch]); @@ -110,12 +114,13 @@ export const useUnbondingBalance = (address?: string) => { }), [] ); - const { data, error, refetch } = useAccountUnbondingBalanceQuery({ - variables: { - address: address ?? '', - }, - skip: !address, - }); + const { data, error, refetch } = + useAccountUnbondingBalanceQuery?.({ + variables: { + address: address ?? '', + }, + skip: !address, + }) || {}; useEffect(() => { if (error) refetch(); }, [error, refetch]); @@ -124,12 +129,13 @@ export const useUnbondingBalance = (address?: string) => { export const useRewards = (address?: string) => { const defaultReturnValue = useMemo(() => ({ delegationRewards: [] }), []); - const { data, error, refetch } = useAccountDelegationRewardsQuery({ - variables: { - address: address ?? '', - }, - skip: !address, - }); + const { data, error, refetch } = + useAccountDelegationRewardsQuery?.({ + variables: { + address: address ?? '', + }, + skip: !address, + }) || {}; useEffect(() => { if (error) refetch(); }, [error, refetch]); diff --git a/packages/ui/src/screens/app/components/inner_app/hooks.ts b/packages/ui/src/screens/app/components/inner_app/hooks.ts index 0e7147fbdc..c8bcb74ec1 100644 --- a/packages/ui/src/screens/app/components/inner_app/hooks.ts +++ b/packages/ui/src/screens/app/components/inner_app/hooks.ts @@ -9,29 +9,30 @@ const isClient = typeof window === 'object'; export function useChainHealthCheck() { const [chainActive, setChainActive] = useState(true); const { t } = useAppTranslation('common'); - const [getLatestBlockTimestamp] = useLatestBlockTimestampLazyQuery({ - onCompleted: (data) => { - const timestamp = dayjs.utc(data?.block?.[0]?.timestamp ?? ''); - const timeNow = dayjs.utc(); - const timeDifference = timeNow.diff(timestamp, 's'); - // if latest block has been over a minute ago - if (timeDifference > 60 && chainActive) { - toast.error( - t('blockTimeAgo', { - time: dayjs.utc(timestamp).fromNow(), - }), - { - autoClose: false, - } - ); - setChainActive(false); - } - }, - }); + const [getLatestBlockTimestamp] = + useLatestBlockTimestampLazyQuery?.({ + onCompleted: (data) => { + const timestamp = dayjs.utc(data?.block?.[0]?.timestamp ?? ''); + const timeNow = dayjs.utc(); + const timeDifference = timeNow.diff(timestamp, 's'); + // if latest block has been over a minute ago + if (timeDifference > 60 && chainActive) { + toast.error( + t('blockTimeAgo', { + time: dayjs.utc(timestamp).fromNow(), + }), + { + autoClose: false, + } + ); + setChainActive(false); + } + }, + }) || []; useEffect(() => { if (!isClient) return; - getLatestBlockTimestamp(); + getLatestBlockTimestamp?.(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); } diff --git a/packages/ui/src/screens/block_details/hooks.ts b/packages/ui/src/screens/block_details/hooks.ts index 4334e56161..d181770902 100644 --- a/packages/ui/src/screens/block_details/hooks.ts +++ b/packages/ui/src/screens/block_details/hooks.ts @@ -36,7 +36,7 @@ export const useBlockDetails = () => { // ========================== // Fetch Data // ========================== - useBlockDetailsQuery({ + useBlockDetailsQuery?.({ variables: { height: numeral(router.query.height).value(), signatureHeight: (numeral(router.query.height).value() ?? 0) + 1, @@ -44,7 +44,7 @@ export const useBlockDetails = () => { onCompleted: (data) => { handleSetState((prevState) => ({ ...prevState, ...formatRaws(data) })); }, - }); + }) || {}; useEffect(() => { // reset every call diff --git a/packages/ui/src/screens/blocks/hooks.ts b/packages/ui/src/screens/blocks/hooks.ts index f8e73f4085..e51c0ab9e3 100644 --- a/packages/ui/src/screens/blocks/hooks.ts +++ b/packages/ui/src/screens/blocks/hooks.ts @@ -56,7 +56,7 @@ export const useBlocks = () => { // ================================ // block subscription // ================================ - useBlocksListenerSubscription({ + useBlocksListenerSubscription?.({ variables: { limit: 1, offset: 0, @@ -78,26 +78,27 @@ export const useBlocks = () => { // block query // ================================ const LIMIT = 51; - const blockQuery = useBlocksQuery({ - variables: { - limit: LIMIT, - offset: 1, - }, - onCompleted: (data) => { - const itemsLength = data.blocks.length; - const newItems = uniqueAndSort([...state.items, ...formatBlocks(data)]); - handleSetState((prevState) => ({ - ...prevState, - loading: false, - items: newItems, - hasNextPage: itemsLength === 51, - isNextPageLoading: false, - })); - }, - onError: () => { - handleSetState((prevState) => ({ ...prevState, loading: false })); - }, - }); + const blockQuery = + useBlocksQuery?.({ + variables: { + limit: LIMIT, + offset: 1, + }, + onCompleted: (data) => { + const itemsLength = data.blocks.length; + const newItems = uniqueAndSort([...state.items, ...formatBlocks(data)]); + handleSetState((prevState) => ({ + ...prevState, + loading: false, + items: newItems, + hasNextPage: itemsLength === 51, + isNextPageLoading: false, + })); + }, + onError: () => { + handleSetState((prevState) => ({ ...prevState, loading: false })); + }, + }) || {}; const loadNextPage = async () => { handleSetState((prevState) => ({ ...prevState, isNextPageLoading: true })); diff --git a/packages/ui/src/screens/home/components/blocks/hooks.ts b/packages/ui/src/screens/home/components/blocks/hooks.ts index 5775798f8a..caed15e947 100644 --- a/packages/ui/src/screens/home/components/blocks/hooks.ts +++ b/packages/ui/src/screens/home/components/blocks/hooks.ts @@ -34,7 +34,7 @@ export const useBlocks = () => { // ================================ // block subscription // ================================ - useBlocksListenerSubscription({ + useBlocksListenerSubscription?.({ onData: (data) => { handleSetState((prevState) => ({ ...prevState, diff --git a/packages/ui/src/screens/home/components/data_blocks/hooks.ts b/packages/ui/src/screens/home/components/data_blocks/hooks.ts index adef74cfad..d17d585aea 100644 --- a/packages/ui/src/screens/home/components/data_blocks/hooks.ts +++ b/packages/ui/src/screens/home/components/data_blocks/hooks.ts @@ -53,7 +53,7 @@ export const useDataBlocks = () => { // block height // ==================================== - useLatestBlockHeightListenerSubscription({ + useLatestBlockHeightListenerSubscription?.({ onData: (data) => { setState((prevState) => ({ ...prevState, @@ -65,7 +65,7 @@ export const useDataBlocks = () => { // ==================================== // block time // ==================================== - useAverageBlockTimeQuery({ + useAverageBlockTimeQuery?.({ onCompleted: (data) => { setState((prevState) => ({ ...prevState, @@ -77,7 +77,7 @@ export const useDataBlocks = () => { // ==================================== // token price // ==================================== - useTokenPriceListenerSubscription({ + useTokenPriceListenerSubscription?.({ variables: { denom: tokenUnits?.[primaryTokenUnit]?.display, }, @@ -92,7 +92,7 @@ export const useDataBlocks = () => { // ==================================== // validators // ==================================== - useActiveValidatorCountQuery({ + useActiveValidatorCountQuery?.({ onCompleted: (data) => { setState((prevState) => ({ ...prevState, diff --git a/packages/ui/src/screens/home/components/hero/hooks.ts b/packages/ui/src/screens/home/components/hero/hooks.ts index 0ec6f2e8c8..8a26362567 100644 --- a/packages/ui/src/screens/home/components/hero/hooks.ts +++ b/packages/ui/src/screens/home/components/hero/hooks.ts @@ -20,7 +20,7 @@ export const useHero = () => { }); }, []); - useTokenPriceHistoryQuery({ + useTokenPriceHistoryQuery?.({ variables: { limit: 48, denom: tokenUnits?.[primaryTokenUnit]?.display, diff --git a/packages/ui/src/screens/home/components/tokenomics/hooks.ts b/packages/ui/src/screens/home/components/tokenomics/hooks.ts index ce5e5e3d5b..ff3042533a 100644 --- a/packages/ui/src/screens/home/components/tokenomics/hooks.ts +++ b/packages/ui/src/screens/home/components/tokenomics/hooks.ts @@ -44,7 +44,7 @@ export const useTokenomics = () => { denom: '', }); - useTokenomicsQuery({ + useTokenomicsQuery?.({ onCompleted: (data) => { setState(formatTokenomics(data, state)); }, diff --git a/packages/ui/src/screens/home/components/tokenomics/index.tsx b/packages/ui/src/screens/home/components/tokenomics/index.tsx index e867f1f629..39350523cc 100644 --- a/packages/ui/src/screens/home/components/tokenomics/index.tsx +++ b/packages/ui/src/screens/home/components/tokenomics/index.tsx @@ -16,7 +16,7 @@ const { tokenUnits } = chainConfig(); const Tokenomics: FC = ({ className }) => { const { t } = useAppTranslation('home'); const { classes, cx, theme } = useStyles(); - const { state } = useTokenomics(); + const { state } = useTokenomics?.() || {}; const customToolTip = ( diff --git a/packages/ui/src/screens/home/components/transactions/hooks.ts b/packages/ui/src/screens/home/components/transactions/hooks.ts index b121113c97..0f61cdb4ee 100644 --- a/packages/ui/src/screens/home/components/transactions/hooks.ts +++ b/packages/ui/src/screens/home/components/transactions/hooks.ts @@ -35,7 +35,7 @@ export const useTransactions = () => { // ================================ // txs subscription // ================================ - useTransactionsListenerSubscription({ + useTransactionsListenerSubscription?.({ onData: (data) => { setState((prevState) => { const newState = { diff --git a/packages/ui/src/screens/params/hooks.ts b/packages/ui/src/screens/params/hooks.ts index 9d48a393fb..6b2082fbec 100644 --- a/packages/ui/src/screens/params/hooks.ts +++ b/packages/ui/src/screens/params/hooks.ts @@ -147,7 +147,7 @@ export const useParams = () => { // ================================ // param query // ================================ - useParamsQuery({ + useParamsQuery?.({ onCompleted: (data) => { handleSetState((prevState) => ({ ...prevState, diff --git a/packages/ui/src/screens/proposals/hooks.ts b/packages/ui/src/screens/proposals/hooks.ts index 918cdd5d99..4fd93429b1 100644 --- a/packages/ui/src/screens/proposals/hooks.ts +++ b/packages/ui/src/screens/proposals/hooks.ts @@ -39,23 +39,24 @@ export const useProposals = () => { // proposals query // ================================ - const proposalQuery = useProposalsQuery({ - variables: { - limit: 50, - offset: 0, - }, - onCompleted: (data) => { - const newItems = R.uniq([...state.items, ...formatProposals(data)]); - handleSetState((prevState) => ({ - ...prevState, - loading: false, - items: newItems, - hasNextPage: newItems.length < (data.total?.aggregate?.count ?? 0), - isNextPageLoading: false, - rawDataTotal: data.total?.aggregate?.count ?? prevState.rawDataTotal, - })); - }, - }); + const proposalQuery = + useProposalsQuery?.({ + variables: { + limit: 50, + offset: 0, + }, + onCompleted: (data) => { + const newItems = R.uniq([...state.items, ...formatProposals(data)]); + handleSetState((prevState) => ({ + ...prevState, + loading: false, + items: newItems, + hasNextPage: newItems.length < (data.total?.aggregate?.count ?? 0), + isNextPageLoading: false, + rawDataTotal: data.total?.aggregate?.count ?? prevState.rawDataTotal, + })); + }, + }) || {}; const loadNextPage = async () => { handleSetState((prevState) => ({ ...prevState, isNextPageLoading: true })); diff --git a/packages/ui/src/screens/transactions/hooks.ts b/packages/ui/src/screens/transactions/hooks.ts index 72c3fe7565..67ef954f2f 100644 --- a/packages/ui/src/screens/transactions/hooks.ts +++ b/packages/ui/src/screens/transactions/hooks.ts @@ -67,7 +67,7 @@ export const useTransactions = () => { // ================================ // tx subscription // ================================ - useTransactionsListenerSubscription({ + useTransactionsListenerSubscription?.({ variables: { limit: 1, offset: 0, @@ -89,26 +89,27 @@ export const useTransactions = () => { // tx query // ================================ const LIMIT = 51; - const transactionQuery = useTransactionsQuery({ - variables: { - limit: LIMIT, - offset: 1, - }, - onError: () => { - handleSetState((prevState) => ({ ...prevState, loading: false })); - }, - onCompleted: (data) => { - const itemsLength = data.transactions.length; - const newItems = uniqueAndSort([...state.items, ...formatTransactions(data)]); - handleSetState((prevState) => ({ - ...prevState, - loading: false, - items: newItems, - hasNextPage: itemsLength === 51, - isNextPageLoading: false, - })); - }, - }); + const transactionQuery = + useTransactionsQuery?.({ + variables: { + limit: LIMIT, + offset: 1, + }, + onError: () => { + handleSetState((prevState) => ({ ...prevState, loading: false })); + }, + onCompleted: (data) => { + const itemsLength = data.transactions.length; + const newItems = uniqueAndSort([...state.items, ...formatTransactions(data)]); + handleSetState((prevState) => ({ + ...prevState, + loading: false, + items: newItems, + hasNextPage: itemsLength === 51, + isNextPageLoading: false, + })); + }, + }) || {}; const loadNextPage = async () => { handleSetState((prevState) => ({ ...prevState, isNextPageLoading: true })); diff --git a/packages/ui/src/screens/validator_details/components/transactions/hooks.ts b/packages/ui/src/screens/validator_details/components/transactions/hooks.ts index 0288136ef9..022b1e29ca 100644 --- a/packages/ui/src/screens/validator_details/components/transactions/hooks.ts +++ b/packages/ui/src/screens/validator_details/components/transactions/hooks.ts @@ -58,25 +58,26 @@ export function useTransactions() { }); }; - const transactionQuery = useGetMessagesByAddressQuery({ - variables: { - limit: LIMIT + 1, // to check if more exist - offset: 0, - address: `{${router?.query?.address ?? ''}}`, - }, - onCompleted: (data) => { - const itemsLength = data.messagesByAddress.length; - const newItems = R.uniq([...state.data, ...formatTransactions(data)]); - const stateChange: TransactionState = { - data: newItems, - hasNextPage: itemsLength === 51, - isNextPageLoading: false, - offsetCount: state.offsetCount + LIMIT, - }; + const transactionQuery = + useGetMessagesByAddressQuery?.({ + variables: { + limit: LIMIT + 1, // to check if more exist + offset: 0, + address: `{${router?.query?.address ?? ''}}`, + }, + onCompleted: (data) => { + const itemsLength = data.messagesByAddress.length; + const newItems = R.uniq([...state.data, ...formatTransactions(data)]); + const stateChange: TransactionState = { + data: newItems, + hasNextPage: itemsLength === 51, + isNextPageLoading: false, + offsetCount: state.offsetCount + LIMIT, + }; - handleSetState((prevState) => ({ ...prevState, ...stateChange })); - }, - }); + handleSetState((prevState) => ({ ...prevState, ...stateChange })); + }, + }) || {}; const loadNextPage = async () => { handleSetState((prevState) => ({ ...prevState, isNextPageLoading: true })); diff --git a/packages/ui/src/screens/validators/components/list/hooks.ts b/packages/ui/src/screens/validators/components/list/hooks.ts index 2dfae48220..d9a86e4437 100644 --- a/packages/ui/src/screens/validators/components/list/hooks.ts +++ b/packages/ui/src/screens/validators/components/list/hooks.ts @@ -104,7 +104,7 @@ export const useValidators = () => { // ========================== // Fetch Data // ========================== - useValidatorsQuery({ + useValidatorsQuery?.({ onCompleted: (data) => { handleSetState((prevState) => ({ ...prevState,