Compare commits

..
Author SHA1 Message Date
sam-keen 138e9f45f4 fix(3086): order proposals list by closing date 2023-03-23 15:33:25 +00:00
327 changed files with 6814 additions and 10572 deletions
-4
View File
@@ -3,7 +3,3 @@ apps/**/node_modules/*
tmp/*
.dockerignore
dockerfiles
node_modules
.git
.github
.vscode
-21
View File
@@ -1,21 +0,0 @@
---
name: Release
about: A template to outline the steps needed to for a successful release of our frontend apps
title: 'Release [add dapp version]-core-[add core version]'
labels:
assignees: ''
---
### Tasks
- [ ] Review [link to core release](xxx)
- [ ] Tag frontend-monorepo
- [ ] Create release and generate release notes
- [ ] Run `@smoke` tests
- [ ] Run `@regression` tests
- [ ] Run `@slow` tests
- [ ] Explorative testing of key flows
- [ ] Set `release/[network]` to tagged commit
- [ ] Verify builds (on Netlify and Fleek) are successful
- [ ] Verify build has been deployed
- [ ] Smoke testing on deployed app
+1 -1
View File
@@ -13,7 +13,7 @@ env:
jobs:
add_issue:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: 'Add issue to project board'
run: |
-110
View File
@@ -1,110 +0,0 @@
name: CI/CD
on:
push:
branches:
- release/*
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
jobs:
lint-test-build:
runs-on: ubuntu-22.04
name: '(CI) lint + unit test + build'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: Install root dependencies
run: yarn install --frozen-lockfile
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v3
with:
main-branch-name: develop
- name: Check formatting
run: yarn nx format:check
- name: Lint affected
run: yarn nx affected:lint --max-warnings=0
- name: Build affected spec
run: yarn nx affected --target=build-spec
- name: Test affected
run: yarn nx affected:test
- name: Build affected
run: yarn nx affected:build
# See affected apps
- name: See affected apps
run: |
echo ">>>> debug"
echo "NX Version: $nx_version"
echo "NX_BASE: ${{ env.NX_BASE }}"
echo "NX_HEAD: ${{ env.NX_HEAD }}"
echo ">>>> eof debug"
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
echo -n "Affected projects: $affected"
projects_e2e=""
if [[ $affected == *"governance"* ]]; then projects_e2e+='"governance-e2e" '; fi
if [[ $affected == *"trading"* ]]; then projects_e2e+='"trading-e2e" '; fi
if [[ $affected == *"explorer"* ]]; then projects_e2e+='"explorer-e2e" '; fi
if [[ -z "$projects_e2e" ]]; then projects_e2e+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi
projects_e2e=${projects_e2e%?}
projects_e2e=[${projects_e2e// /,}]
echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV
outputs:
projects: ${{ env.PROJECTS }}
projects-e2e: ${{ env.PROJECTS_E2E }}
cypress:
needs: lint-test-build
name: '(CI) cypress'
if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
uses: ./.github/workflows/cypress-run.yml
secrets: inherit
with:
projects: ${{ needs.lint-test-build.outputs.projects-e2e }}
tags: '@smoke @regression'
publish-dist:
needs: lint-test-build
name: '(CD) publish dist'
if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
uses: ./.github/workflows/publish-dist.yml
secrets: inherit
with:
projects: ${{ needs.lint-test-build.outputs.projects }}
# Report single result at the end, to avoid mess with required checks in PR
cypress-result:
if: ${{ always() }}
needs: cypress
runs-on: ubuntu-22.04
steps:
- run: |
result="${{ needs.cypress.result }}"
if [[ $result == "success" || $result == "skipped" ]]; then
exit 0
else
exit 1
fi
+1 -1
View File
@@ -13,7 +13,7 @@ on:
jobs:
cypress-run:
name: Run Cypress Trading tests -- live environment
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
+78
View File
@@ -0,0 +1,78 @@
name: Cypress tests - PR
on:
push:
branches:
- develop
- main
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
jobs:
pr:
runs-on: ubuntu-latest
steps:
- name: Checkout frontend mono repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup node
uses: actions/setup-node@v3
- name: Remove package.json & yarn.lock to avoid installing everything
run: rm package.json yarn.lock
- name: Install nx
run: yarn add nx
# Check SHAs
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v2
with:
main-branch-name: ${{ github.base_ref || github.ref_name }}
set-environment-variables-for-job: true
# See affected apps
- name: See affected apps
run: |
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
echo -n "Affected projects: $affected"
projects=""
if [[ $affected == *"governance"* ]]; then projects+='"governance-e2e" '; fi
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi
if [[ -z "$projects" ]]; then projects+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi
projects=${projects%?}
projects=[${projects// /,}]
echo PROJECTS=$projects >> $GITHUB_ENV
outputs:
projects: ${{ env.PROJECTS }}
run:
needs: pr
if: ${{ needs.pr.outputs.projects != '[]' }}
uses: ./.github/workflows/cypress-run.yml
secrets: inherit
with:
projects: ${{ needs.pr.outputs.projects }}
tags: '@smoke @regression'
# Report single result at the end, to avoid mess with required checks in PR
result:
if: ${{ always() }}
needs: run
runs-on: ubuntu-latest
name: Cypress result
steps:
- run: |
result="${{ needs.run.result }}"
if [[ $result == "success" || $result == "skipped" ]]; then
exit 0
else
exit 1
fi
-2
View File
@@ -1,4 +1,3 @@
name: (CI) Cypress Run
on:
workflow_call:
inputs:
@@ -18,7 +17,6 @@ jobs:
fail-fast: false
matrix:
project: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.project }}
runs-on: self-hosted-runner
timeout-minutes: 30
steps:
+9 -12
View File
@@ -8,25 +8,22 @@ on:
jobs:
master:
name: Generate Queries
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
uses: actions/checkout@v2
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
fetch-depth: 0
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.15.1
- name: Install root dependencies
run: yarn install --frozen-lockfile
run: yarn install
- name: Generate queries
run: node ./scripts/get-queries.js
- uses: actions/upload-artifact@v2
with:
name: queries
+10 -17
View File
@@ -3,28 +3,21 @@ name: Verify PR title
on:
pull_request:
types:
- opened
- ready_for_review
- reopened
- edited
- synchronize
types: [opened, ready_for_review, reopened, edited, synchronize]
jobs:
lint_pr:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
uses: actions/checkout@v2
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
fetch-depth: 0
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.15.1
- name: Install root dependencies
run: yarn install --frozen-lockfile
run: yarn install
- name: Check PR title
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
+1 -1
View File
@@ -7,7 +7,7 @@ on:
jobs:
master:
name: Generate Queries
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
-108
View File
@@ -1,108 +0,0 @@
name: (CD) Publish docker + s3
on:
workflow_call:
inputs:
projects:
required: true
type: string
jobs:
publish-dist:
strategy:
fail-fast: false
matrix:
app: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.app }}
runs-on: ubuntu-22.04
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Set up QEMU
id: quemu
uses: docker/setup-qemu-action@v2
- name: Available platforms
run: echo ${{ steps.qemu.outputs.platforms }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# https://docs.github.com/en/actions/learn-github-actions/contexts
- name: Check node version
id: tags
run: |
nodeVersion=$(cat .nvmrc | head -n 1)
echo ::set-output name=nodeVersion::${nodeVersion}
if [[ "${{ github.event_name }}" = "push" ]]; then
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
bucketName="${{ github.event.repository.name }}-$envName"
echo ::set-output name=bucketName::${bucketName}
echo ::set-output name=envName::${envName}
fi
- name: Build and export to local Docker
id: docker_build
uses: docker/build-push-action@v3
with:
load: true
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.nodeVersion }}
ENV_NAME=${{ steps.tags.outputs.envName || '' }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
- name: Sanity check docker image
run: |
echo "Check ipfs-hash"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat ipfs-hash
echo "List html directory"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local ls -lah
echo "Copy dist to local filesystem"
docker create --name=dist ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
docker cp dist:/usr/share/nginx/html dist
echo "Check local dist"
ls -al dist
- name: Publish dist as docker image
uses: docker/build-push-action@v3
if: ${{ github.event_name == 'pull_request' }}
with:
push: true
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.nodeVersion }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
# - uses: shallwefootball/s3-upload-action@master
# if: ${{ github.event_name == 'push' }}
# name: Upload dist S3
# with:
# aws_key_id: ${{ secrets.AWS_KEY_ID }}
# aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY}}
# aws_bucket: ${{ steps.tags.outputs.bucketName }}
# source_dir: 'dist'
- name: Add preview label
uses: actions-ecosystem/action-add-labels@v1
if: ${{ github.event_name == 'pull_request' }}
with:
labels: ${{ matrix.app }}-preview
number: ${{ github.event.number }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
@@ -0,0 +1,89 @@
name: Publish docker containers
'on':
pull_request:
jobs:
master:
strategy:
fail-fast: false
# to be replaced by nix: https://github.com/vegaprotocol/frontend-monorepo/blob/develop/tools/ipfs-deploy.js#L106-L108
matrix:
app: ${{ fromJson('["explorer"]') }}
name: Build the ${{ matrix.app }} image
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Set up QEMU
id: quemu
uses: docker/setup-qemu-action@v2
- name: Available platforms
run: echo ${{ steps.qemu.outputs.platforms }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# https://docs.github.com/en/actions/learn-github-actions/contexts
# https://github.com/actions/checkout#Checkout-pull-request-HEAD-commit-instead-of-merge-commit
- name: Determine Docker Image tag
id: tags
run: |
npmVersion=$(cat .nvmrc | head -n 1)
versionTag=${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || github.event.pull_request.head.sha }}
echo ::set-output name=npmVersion::${npmVersion}
echo ::set-output name=version::${versionTag}
- name: Print config
run: |
git rev-parse --verify HEAD
git status
echo "steps.tags.outputs.version=${{ steps.tags.outputs.version }}"
- name: Build and export to local Docker
uses: docker/build-push-action@v3
with:
context: .
push: false
file: dockerfiles/Dockerfile
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.npmVersion }}
load: true
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
- name: Sanity check docker image
run: |
echo "Check .env file"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat .env
echo "Check ipfs-hash"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat ipfs-hash
echo "List html directory"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local ls -lah
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v3
with:
context: .
push: true
file: dockerfiles/Dockerfile
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.npmVersion }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:latest
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ steps.tags.outputs.version }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
+11 -10
View File
@@ -8,7 +8,6 @@ on:
required: true
type: choice
options:
- announcements
- ui-toolkit
- react-helpers
- tailwindcss-config
@@ -19,27 +18,29 @@ on:
jobs:
publish:
name: Build & Publish - Tag
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
permissions:
contents: 'read'
actions: 'read'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup node
with:
fetch-depth: 0
- name: User Node.js 16
id: Node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
node-version: 16.15.1
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: node_modules-${{ hashFiles('**/yarn.lock') }}
- name: Install root dependencies
run: yarn install --frozen-lockfile
- name: Build project
run: yarn nx build ${{inputs.project}}
- name: Publish project to @vegaprotocol
uses: JS-DevTools/npm-publish@v1
with:
+46
View File
@@ -0,0 +1,46 @@
name: Unit tests & build
on:
push:
branches:
- develop
- main
pull_request:
jobs:
pr:
name: Test and lint - PR
runs-on: ubuntu-latest
permissions:
contents: 'read'
actions: 'read'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v2
with:
main-branch-name: ${{ github.base_ref }}
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v3
with:
node-version: 16.15.1
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: node_modules-${{ hashFiles('**/yarn.lock') }}
- name: Install root dependencies
run: yarn install --frozen-lockfile
- name: Check formatting
run: yarn nx format:check
- name: Lint affected
run: yarn nx affected:lint --max-warnings=0
- name: Test affected
run: yarn nx affected:test
- name: Build affected
run: yarn nx affected:build
- name: Build affected spec
run: yarn nx affected --target=build-spec
+87 -8
View File
@@ -6,10 +6,18 @@ context('Home Page', function () {
describe('Stats page', { tags: '@smoke' }, function () {
const statsValue = '[data-testid="stats-value"]';
it('Should show connected environment', function () {
const deployedEnv = Cypress.env('environment').toUpperCase();
cy.get('[data-testid="stats-environment"]').should(
'have.text',
`/ ${deployedEnv}`
);
});
it('should show connected environment stats', function () {
const statTitles = {
0: 'Status',
1: 'Block height',
1: 'Height',
2: 'Uptime',
3: 'Total nodes',
4: 'Total staked',
@@ -28,27 +36,27 @@ context('Home Page', function () {
cy.get('[data-testid="stats-title"]')
.each(($list, index) => {
cy.wrap($list).should('contain.text', statTitles[index]);
cy.wrap($list).should('have.text', statTitles[index]);
})
.then(($list) => {
cy.wrap($list).should('have.length', 16);
});
cy.get(statsValue).eq(0).should('contain.text', 'CONNECTED');
cy.get(statsValue).eq(0).should('have.text', 'CONNECTED');
cy.get(statsValue).eq(1).should('not.be.empty');
cy.get(statsValue)
.eq(2)
.invoke('text')
.should('match', /\d+d \d+h \d+m \d+s/i);
cy.get(statsValue).eq(3).should('contain.text', '2');
cy.get(statsValue).eq(3).should('have.text', '2');
cy.get(statsValue)
.eq(4)
.invoke('text')
.should('match', /\d+\.\d\d(?!\d)/i);
cy.get(statsValue).eq(5).should('contain.text', '0');
cy.get(statsValue).eq(6).should('contain.text', '0');
cy.get(statsValue).eq(7).should('contain.text', '0');
cy.get(statsValue).eq(8).should('contain.text', '0');
cy.get(statsValue).eq(5).should('have.text', '0');
cy.get(statsValue).eq(6).should('have.text', '0');
cy.get(statsValue).eq(7).should('have.text', '0');
cy.get(statsValue).eq(8).should('have.text', '0');
cy.get(statsValue).eq(9).should('not.be.empty');
cy.get(statsValue).eq(10).should('not.be.empty');
cy.get(statsValue).eq(11).should('not.be.empty');
@@ -78,4 +86,75 @@ context('Home Page', function () {
});
});
});
describe('Git info', function () {
it('git info is rendered on the footer of the page', function () {
cy.getByTestId('git-info').within(() => {
cy.getByTestId('git-network-data').within(() => {
cy.contains('Reading network data from').should('be.visible');
cy.get('span').should('have.text', Cypress.env('networkQueryUrl'));
cy.getByTestId('link').should('be.visible');
});
cy.getByTestId('git-eth-data').within(() => {
cy.contains('Reading Ethereum data from').should('be.visible');
cy.get('span').should('have.text', Cypress.env('ethUrl'));
});
cy.getByTestId('git-commit-hash').within(() => {
cy.contains('Version/commit hash:').should('be.visible');
cy.getByTestId('link').should('have.text', Cypress.env('commitHash'));
});
});
});
});
describe('Search bar', function () {
it('Successful search for specific id by block id', function () {
const blockId = '973624';
search(blockId);
cy.url().should('include', blockId);
});
it('Successful search for specific id by tx hash', function () {
const txHash =
'9ED3718AA8308E7E08EC588EE7AADAF49711D2138860D8914B4D81A2054D9FB8';
search(txHash);
cy.url().should('include', txHash);
});
it('Successful search for specific id by tx id', function () {
const txId =
'0x61DCCEBB955087F50D0B85382DAE138EDA9631BF1A4F92E563D528904AA38898';
search(txId);
cy.url().should('include', txId);
});
it('Error message displayed when invalid search by wrong string length', function () {
search('9ED3718AA8308E7E08EC588EE7AADAF497D2138860D8914B4D81A2054D9FB8');
validateSearchError("Something doesn't look right");
});
it('Error message displayed when invalid search by invalid hash', function () {
search(
'9ED3718AA8308E7E08ECht8EE753DAF49711D2138860D8914B4D81A2054D9FB8'
);
validateSearchError('Transaction is not hexadecimal');
});
it('Error message displayed when searching empty field', function () {
cy.get('[data-testid="search"]').clear();
cy.get('[data-testid="search-button"]').click();
validateSearchError('Search required');
});
function search(searchTxt) {
cy.get('[data-testid="search"]').clear().type(searchTxt);
cy.get('[data-testid="search-button"]').click();
}
function validateSearchError(expectedError) {
cy.get('[data-testid="search-error"]').should('have.text', expectedError);
}
});
});
+1 -1
View File
@@ -1,7 +1,7 @@
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml
NX_VEGA_NETWORKS='{"STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_NETWORKS='{"SANDBOX":"https://sandbox.explorer.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_ENV=STAGNET3
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
-1
View File
@@ -3,7 +3,6 @@ NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_ENV=CUSTOM
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
# App flags
NX_EXPLORER_ASSETS=1
-1
View File
@@ -8,4 +8,3 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
-1
View File
@@ -6,4 +6,3 @@ NX_VEGA_ENV=MAINNET
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
+19
View File
@@ -0,0 +1,19 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
NX_VEGA_ENV=MIRROR
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
# App flags
NX_EXPLORER_ASSETS=1
NX_EXPLORER_GENESIS=1
NX_EXPLORER_GOVERNANCE=1
NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1
NX_EXPLORER_VALIDATORS=1
NX_EXPLORER_MARKETS=0
NX_EXPLORER_ORACLES=0
NX_EXPLORER_TXS_LIST=1
+11
View File
@@ -0,0 +1,11 @@
# App configuration variables
NX_VEGA_ENV=SANDBOX
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.sandbox.vega.xyz/websocket
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
-1
View File
@@ -11,4 +11,3 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://stagnet1.token.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
-1
View File
@@ -5,4 +5,3 @@ NX_VEGA_ENV=STAGNET3
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases/
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
-1
View File
@@ -8,4 +8,3 @@ NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
+1 -2
View File
@@ -5,9 +5,8 @@ NX_VEGA_URL=https://api-validators-testnet.vega.rocks/graphql
NX_VEGA_REST=https://api-validators-testnet.vega.rocks/
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
NX_TENDERMINT_URL=https://tm-be.validators-testnet.vega.rocks
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
-1
View File
@@ -4,4 +4,3 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
NX_VEGA_ENV=CUSTOM
NX_BLOCK_EXPLORER=
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
+75 -9
View File
@@ -1,21 +1,87 @@
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
import { Header } from './components/header';
import { Main } from './components/main';
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { Footer } from './components/footer/footer';
import {
AnnouncementBanner,
ExternalLink,
Icon,
} from '@vegaprotocol/ui-toolkit';
import {
AssetDetailsDialog,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router-config';
import classNames from 'classnames';
import { useState } from 'react';
const splashLoading = (
<Splash>
<Loader />
</Splash>
);
const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
return (
<AssetDetailsDialog
assetId={id}
trigger={trigger || null}
asJson={asJson}
open={isOpen}
onChange={setOpen}
/>
);
};
const MainnetSimAd = () => {
const [shouldDisplayBanner, setShouldDisplayBanner] = useState<boolean>(true);
// Return an empty div so that the grid layout in _app.page.ts
// renders correctly
if (!shouldDisplayBanner) {
return <div />;
}
return (
<AnnouncementBanner>
<div className="grid grid-cols-[auto_1fr] gap-4 font-alpha calt uppercase text-center text-lg text-white">
<button
className="flex items-center"
onClick={() => setShouldDisplayBanner(false)}
>
<Icon name="cross" className="w-6 h-6" ariaLabel="dismiss" />
</button>
<div>
<span className="pr-4">Mainnet sim 3 is live!</span>
<ExternalLink href="https://fairground.wtf/">Learn more</ExternalLink>
</div>
</div>
</AnnouncementBanner>
);
};
function App() {
return (
<TendermintWebsocketProvider>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<RouterProvider router={router} fallbackElement={splashLoading} />
<div
className={classNames(
'max-w-[1500px] min-h-[100vh]',
'mx-auto my-0',
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
'border-vega-light-200 dark:border-vega-dark-200 lg:border-l lg:border-r',
'antialiased text-black dark:text-white',
'overflow-hidden relative'
)}
>
<div>
<MainnetSimAd />
<Header />
</div>
<div>
<Main />
</div>
<div>
<Footer />
</div>
</div>
<DialogsContainer />
</NetworkLoader>
</TendermintWebsocketProvider>
);
@@ -13,7 +13,7 @@ jest.mock('../search', () => ({
}));
const renderComponent = () => (
<MemoryRouter initialEntries={['/txs']}>
<MemoryRouter>
<Header />
</MemoryRouter>
);
@@ -24,7 +24,6 @@ describe('Header', () => {
expect(screen.getByTestId('navigation')).toHaveTextContent('Explorer');
});
it('should render search', () => {
render(renderComponent());
@@ -1,4 +1,4 @@
import { matchPath, useLocation, useMatch } from 'react-router-dom';
import { matchPath, useLocation } from 'react-router-dom';
import {
ThemeSwitcher,
Navigation,
@@ -13,26 +13,23 @@ import { t } from '@vegaprotocol/i18n';
import { Routes } from '../../routes/route-names';
import { NetworkSwitcher } from '@vegaprotocol/environment';
import type { Navigable } from '../../routes/router-config';
import { isNavigable } from '../../routes/router-config';
import { routerConfig } from '../../routes/router-config';
import routerConfig from '../../routes/router-config';
import { useMemo } from 'react';
import compact from 'lodash/compact';
import { Search } from '../search';
const routeToNavigationItem = (r: Navigable) => (
<NavigationItem key={r.handle.name}>
<NavigationLink to={r.path}>{r.handle.text}</NavigationLink>
<NavigationItem key={r.name}>
<NavigationLink to={r.path}>{r.text}</NavigationLink>
</NavigationItem>
);
export const Header = () => {
const isHome = Boolean(useMatch(Routes.HOME));
const pages = routerConfig[0].children || [];
const mainItems = compact(
[Routes.TX, Routes.BLOCKS, Routes.ORACLES, Routes.VALIDATORS].map((n) =>
pages.find((r) => r.path === n)
routerConfig.find((r) => r.path === n)
)
).filter(isNavigable);
);
const groupedItems = compact(
[
@@ -42,8 +39,8 @@ export const Header = () => {
Routes.GOVERNANCE,
Routes.NETWORK_PARAMETERS,
Routes.GENESIS,
].map((n) => pages.find((r) => r.path === n))
).filter(isNavigable);
].map((n) => routerConfig.find((r) => r.path === n))
);
const { pathname } = useLocation();
@@ -70,7 +67,7 @@ export const Header = () => {
actions={
<>
<ThemeSwitcher />
{!isHome && <Search />}
<Search />
</>
}
onResize={(width, el) => {
@@ -93,7 +90,7 @@ export const Header = () => {
hide={[NavigationBreakpoint.Small, NavigationBreakpoint.Narrow]}
>
{mainItems.map(routeToNavigationItem)}
{groupedItems && groupedItems.length > 0 && (
{groupedItems && (
<NavigationItem>
<NavigationTrigger isActive={Boolean(isOnOther)}>
{t('Other')}
@@ -0,0 +1,9 @@
import { AppRouter } from '../../routes';
export const Main = () => {
return (
<main className="p-4">
<AppRouter />
</main>
);
};
@@ -0,0 +1,32 @@
import { t } from '@vegaprotocol/i18n';
import React from 'react';
interface RouteErrorBoundaryProps {
children: React.ReactElement;
}
export class RouteErrorBoundary extends React.Component<
RouteErrorBoundaryProps,
{ hasError: boolean }
> {
constructor(props: RouteErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
override componentDidCatch(error: Error) {
console.log(`Error caught in App error boundary ${error.message}`, error);
}
override render() {
if (this.state.hasError) {
return <h1>{t('Something went wrong')}</h1>;
}
return this.props.children;
}
}
@@ -1,70 +1,189 @@
import {
determineType,
detectTypeByFetching,
detectTypeFromQuery,
getSearchType,
isBlock,
isHexadecimal,
isNetworkParty,
isNonHex,
SearchTypes,
isHash,
toHex,
toNonHex,
} from './detect-search';
import { DATA_SOURCES } from '../../config';
global.fetch = jest.fn();
describe('Detect Search', () => {
it.each([
['0000000000000000000000000000000000000000000000000000000000000000', true],
['0000000000000000000000000000000000000000000000000000000000000001', true],
[
'LOOONG0000000000000000000000000000000000000000000000000000000000000000',
false,
],
['xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', false],
['something else', false],
])("should detect that it's a hash", (input, expected) => {
expect(isHash(input)).toBe(expected);
it("should detect that it's a hexadecimal", () => {
const expected = true;
const testString =
'0x073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
const actual = isHexadecimal(testString);
expect(actual).toBe(expected);
});
it("should detect that it's not hexadecimal", () => {
const expected = true;
const testString =
'073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
const actual = isNonHex(testString);
expect(actual).toBe(expected);
});
it("should detect that it's a network party", () => {
expect(isNetworkParty('network')).toBe(true);
expect(isNetworkParty('web')).toBe(false);
const expected = true;
const testString = 'network';
const actual = isNetworkParty(testString);
expect(actual).toBe(expected);
});
it("should detect that it's a block", () => {
expect(isBlock('123')).toBe(true);
expect(isBlock('x123')).toBe(false);
const expected = true;
const testString = '3188';
const actual = isBlock(testString);
expect(actual).toBe(expected);
});
it.each([
[
'0000000000000000000000000000000000000000000000000000000000000000',
SearchTypes.Transaction,
],
[
'0000000000000000000000000000000000000000000000000000000000000001',
SearchTypes.Party,
],
['123', SearchTypes.Block],
['network', SearchTypes.Party],
['something else', SearchTypes.Unknown],
])(
"detectTypeByFetching should call fetch with non-hex query it's a transaction",
async (input, type) => {
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok:
input ===
'0000000000000000000000000000000000000000000000000000000000000000',
json: () =>
Promise.resolve({
transaction: {
hash: input,
},
}),
})
)
);
const result = await determineType(input);
expect(result).toBe(type);
}
);
it('should convert from non-hex to hex', () => {
const expected = '0x123';
const testString = '123';
const actual = toHex(testString);
expect(actual).toBe(expected);
});
it('should convert from hex to non-hex', () => {
const expected = '123';
const testString = '0x123';
const actual = toNonHex(testString);
expect(actual).toBe(expected);
});
it("should detect type client side from query if it's a hexadecimal", () => {
const expected = [SearchTypes.Party, SearchTypes.Transaction];
const testString =
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("should detect type client side from query if it's a non hex", () => {
const expected = [SearchTypes.Party, SearchTypes.Transaction];
const testString =
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("should detect type client side from query if it's a network party", () => {
const expected = [SearchTypes.Party];
const testString = 'network';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("should detect type client side from query if it's a block (number)", () => {
const expected = [SearchTypes.Block];
const testString = '23432';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("detectTypeByFetching should call fetch with non-hex query it's a transaction", async () => {
const query = '0xabc';
const type = SearchTypes.Transaction;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
transaction: {
hash: query,
},
}),
})
)
);
const result = await detectTypeByFetching(query);
expect(fetch).toHaveBeenCalledWith(
`${DATA_SOURCES.blockExplorerUrl}/transactions/${toNonHex(query)}`
);
expect(result).toBe(type);
});
it("detectTypeByFetching should call fetch with non-hex query it's a party", async () => {
const query = 'abc';
const type = SearchTypes.Party;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: false,
})
)
);
const result = await detectTypeByFetching(query);
expect(result).toBe(type);
});
it('getSearchType should return party from fetch response', async () => {
const query =
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const expected = SearchTypes.Party;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: false,
})
)
);
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return transaction from fetch response', async () => {
const query =
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const expected = SearchTypes.Transaction;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
transaction: {
hash: query,
},
}),
})
)
);
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return undefined from transaction response', async () => {
const query = 'u';
const expected = undefined;
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return block if query is number', async () => {
const query = '123';
const expected = SearchTypes.Block;
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return party if query is network', async () => {
const query = 'network';
const expected = SearchTypes.Party;
const result = await getSearchType(query);
expect(result).toBe(expected);
});
});
@@ -1,4 +1,3 @@
import { remove0x } from '@vegaprotocol/utils';
import { DATA_SOURCES } from '../../config';
import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response';
@@ -7,20 +6,15 @@ export enum SearchTypes {
Party = 'party',
Block = 'block',
Order = 'order',
Unknown = 'unknown',
}
export const HASH_LENGTH = 64;
export const isHash = (value: string) =>
/[0-9a-fA-F]+/.test(remove0x(value)) &&
remove0x(value).length === HASH_LENGTH;
export const TX_LENGTH = 64;
export const isHexadecimal = (search: string) =>
search.startsWith('0x') && search.length === 2 + HASH_LENGTH;
search.startsWith('0x') && search.length === 2 + TX_LENGTH;
export const isNonHex = (search: string) =>
!search.startsWith('0x') && search.length === HASH_LENGTH;
!search.startsWith('0x') && search.length === TX_LENGTH;
export const isBlock = (search: string) => !Number.isNaN(Number(search));
@@ -29,44 +23,121 @@ export const isNetworkParty = (search: string) => search === 'network';
export const toHex = (query: string) =>
isHexadecimal(query) ? query : `0x${query}`;
export const toNonHex = remove0x;
export const toNonHex = (query: string) =>
isNonHex(query) ? query : `${query.replace('0x', '')}`;
/**
* Determine the type of the given query
*/
export const determineType = async (query: string): Promise<SearchTypes> => {
const value = query.toLowerCase();
if (isHash(value)) {
// it can be either `SearchTypes.Party` or `SearchTypes.Transaction`
if (await isTransactionHash(value)) {
return SearchTypes.Transaction;
} else {
return SearchTypes.Party;
}
} else if (isNetworkParty(value)) {
return SearchTypes.Party;
} else if (isBlock(value)) {
return SearchTypes.Block;
export const detectTypeFromQuery = (
query: string
): SearchTypes[] | undefined => {
const i = query.toLowerCase();
if (isHexadecimal(i) || isNonHex(i)) {
return [SearchTypes.Party, SearchTypes.Transaction];
} else if (isNetworkParty(i)) {
return [SearchTypes.Party];
} else if (isBlock(i)) {
return [SearchTypes.Block];
}
return SearchTypes.Unknown;
return undefined;
};
/**
* Checks if given input is a transaction hash by querying the transactions
* endpoint
*/
export const isTransactionHash = async (input: string): Promise<boolean> => {
const hash = remove0x(input);
export const detectTypeByFetching = async (
query: string
): Promise<SearchTypes | undefined> => {
const hash = toNonHex(query);
const request = await fetch(
`${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
);
if (request?.ok) {
const body: BlockExplorerTransaction = await request.json();
if (body?.transaction) {
return true;
return SearchTypes.Transaction;
}
}
return false;
return SearchTypes.Party;
};
// Code commented out because the current solution to detect a hex is temporary (by process of elimination)
// export const detectTypeByFetching = async (
// query: string,
// type: SearchTypes
// ): Promise<SearchTypes | undefined> => {
// const TYPES = [SearchTypes.Party, SearchTypes.Transaction];
//
// if (!TYPES.includes(type)) {
// throw new Error('Search type provided not recognised');
// }
//
// if (type === SearchTypes.Transaction) {
// const hash = toNonHex(query);
// const request = await fetch(
// `${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
// );
//
// if (request?.ok) {
// const body: BlockExplorerTransaction = await request.json();
//
// if (body?.transaction) {
// return SearchTypes.Transaction;
// }
// }
// } else if (type === SearchTypes.Party) {
// const party = toNonHex(query);
//
// const request = await fetch(
// `${DATA_SOURCES.blockExplorerUrl}/transactions?limit=1&filters[tx.submitter]=${party}`
// );
//
// if (request.ok) {
// const body: BlockExplorerTransactions = await request.json();
//
// if (body?.transactions?.length) {
// return SearchTypes.Party;
// }
// }
// }
//
// return undefined;
// };
// export const getSearchType = async (
// query: string
// ): Promise<SearchTypes | undefined> => {
// const searchTypes = detectTypeFromQuery(query);
// const hasResults = searchTypes?.length;
//
// if (hasResults) {
// if (hasResults > 1) {
// const promises = searchTypes.map((type) =>
// detectTypeByFetching(query, type)
// );
// const results = await Promise.all(promises);
// return results.find((result) => result !== undefined);
// }
//
// return searchTypes[0];
// }
//
// return undefined;
// };
export const getSearchType = async (
query: string
): Promise<SearchTypes | undefined> => {
const searchTypes = detectTypeFromQuery(query);
const hasResults = searchTypes?.length;
if (hasResults) {
if (hasResults > 1) {
return await detectTypeByFetching(query);
}
return searchTypes[0];
}
return undefined;
};
@@ -1,82 +1,157 @@
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { SearchForm } from './search';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { Search } from './search';
import { MemoryRouter } from 'react-router-dom';
import { Routes } from '../../routes/route-names';
import { SearchTypes, getSearchType } from './detect-search';
global.fetch = jest.fn();
const mockedNavigate = jest.fn();
const mockGetSearchType = getSearchType as jest.MockedFunction<
typeof getSearchType
>;
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockedNavigate,
}));
jest.mock('./detect-search', () => ({
...jest.requireActual('./detect-search'),
getSearchType: jest.fn(),
}));
beforeEach(() => {
mockedNavigate.mockClear();
});
const renderComponent = () =>
render(
<MemoryRouter>
<SearchForm />
</MemoryRouter>
);
const renderComponent = () => (
<MemoryRouter>
<Search />
</MemoryRouter>
);
describe('SearchForm', () => {
const getInputs = () => ({
input: screen.getByTestId('search'),
button: screen.getByTestId('search-button'),
});
describe('Search', () => {
it('should render search input and button', () => {
renderComponent();
render(renderComponent());
expect(screen.getByTestId('search')).toBeInTheDocument();
expect(screen.getByTestId('search-button')).toHaveTextContent('Search');
});
it.each([
[
Routes.TX,
'0000000000000000000000000000000000000000000000000000000000000000',
],
[
Routes.PARTIES,
'0000000000000000000000000000000000000000000000000000000000000001',
],
[Routes.BLOCKS, '123'],
[undefined, 'something else'],
])('should redirect to %s', async (route, input) => {
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok:
input ===
'0000000000000000000000000000000000000000000000000000000000000000',
json: () =>
Promise.resolve({
transaction: {
hash: input,
},
}),
})
)
it('should render error if input is not known', async () => {
render(renderComponent());
const { button, input } = getInputs();
fireEvent.change(input, { target: { value: 'asd' } });
fireEvent.click(button);
expect(await screen.findByTestId('search-error')).toHaveTextContent(
'Transaction type is not recognised'
);
renderComponent();
await act(async () => {
fireEvent.change(screen.getByTestId('search'), {
target: {
value: input,
},
});
fireEvent.click(screen.getByTestId('search-button'));
});
it('should render error if no input is given', async () => {
render(renderComponent());
const { button } = getInputs();
fireEvent.click(button);
expect(await screen.findByTestId('search-error')).toHaveTextContent(
'Search query required'
);
});
it('should redirect to transactions page', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Transaction);
fireEvent.change(input, {
target: {
value:
'0x1234567890123456789012345678901234567890123456789012345678901234',
},
});
fireEvent.click(button);
await waitFor(() => {
expect(mockedNavigate).toBeCalledTimes(route ? 1 : 0);
if (route) {
// eslint-disable-next-line jest/no-conditional-expect
expect(mockedNavigate).toBeCalledWith(`${route}/${input}`);
}
expect(mockedNavigate).toBeCalledWith(
`${Routes.TX}/0x1234567890123456789012345678901234567890123456789012345678901234`
);
});
});
it('should redirect to transactions page without proceeding 0x', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Transaction);
fireEvent.change(input, {
target: {
value:
'1234567890123456789012345678901234567890123456789012345678901234',
},
});
fireEvent.click(button);
await waitFor(() => {
expect(mockedNavigate).toBeCalledWith(
`${Routes.TX}/0x1234567890123456789012345678901234567890123456789012345678901234`
);
});
});
it('should redirect to parties page', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Party);
fireEvent.change(input, {
target: {
value:
'0x1234567890123456789012345678901234567890123456789012345678901234',
},
});
fireEvent.click(button);
await waitFor(() => {
expect(mockedNavigate).toBeCalledWith(
`${Routes.PARTIES}/0x1234567890123456789012345678901234567890123456789012345678901234`
);
});
});
it('should redirect to parties page without proceeding 0x', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Party);
fireEvent.change(input, {
target: {
value:
'1234567890123456789012345678901234567890123456789012345678901234',
},
});
fireEvent.click(button);
await waitFor(() => {
expect(mockedNavigate).toBeCalledWith(
`${Routes.PARTIES}/0x1234567890123456789012345678901234567890123456789012345678901234`
);
});
});
it('should redirect to blocks page if passed a number', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Block);
fireEvent.change(input, {
target: {
value: '123',
},
});
fireEvent.click(button);
await waitFor(() => {
expect(mockedNavigate).toBeCalledWith(`${Routes.BLOCKS}/123`);
});
});
});
@@ -1,13 +1,12 @@
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import { t } from '@vegaprotocol/i18n';
import { Button, Input, InputError } from '@vegaprotocol/ui-toolkit';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { getSearchType, SearchTypes, toHex } from './detect-search';
import { Routes } from '../../routes/route-names';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
import { remove0x } from '@vegaprotocol/utils';
import { determineType, isBlock, isHash, SearchTypes } from './detect-search';
interface FormFields {
search: string;
@@ -31,26 +30,101 @@ const MagnifyingGlass = () => (
</svg>
);
const Clear = () => (
<svg
className="w-3 h-3"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.3748 1.37478L1.37478 11.3748L0.625244 10.6252L10.6252 0.625244L11.3748 1.37478Z"
fill="currentColor"
/>
<path
d="M1.37478 0.625244L11.3748 10.6252L10.6252 11.3748L0.625244 1.37478L1.37478 0.625244Z"
fill="currentColor"
/>
</svg>
);
export const Search = () => {
const searchForm = <SearchForm />;
const { register, handleSubmit } = useForm<FormFields>();
const navigate = useNavigate();
const [error, setError] = useState<Error | null>(null);
const onSubmit = useCallback(
async (fields: FormFields) => {
setError(null);
const query = fields.search;
if (!query) {
return setError(new Error(t('Search query required')));
}
const result = await getSearchType(query);
const urlAsHex = toHex(query);
const unrecognisedError = new Error(
t('Transaction type is not recognised')
);
if (result) {
switch (result) {
case SearchTypes.Party:
return navigate(`${Routes.PARTIES}/${urlAsHex}`);
case SearchTypes.Transaction:
return navigate(`${Routes.TX}/${urlAsHex}`);
case SearchTypes.Block:
return navigate(`${Routes.BLOCKS}/${Number(query)}`);
default:
return setError(unrecognisedError);
}
}
return setError(unrecognisedError);
},
[navigate]
);
const searchForm = (
<form className="block min-w-[290px]" onSubmit={handleSubmit(onSubmit)}>
<div className="flex relative items-stretch gap-2 text-xs">
<label htmlFor="search" className="sr-only">
{t('Search by block number or transaction hash')}
</label>
<button
className={classNames(
'absolute top-[50%] translate-y-[-50%] left-2',
'text-vega-light-300 dark:text-vega-dark-300'
)}
>
<MagnifyingGlass />
</button>
<Input
{...register('search')}
id="search"
data-testid="search"
className={classNames(
'peer',
'pl-8 py-2 text-xs',
'border rounded border-vega-light-200 dark:border-vega-dark-200'
)}
hasError={Boolean(error?.message)}
type="text"
placeholder={t('Enter block number, public key or transaction hash')}
/>
{error?.message && (
<div
className={classNames(
'hidden peer-focus:block',
'bg-white dark:bg-black',
'border rounded-b border-t-0 border-vega-light-200 dark:border-vega-dark-200',
'absolute top-[100%] flex-1 w-full pb-2 px-2 text-black dark:text-white'
)}
>
<InputError
data-testid="search-error"
intent="danger"
className="text-xs"
>
{error.message}
</InputError>
</div>
)}
<Button
className="hidden [.search-dropdown_&]:block"
type="submit"
size="xs"
data-testid="search-button"
>
{t('Search')}
</Button>
</div>
</form>
);
const searchTrigger = (
<DropdownMenu.Root>
@@ -80,135 +154,10 @@ export const Search = () => {
return (
<>
<div className="hidden [.nav-search-full_&]:block min-w-[290px]">
{searchForm}
</div>
<div className="hidden [.nav-search-full_&]:block">{searchForm}</div>
<div className="hidden [.nav-search-compact_&]:block">
{searchTrigger}
</div>
</>
);
};
export const SearchForm = () => {
const {
register,
handleSubmit,
setValue,
setError,
clearErrors,
formState,
watch,
} = useForm<FormFields>();
const navigate = useNavigate();
const onSubmit = useCallback(
async (fields: FormFields) => {
clearErrors();
const type = await determineType(fields.search);
if (type) {
switch (type) {
case SearchTypes.Party:
return navigate(`${Routes.PARTIES}/${remove0x(fields.search)}`);
case SearchTypes.Transaction:
return navigate(`${Routes.TX}/${remove0x(fields.search)}`);
case SearchTypes.Block:
return navigate(`${Routes.BLOCKS}/${Number(fields.search)}`);
}
}
setError('search', new Error(t('The search term is not a valid query')));
},
[clearErrors, navigate, setError]
);
const searchQuery = watch('search', '');
return (
<form className="block min-w-[200px]" onSubmit={handleSubmit(onSubmit)}>
<div className="flex relative items-stretch gap-2 text-xs">
<div className="relative w-full">
<label htmlFor="search" className="sr-only">
{t('Search by block number or transaction hash')}
</label>
<button
className={classNames(
'absolute top-[50%] translate-y-[-50%] left-2',
'text-vega-light-300 dark:text-vega-dark-300'
)}
>
<MagnifyingGlass />
</button>
<button
onClick={(e) => {
e.preventDefault();
setValue('search', '');
clearErrors();
}}
className={classNames(
{ hidden: searchQuery.length === 0 },
'absolute top-[50%] translate-y-[-50%] right-2',
'text-vega-light-300 dark:text-vega-dark-300'
)}
>
<Clear />
</button>
<Input
{...register('search', {
required: t('Search query is required'),
validate: (value) =>
isHash(value) ||
isBlock(value) ||
t('Search query has to be a number or a 64 character hash'),
onBlur: () => clearErrors('search'),
})}
id="search"
data-testid="search"
className={classNames(
'pl-8 py-2 text-xs',
{ 'pr-8': searchQuery.length > 1 },
'border rounded border-vega-light-200 dark:border-vega-dark-200',
{
'border-vega-pink dark:border-vega-pink': Boolean(
formState.errors.search
),
}
)}
hasError={Boolean(formState.errors.search)}
type="text"
placeholder={t(
'Enter block number, public key or transaction hash'
)}
/>
</div>
{formState.errors.search && (
<div
className={classNames(
'[nav_&]:border [nav_&]:rounded [nav_&]:border-vega-light-300 [nav_&]:dark:border-vega-light-300',
'[.search-dropdown_&]:border [.search-dropdown_&]:rounded [.search-dropdown_&]:border-vega-light-300 [.search-dropdown_&]:dark:border-vega-light-300',
'bg-white dark:bg-black',
'absolute top-[100%] flex-1 w-full pb-2 px-2 text-black dark:text-white'
)}
>
<InputError
data-testid="search-error"
intent="danger"
className="text-xs"
>
{formState.errors.search.message}
</InputError>
</div>
)}
<Button
variant="primary"
type="submit"
size="xs"
data-testid="search-button"
className="[nav_&]:hidden"
>
{t('Search')}
</Button>
</div>
</form>
);
};
+3 -8
View File
@@ -1,19 +1,14 @@
import { StatsManager } from '@vegaprotocol/network-stats';
import { SearchForm } from '../../components/search';
import { useDocumentTitle } from '../../hooks/use-document-title';
const Home = () => {
const classnames = 'mt-4 mb-4';
const classnames = 'mt-4 grid grid-cols-1 lg:grid-cols-2 lg:gap-4';
useDocumentTitle();
return (
<section>
<div className="p-20 max-sm:py-10 max-sm:px-0">
<SearchForm />
</div>
<div className="px-20 max-sm:px-0">
<StatsManager className={classnames} />
</div>
<StatsManager className={classnames} />
</section>
);
};
+26
View File
@@ -0,0 +1,26 @@
import React from 'react';
import { useRoutes } from 'react-router-dom';
import { RouteErrorBoundary } from '../components/router-error-boundary';
import routerConfig from './router-config';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
export interface RouteChildProps {
name: string;
}
export const AppRouter = () => {
const routes = useRoutes(routerConfig);
const splashLoading = (
<Splash>
<Loader />
</Splash>
);
return (
<RouteErrorBoundary>
<React.Suspense fallback={splashLoading}>{routes}</React.Suspense>
</RouteErrorBoundary>
);
};
-164
View File
@@ -1,164 +0,0 @@
import {
AssetDetailsDialog,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { useEnvironment } from '@vegaprotocol/environment';
import { AnnouncementBanner } from '@vegaprotocol/announcements';
import {
BackgroundVideo,
BreadcrumbsContainer,
ButtonLink,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import {
isRouteErrorResponse,
Link,
Outlet,
useMatch,
useRouteError,
} from 'react-router-dom';
import { Footer } from '../components/footer/footer';
import { Header } from '../components/header';
import { Routes } from './route-names';
const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
return (
<AssetDetailsDialog
assetId={id}
trigger={trigger || null}
asJson={asJson}
open={isOpen}
onChange={setOpen}
/>
);
};
export const Layout = () => {
const isHome = Boolean(useMatch(Routes.HOME));
const { ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
return (
<>
<div
className={classNames(
'max-w-[1500px] min-h-[100vh]',
'mx-auto my-0',
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
'border-vega-light-200 dark:border-vega-dark-200 lg:border-l lg:border-r',
'antialiased text-black dark:text-white',
'overflow-hidden relative'
)}
>
<div>
{ANNOUNCEMENTS_CONFIG_URL && (
<AnnouncementBanner
app="explorer"
configUrl={ANNOUNCEMENTS_CONFIG_URL}
/>
)}
<Header />
</div>
<div>
<main className="p-4">
{!isHome && <BreadcrumbsContainer className="mb-4" />}
<Outlet />
</main>
</div>
<div>
<Footer />
</div>
</div>
<DialogsContainer />
</>
);
};
export const ErrorBoundary = () => {
const error = useRouteError();
const errorTitle = isRouteErrorResponse(error)
? `${error.status} ${error.statusText}`
: t('Something went wrong');
const errorMessage = isRouteErrorResponse(error)
? error.error?.message
: (error as Error).message || JSON.stringify(error);
return (
<>
<BackgroundVideo className="brightness-50" />
<div
className={classNames(
'max-w-[620px] p-2 mt-[10vh]',
'mx-auto my-0',
'antialiased text-white',
'overflow-hidden relative',
'flex flex-col gap-2'
)}
>
<div className="flex gap-4">
<div>{GHOST}</div>
<h1 className="text-[2.7rem] font-alpha calt break-words uppercase">
{errorTitle}
</h1>
</div>
<div className="text-sm mt-10 overflow-auto break-all font-mono">
{errorMessage}
</div>
<div>
<ButtonLink onClick={() => window.location.reload()}>
{t('Try refreshing')}
</ButtonLink>{' '}
{t('or go back to')}{' '}
<Link className="underline" to="/">
{t('Home')}
</Link>
</div>
</div>
</>
);
};
const GHOST = (
<svg
width="56"
height="85"
viewBox="0 0 56 85"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M41 0.5H3V60.5H41V0.5Z" fill="white" />
<path d="M15 18.5H13V20.5H15V18.5Z" fill="black" />
<path d="M17 20.5H15V22.5H17V20.5Z" fill="black" />
<path d="M19 18.5H17V20.5H19V18.5Z" fill="black" />
<path d="M15 22.5H13V24.5H15V22.5Z" fill="black" />
<path d="M19 22.5H17V24.5H19V22.5Z" fill="black" />
<path d="M29 28.5H15V30.5H29V28.5Z" fill="black" />
<path d="M27 18.5H25V20.5H27V18.5Z" fill="black" />
<path d="M29 20.5H27V22.5H29V20.5Z" fill="black" />
<path d="M31 18.5H29V20.5H31V18.5Z" fill="black" />
<path d="M27 22.5H25V24.5H27V22.5Z" fill="black" />
<path d="M31 22.5H29V24.5H31V22.5Z" fill="black" />
<path d="M31 26.5H29V28.5H31V26.5Z" fill="black" />
<path d="M19 60.5H17V84.5H19V60.5Z" fill="black" />
<path d="M27 60.5H25V84.5H27V60.5Z" fill="black" />
<path
d="M3 42.5V58.64V60.5V64.5H21V60.5H23V64.5H41V60.5V58.64V42.5H3Z"
fill="#FF077F"
/>
<path d="M35 46.5H41V42.5H3V46.5H31H35Z" fill="#CB0666" />
<path d="M3 32.32V29.5L0 32.5V60.5H2V33.33L3 32.32Z" fill="black" />
<path d="M41 31.8V29.49L54.79 21.53L55.79 23.26L41 31.8Z" fill="black" />
<path d="M36 54.5H35V55.5H36V54.5Z" fill="black" />
<path d="M35 53.5H34V54.5H35V53.5Z" fill="black" />
<path d="M34 48.5H33V53.5H34V48.5Z" fill="black" />
<path d="M38 48.5H37V52.5H38V48.5Z" fill="black" />
<path d="M37 53.5H36V54.5H37V53.5Z" fill="black" />
<path d="M39 52.5H38V53.5H39V52.5Z" fill="black" />
<path
d="M55.7901 23.27L53.0601 22.54L45.1001 8.75L46.8301 7.75L55.7901 23.27Z"
fill="black"
/>
</svg>
);
+77 -201
View File
@@ -8,6 +8,7 @@ import { Oracle } from './oracles/id';
import Party from './parties';
import { Parties } from './parties/home';
import { Party as PartySingle } from './parties/id';
import Txs from './txs';
import { ValidatorsPage } from './validators';
import Genesis from './genesis';
import { Block } from './blocks/id';
@@ -19,55 +20,23 @@ import flags from '../config/flags';
import { t } from '@vegaprotocol/i18n';
import { Routes } from './route-names';
import { NetworkParameters } from './network-parameters';
import type { Params, RouteObject } from 'react-router-dom';
import { createBrowserRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
import type { RouteObject } from 'react-router-dom';
import { MarketPage, MarketsPage } from './markets';
import type { ReactNode } from 'react';
import { ErrorBoundary, Layout } from './layout';
import compact from 'lodash/compact';
import { AssetLink, MarketLink } from '../components/links';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { remove0x } from '@vegaprotocol/utils';
export type Navigable = {
path: string;
handle: {
name: string;
text: string;
};
};
export const isNavigable = (item: RouteObject): item is Navigable =>
(item as Navigable).path !== undefined &&
(item as Navigable).handle !== undefined &&
(item as Navigable).handle.name !== undefined &&
(item as Navigable).handle.text !== undefined;
export type Breadcrumbable = {
handle: { breadcrumb: (data?: Params<string>) => ReactNode | string };
};
export const isBreadcrumbable = (item: RouteObject): item is Breadcrumbable =>
(item as Breadcrumbable).handle !== undefined &&
(item as Breadcrumbable).handle.breadcrumb !== undefined;
type RouteItem =
| RouteObject
| (RouteObject & Navigable)
| (RouteObject & Breadcrumbable);
type Route = RouteItem & {
children?: RouteItem[];
name: string;
text: string;
};
type Route = RouteObject & Navigable;
const partiesRoutes: Route[] = flags.parties
? [
{
path: Routes.PARTIES,
name: t('Parties'),
text: t('Parties'),
element: <Party />,
handle: {
name: t('Parties'),
text: t('Parties'),
breadcrumb: () => <Link to={Routes.PARTIES}>{t('Parties')}</Link>,
},
children: [
{
index: true,
@@ -76,13 +45,6 @@ const partiesRoutes: Route[] = flags.parties
{
path: ':party',
element: <PartySingle />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.PARTIES, params.party)}>
{truncateMiddle(params.party as string)}
</Link>
),
},
},
],
},
@@ -93,11 +55,8 @@ const assetsRoutes: Route[] = flags.assets
? [
{
path: Routes.ASSETS,
handle: {
name: t('Assets'),
text: t('Assets'),
breadcrumb: () => <Link to={Routes.ASSETS}>{t('Assets')}</Link>,
},
text: t('Assets'),
name: t('Assets'),
children: [
{
index: true,
@@ -106,11 +65,6 @@ const assetsRoutes: Route[] = flags.assets
{
path: ':assetId',
element: <AssetPage />,
handle: {
breadcrumb: (params: Params<string>) => (
<AssetLink assetId={params.assetId as string} />
),
},
},
],
},
@@ -121,13 +75,8 @@ const genesisRoutes: Route[] = flags.genesis
? [
{
path: Routes.GENESIS,
handle: {
name: t('Genesis'),
text: t('Genesis Parameters'),
breadcrumb: () => (
<Link to={Routes.GENESIS}>{t('Genesis Parameters')}</Link>
),
},
name: t('Genesis'),
text: t('Genesis Parameters'),
element: <Genesis />,
},
]
@@ -137,13 +86,8 @@ const governanceRoutes: Route[] = flags.governance
? [
{
path: Routes.GOVERNANCE,
handle: {
name: t('Governance proposals'),
text: t('Governance Proposals'),
breadcrumb: () => (
<Link to={Routes.GOVERNANCE}>{t('Governance Proposals')}</Link>
),
},
name: t('Governance proposals'),
text: t('Governance Proposals'),
element: <Proposals />,
},
]
@@ -153,11 +97,8 @@ const marketsRoutes: Route[] = flags.markets
? [
{
path: Routes.MARKETS,
handle: {
name: t('Markets'),
text: t('Markets'),
breadcrumb: () => <Link to={Routes.MARKETS}>{t('Markets')}</Link>,
},
name: t('Markets'),
text: t('Markets'),
children: [
{
index: true,
@@ -166,11 +107,6 @@ const marketsRoutes: Route[] = flags.markets
{
path: ':marketId',
element: <MarketPage />,
handle: {
breadcrumb: (params: Params<string>) => (
<MarketLink id={params.marketId as string} />
),
},
},
],
},
@@ -181,15 +117,8 @@ const networkParametersRoutes: Route[] = flags.networkParameters
? [
{
path: Routes.NETWORK_PARAMETERS,
handle: {
name: t('NetworkParameters'),
text: t('Network Parameters'),
breadcrumb: () => (
<Link to={Routes.NETWORK_PARAMETERS}>
{t('Network Parameters')}
</Link>
),
},
name: t('NetworkParameters'),
text: t('Network Parameters'),
element: <NetworkParameters />,
},
]
@@ -199,133 +128,80 @@ const validators: Route[] = flags.validators
? [
{
path: Routes.VALIDATORS,
handle: {
name: t('Validators'),
text: t('Validators'),
breadcrumb: () => (
<Link to={Routes.VALIDATORS}>{t('Validators')}</Link>
),
},
name: t('Validators'),
text: t('Validators'),
element: <ValidatorsPage />,
},
]
: [];
const linkTo = (...segments: (string | undefined)[]) =>
compact(segments).join('/');
export const routerConfig: Route[] = [
const routerConfig: Route[] = [
{
path: Routes.HOME,
element: <Layout />,
handle: {
name: t('Home'),
text: t('Home'),
breadcrumb: () => <Link to={Routes.HOME}>{t('Home')}</Link>,
},
errorElement: <ErrorBoundary />,
name: t('Home'),
text: t('Home'),
element: <Home />,
index: true,
},
{
path: Routes.TX,
name: t('Txs'),
text: t('Transactions'),
element: <Txs />,
children: [
{
path: 'pending',
element: <PendingTxs />,
},
{
path: ':txHash',
element: <Tx />,
},
{
index: true,
element: <TxsList />,
},
],
},
{
path: Routes.BLOCKS,
name: t('Blocks'),
text: t('Blocks'),
element: <BlockPage />,
children: [
{
index: true,
element: <Home />,
element: <Blocks />,
},
{
path: Routes.TX,
handle: {
name: t('Txs'),
text: t('Transactions'),
breadcrumb: () => <Link to={Routes.TX}>{t('Transactions')}</Link>,
},
children: [
{
path: 'pending',
element: <PendingTxs />,
handle: {
breadcrumb: () => (
<Link to={linkTo(Routes.TX, 'pending')}>
{t('Pending transactions')}
</Link>
),
},
},
{
path: ':txHash',
element: <Tx />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.TX, params.txHash)}>
{truncateMiddle(remove0x(params.txHash as string))}
</Link>
),
},
},
{
index: true,
element: <TxsList />,
},
],
path: ':block',
element: <Block />,
},
{
path: Routes.BLOCKS,
handle: {
name: t('Blocks'),
text: t('Blocks'),
breadcrumb: () => <Link to={Routes.BLOCKS}>{t('Blocks')}</Link>,
},
element: <BlockPage />,
children: [
{
index: true,
element: <Blocks />,
},
{
path: ':block',
element: <Block />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.BLOCKS, params.block)}>
{params.block}
</Link>
),
},
},
],
},
{
path: Routes.ORACLES,
handle: {
name: t('Oracles'),
text: t('Oracles'),
breadcrumb: () => <Link to={Routes.ORACLES}>{t('Oracles')}</Link>,
},
element: <OraclePage />,
children: [
{
index: true,
element: <Oracles />,
},
{
path: ':id',
element: <Oracle />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.ORACLES, params.id)}>
{truncateMiddle(params.id as string)}
</Link>
),
},
},
],
},
...partiesRoutes,
...assetsRoutes,
...genesisRoutes,
...governanceRoutes,
...marketsRoutes,
...networkParametersRoutes,
...validators,
],
},
{
path: Routes.ORACLES,
name: t('Oracles'),
text: t('Oracles'),
element: <OraclePage />,
children: [
{
index: true,
element: <Oracles />,
},
{
path: ':id',
element: <Oracle />,
},
],
},
...partiesRoutes,
...assetsRoutes,
...genesisRoutes,
...governanceRoutes,
...marketsRoutes,
...networkParametersRoutes,
...validators,
];
export const router = createBrowserRouter(routerConfig);
export default routerConfig;
+16 -1
View File
@@ -1,4 +1,5 @@
import { useParams } from 'react-router-dom';
import React from 'react';
import { Link, useParams } from 'react-router-dom';
import { remove0x } from '@vegaprotocol/utils';
import { useFetch } from '@vegaprotocol/react-helpers';
import { DATA_SOURCES } from '../../../config';
@@ -7,6 +8,9 @@ import { TxDetails } from './tx-details';
import type { BlockExplorerTransaction } from '../../../routes/types/block-explorer-response';
import { toNonHex } from '../../../components/search/detect-search';
import { PageHeader } from '../../../components/page-header';
import { Routes } from '../../../routes/route-names';
import { IconNames } from '@blueprintjs/icons';
import { Icon } from '@vegaprotocol/ui-toolkit';
import { useDocumentTitle } from '../../../hooks/use-document-title';
const Tx = () => {
@@ -31,6 +35,17 @@ const Tx = () => {
return (
<section>
<Link
className="font-normal underline underline-offset-4 block mb-5"
to={`/${Routes.TX}`}
>
<Icon
className="text-vega-light-150 dark:text-vega-light-150"
name={IconNames.CHEVRON_LEFT}
/>
All Transactions
</Link>
<PageHeader
title="transaction"
truncateStart={5}
+4 -1
View File
@@ -1,4 +1,5 @@
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './styles.css';
import App from './app/app';
@@ -9,6 +10,8 @@ const root = rootElement && createRoot(rootElement);
root?.render(
<StrictMode>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);
-2
View File
@@ -13,8 +13,6 @@ NX_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
#Test configuration variables
CYPRESS_FAIRGROUND=false
@@ -56,7 +56,7 @@ describe(
navigateTo(navigation.proposals);
});
// 3001-VOTE-050 3001-VOTE-054 3001-VOTE-055 3002-PROP-019
// 3001-VOTE-055
it('Newly created raw proposal details - shows proposal title and full description', function () {
createRawProposal();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
@@ -128,7 +128,7 @@ context(
.and('be.visible');
});
// 3001-VOTE-048 3001-VOTE-049 3001-VOTE-050
// 3001-VOTE-048 3001-VOTE-049
it('Able to fail proposal due to lack of participation', function () {
const proposalTitle = 'Add New free form proposal with short enactment';
const proposalTx = createFreeFormProposalTxBody();
@@ -19,7 +19,6 @@ import {
waitForSpinner,
navigateTo,
navigation,
closeDialog,
} from '../../support/common.functions';
import {
clickOnValidatorFromList,
@@ -42,6 +41,7 @@ const vegaWalletNameElement = '[data-testid="wallet-name"]';
const vegaWallet = '[data-testid="vega-wallet"]';
const connectToVegaWalletButton = '[data-testid="connect-to-vega-wallet-btn"]';
const newProposalSubmitButton = '[data-testid="proposal-submit"]';
const dialogCloseButton = '[data-testid="dialog-close"]';
const viewProposalButton = '[data-testid="view-proposal-btn"]';
const rawProposalData = '[data-testid="proposal-data"]';
const minVoteButton = '[data-testid="min-vote"]';
@@ -177,7 +177,7 @@ context(
'be.visible'
);
cy.contains('Proposal rejected', proposalTimeout).should('be.visible');
closeDialog();
cy.get(dialogCloseButton).click();
waitForProposalSync();
navigateTo(navigation.proposals);
cy.get(rejectProposalsLink).click();
@@ -214,7 +214,7 @@ context(
enterRawProposalBody(createTenDigitUnixTimeStampForSpecifiedDays(8));
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should('have.text', errorMsg);
closeDialog();
cy.get(dialogCloseButton).click();
});
// 3002-PROP-009
@@ -227,7 +227,7 @@ context(
enterRawProposalBody(createTenDigitUnixTimeStampForSpecifiedDays(8));
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should('have.text', errorMsg);
closeDialog();
cy.get(dialogCloseButton).click();
});
it('Unable to create a freeform proposal - when json parent section contains unexpected field', function () {
@@ -251,7 +251,7 @@ context(
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should('have.text', errorMsg);
closeDialog();
cy.get(dialogCloseButton).click();
cy.get(rawProposalData)
.invoke('val')
.should('contain', "i shouldn't be here");
@@ -279,7 +279,7 @@ context(
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should('have.text', errorMsg);
closeDialog();
cy.get(dialogCloseButton).click();
});
// 1005-PROP-009
@@ -1,5 +1,4 @@
import {
closeDialog,
navigateTo,
navigation,
waitForSpinner,
@@ -40,6 +39,7 @@ const maxVoteDeadline = '[data-testid="max-vote"]';
const minValidationDeadline = '[data-testid="min-validation"]';
const minEnactDeadline = '[data-testid="min-enactment"]';
const maxEnactDeadline = '[data-testid="max-enactment"]';
const dialogCloseButton = '[data-testid="dialog-close"]';
const inputError = '[data-testid="input-error-text"]';
const enactmentDeadlineError =
'[data-testid="enactment-before-voting-deadline"]';
@@ -48,7 +48,6 @@ const feedbackError = '[data-testid="Error"]';
const viewProposalBtn = 'view-proposal-btn';
const liquidityVoteStatus = 'liquidity-votes-status';
const tokenVoteStatus = 'token-votes-status';
const proposalTermsSection = 'proposal';
const vegaWalletPublicKey = Cypress.env('vegaWalletPublicKey');
const epochTimeout = Cypress.env('epochTimeout');
const proposalTimeout = { timeout: 14000 };
@@ -69,6 +68,7 @@ context(
{ tags: '@slow' },
function () {
before('connect wallets and set approval limit', function () {
cy.createMarket();
cy.visit('/');
vegaWalletSetSpecifiedApprovalAmount('1000');
});
@@ -78,7 +78,6 @@ context(
waitForSpinner();
cy.connectVegaWallet();
ethereumWalletConnect();
cy.createMarket();
ensureSpecifiedUnstakedTokensAreAssociated('1');
navigateTo(navigation.proposals);
});
@@ -195,7 +194,7 @@ context(
'have.text',
'Invalid params: proposal_submission.terms.closing_timestamp (cannot be after enactment time)'
);
closeDialog();
cy.get(dialogCloseButton).click();
cy.get(minVoteDeadline).click();
cy.get(enactmentDeadlineError).should('not.exist');
});
@@ -287,7 +286,7 @@ context(
);
});
// 3001-VOTE-092 3004-PMAC-001
// 3001-VOTE-092
it('Able to submit update market proposal and vote for proposal', function () {
vegaWalletFaucetAssetsWithoutCheck(
'fUSDC',
@@ -342,15 +341,14 @@ context(
'Currently expected to pass'
);
getProposalInformationFromTable('Expected to pass')
.contains('👍 by token vote')
.contains('👍 by Token vote')
.should('be.visible');
});
// 3001-VOTE-026 3001-VOTE-027 3001-VOTE-028 3001-VOTE-095 3001-VOTE-096 3005-PASN-001
it('Able to submit new asset proposal using min deadlines', function () {
const proposalTitle = 'Test new asset proposal';
goToMakeNewProposal(governanceProposalType.NEW_ASSET);
cy.get(newProposalTitle).type(proposalTitle);
cy.get(newProposalTitle).type('Test new asset proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-asset').then((newAssetProposal) => {
const newAssetPayload = JSON.stringify(newAssetProposal);
@@ -369,7 +367,7 @@ context(
cy.contains('Proposal waiting for node vote', proposalTimeout).should(
'be.visible'
);
closeDialog();
cy.get(dialogCloseButton).click();
cy.get(newProposalSubmitButton).should('be.visible').click();
// cannot submit a proposal with ERC20 address already in use
cy.contains('Proposal rejected', proposalTimeout).should('be.visible');
@@ -379,17 +377,6 @@ context(
'PROPOSAL_ERROR_ERC20_ADDRESS_ALREADY_IN_USE'
);
});
closeDialog();
navigateTo(navigation.proposals);
cy.contains(proposalTitle)
.parentsUntil(proposalListItem)
.within(() => {
cy.getByTestId(viewProposalBtn).click();
});
cy.getByTestId(proposalTermsSection).within(() => {
cy.contains('USDT Coin').should('be.visible');
cy.contains('USDT').should('be.visible');
});
});
it('Unable to submit new asset proposal with missing/invalid fields', function () {
@@ -428,15 +415,9 @@ context(
getProposalInformationFromTable('Proposed enactment') // 3001-VOTE-044
.invoke('text')
.should('not.be.empty');
// 3001-VOTE-030 3001-VOTE-031
cy.getByTestId(proposalTermsSection).within(() => {
cy.contains('UpdateAsset').should('be.visible');
cy.contains('UpdateERC20').should('be.visible');
cy.contains('"lifetimeLimit": "10"').should('be.visible');
});
});
it.only('Able to submit update asset proposal using max deadline', function () {
it('Able to submit update asset proposal using max deadline', function () {
goToMakeNewProposal(governanceProposalType.UPDATE_ASSET);
enterUpdateAssetProposalDetails();
cy.get(maxVoteDeadline).click();
@@ -25,24 +25,22 @@ import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
const stakeValidatorListTotalStake = 'total-stake';
const stakeValidatorListTotalShare = 'total-stake-share';
const stakeValidatorListStakePercentage = 'stake-percentage';
const userStakeBtn = 'my-stake-btn';
const userStake = 'user-stake';
const userStakeShare = 'user-stake-share';
const viewAllValidatorsToggle = 'validators-view-toggle-all';
const viewStakedByMeToggle = 'validators-view-toggle-myStake';
const stakeRemoveStakeRadioButton = 'remove-stake-radio';
const stakeTokenAmountInputBox = 'token-amount-input';
const stakeTokenSubmitButton = 'token-input-submit-button';
const stakeAddStakeRadioButton = 'add-stake-radio';
const stakeMaximumTokens = 'token-amount-use-maximum';
const vegaWalletAssociatedBalance = 'currency-value';
const vegaWalletStakedBalances = 'vega-wallet-balance-staked-validators';
const ethWalletContainer = 'ethereum-wallet';
const vegaWallet = 'vega-wallet';
const stakeValidatorListTotalStake = '[col-id="stake"] > div > span';
const stakeValidatorListTotalShare = '[col-id="stakeShare"] > div > span';
const stakeValidatorListValidatorStake = '[col-id="stake"] > div > span';
const stakeRemoveStakeRadioButton = '[data-testid="remove-stake-radio"]';
const stakeTokenAmountInputBox = '[data-testid="token-amount-input"]';
const stakeTokenSubmitButton = '[data-testid="token-input-submit-button"]';
const stakeAddStakeRadioButton = '[data-testid="add-stake-radio"]';
const stakeMaximumTokens = '[data-testid="token-amount-use-maximum"]';
const totalStake = '[data-testid="total-stake"]';
const stakeShare = '[data-testid="stake-percentage"]';
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
const vegaWalletStakedBalances =
'[data-testid="vega-wallet-balance-staked-validators"]';
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const vegaWallet = '[data-testid="vega-wallet"]';
const txTimeout = Cypress.env('txTimeout');
const epochTimeout = Cypress.env('epochTimeout');
@@ -93,39 +91,6 @@ context(
validateValidatorListTotalStakeAndShare('0', '2.00', '100.00%');
});
it('Able to view validators staked by me', function () {
ensureSpecifiedUnstakedTokensAreAssociated('4');
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('2');
closeStakingDialog();
navigateTo(navigation.validators);
cy.getByTestId(userStake, epochTimeout)
.first()
.should('have.text', '2.00');
cy.getByTestId('total-stake').first().realHover();
cy.getByTestId('staked-by-user-tooltip')
.first()
.should('have.text', 'Staked by me: 2.00');
cy.getByTestId('total-pending-stake').first().realHover();
cy.getByTestId('pending-user-stake-tooltip')
.first()
.should('have.text', 'My pending stake: 0.00');
cy.getByTestId(userStakeShare).invoke('text').should('not.be.empty'); // Adjust when #3286 is resolved
cy.getByTestId(userStakeBtn).should('exist').click();
verifyThisEpochValue(2.0);
navigateTo(navigation.validators);
cy.getByTestId(viewStakedByMeToggle).click();
cy.getByTestId(userStakeBtn).should('have.length', 1);
cy.getByTestId(viewAllValidatorsToggle).click();
clickOnValidatorFromList(1);
stakingValidatorPageAddStake('2');
closeStakingDialog();
navigateTo(navigation.validators);
cy.getByTestId(viewStakedByMeToggle).click();
cy.getByTestId(userStakeBtn).should('have.length', 2);
});
it('Able to stake against a validator - using vega from vesting contract', function () {
stakingPageAssociateTokens('3', { type: 'contract' });
verifyUnstakedBalance(3.0);
@@ -144,13 +109,14 @@ context(
});
it('Able to stake against a validator - using vega from both wallet and vesting contract', function () {
vegaWalletTeardown();
stakingPageAssociateTokens('3', { type: 'contract' });
navigateTo(navigation.validators);
stakingPageAssociateTokens('4', { type: 'wallet' });
verifyUnstakedBalance(7.0);
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletTotalAssociatedBalance('4.0');
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletTotalAssociatedBalance('4.0');
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('6');
@@ -170,7 +136,7 @@ context(
clickOnValidatorFromList(0);
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(3.0);
cy.getByTestId(vegaWalletStakedBalances, txTimeout)
cy.get(vegaWalletStakedBalances, txTimeout)
.parent()
.should('contain', 2.0, txTimeout);
closeStakingDialog();
@@ -178,36 +144,36 @@ context(
clickOnValidatorFromList(1);
stakingValidatorPageAddStake('1');
verifyUnstakedBalance(2.0);
cy.getByTestId(vegaWalletStakedBalances, txTimeout)
cy.get(vegaWalletStakedBalances, txTimeout)
.should('have.length', 4, txTimeout)
.eq(0)
.should('contain', 2.0, txTimeout);
cy.getByTestId(vegaWalletStakedBalances, txTimeout)
cy.get(vegaWalletStakedBalances, txTimeout)
.eq(1)
.should('contain', 1.0, txTimeout);
closeStakingDialog();
navigateTo(navigation.validators);
cy.get(`[row-id="${0}"]`).within(() => {
cy.getByTestId(stakeValidatorListTotalStake)
cy.get(stakeValidatorListTotalStake)
.should('have.text', '2.00')
.and('be.visible');
cy.getByTestId(stakeValidatorListTotalShare)
cy.get(stakeValidatorListTotalShare)
.should('have.text', '66.67%')
.and('be.visible');
cy.getByTestId(stakeValidatorListTotalStake)
cy.get(stakeValidatorListValidatorStake)
.scrollIntoView()
.should('have.text', '2.00')
.and('be.visible');
});
cy.get(`[row-id="${1}"]`).within(() => {
cy.getByTestId(stakeValidatorListTotalStake)
cy.get(stakeValidatorListTotalStake)
.scrollIntoView()
.should('have.text', '1.00')
.and('be.visible');
cy.getByTestId(stakeValidatorListTotalShare)
cy.get(stakeValidatorListTotalShare)
.should('have.text', '33.33%')
.and('be.visible');
cy.getByTestId(stakeValidatorListTotalStake)
cy.get(stakeValidatorListValidatorStake)
.scrollIntoView()
.should('have.text', '1.00')
.and('be.visible');
@@ -237,15 +203,9 @@ context(
verifyStakedBalance(2.0);
verifyNextEpochValue(2.0);
verifyThisEpochValue(2.0);
cy.getByTestId(stakeValidatorListTotalStake, epochTimeout).should(
'contain.text',
'2'
);
cy.get(totalStake, epochTimeout).should('contain.text', '2');
waitForBeginningOfEpoch();
cy.getByTestId(stakeValidatorListStakePercentage).should(
'have.text',
'100%'
);
cy.get(stakeShare).should('have.text', '100%');
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '2.00', '100.00%');
}
@@ -267,16 +227,12 @@ context(
verifyUnstakedBalance(3.0);
verifyNextEpochValue(0.0);
verifyThisEpochValue(0.0);
cy.getByTestId(vegaWalletStakedBalances, txTimeout).should(
cy.get(vegaWalletStakedBalances, txTimeout).should(
'not.exist',
txTimeout
);
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '0.00', '0.00%');
cy.getByTestId(userStakeBtn).should('not.exist');
cy.getByTestId(userStake).should('not.exist');
cy.getByTestId(userStakeShare).should('not.exist');
});
it('Unable to remove a stake with a negative value for a validator', function () {
@@ -290,10 +246,10 @@ context(
closeStakingDialog();
navigateTo(navigation.validators);
clickOnValidatorFromList(0);
cy.getByTestId(stakeRemoveStakeRadioButton, txTimeout).click();
cy.getByTestId(stakeTokenAmountInputBox).type('-0.1');
cy.get(stakeRemoveStakeRadioButton, txTimeout).click();
cy.get(stakeTokenAmountInputBox).type('-0.1');
cy.contains('Waiting for next epoch to start', epochTimeout);
cy.getByTestId(stakeTokenSubmitButton)
cy.get(stakeTokenSubmitButton)
.should('be.disabled', epochTimeout)
.and('contain', `Remove -0.1 $VEGA tokens at the end of epoch`)
.and('be.visible');
@@ -310,10 +266,10 @@ context(
closeStakingDialog();
navigateTo(navigation.validators);
clickOnValidatorFromList(0);
cy.getByTestId(stakeRemoveStakeRadioButton).click();
cy.getByTestId(stakeTokenAmountInputBox).type('4');
cy.get(stakeRemoveStakeRadioButton).click();
cy.get(stakeTokenAmountInputBox).type('4');
cy.contains('Waiting for next epoch to start', epochTimeout);
cy.getByTestId(stakeTokenSubmitButton)
cy.get(stakeTokenSubmitButton)
.should('be.disabled', epochTimeout)
.and('contain', `Remove 4 $VEGA tokens at the end of epoch`)
.and('be.visible');
@@ -329,17 +285,17 @@ context(
verifyStakedBalance(2.0);
closeStakingDialog();
stakingPageDisassociateAllTokens();
cy.getByTestId(ethWalletContainer).within(() => {
cy.get(ethWalletContainer).within(() => {
cy.contains(vegaWalletPublicKeyShort, txTimeout).should('not.exist');
});
verifyEthWalletTotalAssociatedBalance('0.0');
cy.getByTestId(vegaWallet).within(() => {
cy.getByTestId(vegaWalletAssociatedBalance, txTimeout).should(
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'0.00'
);
});
cy.getByTestId(vegaWalletStakedBalances, txTimeout).should(
cy.get(vegaWalletStakedBalances, txTimeout).should(
'not.exist',
txTimeout
);
@@ -357,17 +313,17 @@ context(
verifyStakedBalance(2.0);
closeStakingDialog();
stakingPageDisassociateAllTokens('contract');
cy.getByTestId(ethWalletContainer).within(() => {
cy.get(ethWalletContainer).within(() => {
cy.contains(vegaWalletPublicKeyShort, txTimeout).should('not.exist');
});
verifyEthWalletTotalAssociatedBalance('0.0');
cy.getByTestId(vegaWallet).within(() => {
cy.getByTestId(vegaWalletAssociatedBalance, txTimeout).should(
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'0.00'
);
});
cy.getByTestId(vegaWalletStakedBalances, txTimeout).should(
cy.get(vegaWalletStakedBalances, txTimeout).should(
'not.exist',
txTimeout
);
@@ -386,8 +342,8 @@ context(
closeStakingDialog();
stakingPageDisassociateTokens('1');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.getByTestId(vegaWallet).within(() => {
cy.getByTestId(vegaWalletAssociatedBalance, txTimeout).should(
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'2.00'
);
@@ -453,8 +409,8 @@ context(
verifyUnstakedBalance(0.0);
closeStakingDialog();
stakingPageAssociateTokens('6');
cy.getByTestId(vegaWallet).within(() => {
cy.getByTestId(vegaWalletAssociatedBalance, txTimeout).should(
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'12.00'
);
@@ -473,12 +429,9 @@ context(
verifyUnstakedBalance(1.0);
closeStakingDialog();
clickOnValidatorFromList(0);
cy.getByTestId(stakeAddStakeRadioButton).click();
cy.getByTestId(stakeMaximumTokens, { timeout: 60000 }).click();
cy.getByTestId(stakeTokenSubmitButton).should(
'contain',
'Add 1 $VEGA tokens'
);
cy.get(stakeAddStakeRadioButton).click();
cy.get(stakeMaximumTokens, { timeout: 60000 }).click();
cy.get(stakeTokenSubmitButton).should('contain', 'Add 1 $VEGA tokens');
});
afterEach('Teardown Wallet', function () {
@@ -183,7 +183,6 @@ context(
// 1004-ASSO-018
// 1004-ASSO-024
// 1004-ASSO-023
// 1004-ASSO-032
stakingPageAssociateTokens('2', {
type: 'contract',
@@ -6,203 +6,159 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
cy.get('nav', { timeout: 10000 }).should('be.visible');
});
describe('Links and buttons', function () {
it('should have link for proposal page', function () {
cy.getByTestId('home-proposals').within(() => {
cy.get('[href="/proposals"]')
.should('exist')
.and('have.text', 'Browse, vote, and propose');
});
});
it('should display announcement banner', function () {
cy.getByTestId('app-announcement')
.should('be.visible')
.within(() => {
cy.getByTestId('external-link').should('exist');
describe('with wallets disconnected', function () {
describe('Links and buttons', function () {
it('should have link for proposal page', function () {
cy.getByTestId('home-proposals').within(() => {
cy.get('[href="/proposals"]')
.should('exist')
.and('have.text', 'Browse, vote, and propose');
});
cy.getByTestId('app-announcement-close').should('be.visible').click();
cy.getByTestId('app-announcement').should('not.exist');
});
it('should show open or enacted proposals with proposal summary', function () {
cy.get('body').then(($body) => {
if (!$body.find('[data-testid="proposals-list-item"]').length) {
cy.createMarket();
cy.reload();
waitForSpinner();
}
});
cy.getByTestId('proposals-list-item')
.should('have.length.at.least', 1)
.first()
.within(() => {
cy.getByTestId('proposal-title')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-type').invoke('text').should('not.be.empty');
cy.getByTestId('proposal-description')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-details')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-status')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('vote-details').invoke('text').should('not.be.empty');
cy.getByTestId('view-proposal-btn').should('be.visible');
it('should show open or enacted proposals with proposal summary', function () {
cy.get('body').then(($body) => {
if (!$body.find('[data-testid="proposals-list-item"]').length) {
cy.createMarket();
cy.reload();
waitForSpinner();
}
});
});
it('should have external link for governance', function () {
cy.getByTestId('home-proposals').within(() => {
cy.getByTestId('external-link')
.should('have.attr', 'href')
.and('contain', 'https://vega.xyz/governance');
});
});
it('should have link for validator page', function () {
cy.getByTestId('home-validators').within(() => {
cy.get('[href="/validators"]')
cy.getByTestId('proposals-list-item')
.should('have.length.at.least', 1)
.first()
.should('exist')
.and('have.text', 'Browse, and stake');
.within(() => {
cy.getByTestId('proposal-title')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-type')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-description')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-details')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-status')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('vote-details')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('view-proposal-btn').should('be.visible');
});
});
});
it('should have external link for validators', function () {
cy.getByTestId('home-validators').within(() => {
cy.getByTestId('external-link')
.should('have.attr', 'href')
.and(
'contain',
'https://community.vega.xyz/c/mainnet-validator-candidates'
);
});
});
it('should have information on active nodes', function () {
cy.getByTestId('node-information')
.first()
.should('contain.text', '2')
.and('contain.text', 'active nodes');
});
it('should have information on consensus nodes', function () {
cy.getByTestId('node-information')
.last()
.should('contain.text', '2')
.and('contain.text', 'consensus nodes');
});
it('should contain link to specific validators', function () {
cy.getByTestId('validators')
.should('have.length', '2')
.each(($validator) => {
cy.wrap($validator).find('a').should('have.attr', 'href');
it('should have external link for governance', function () {
cy.getByTestId('home-proposals').within(() => {
cy.getByTestId('external-link')
.should('have.attr', 'href')
.and('contain', 'https://vega.xyz/governance');
});
});
it('should have link for rewards page', function () {
cy.getByTestId('home-rewards').within(() => {
cy.get('[href="/rewards"]')
.first()
.should('exist')
.and('have.text', 'See rewards');
});
});
it('should have link for withdrawal page', function () {
cy.getByTestId('home-vega-token').within(() => {
cy.get('[href="/token/withdraw"]')
.first()
.should('exist')
.and('have.text', 'Manage tokens');
});
});
it('should display network data', function () {
cy.getByTestId('git-network-data')
.should('contain.text', 'Reading network data from')
.within(() => {
cy.get('span')
it('should have link for validator page', function () {
cy.getByTestId('home-validators').within(() => {
cy.get('[href="/validators"]')
.first()
.should('have.text', 'http://localhost:3028/query');
cy.getByTestId('link').should('exist');
.should('exist')
.and('have.text', 'Browse, and stake');
});
});
it('should display eth data', function () {
cy.getByTestId('git-eth-data')
.should('contain.text', 'Reading Ethereum data from')
.within(() => {
cy.get('span').should('have.text', 'http://localhost:8545');
});
it('should have external link for validators', function () {
cy.getByTestId('home-validators').within(() => {
cy.getByTestId('external-link')
.should('have.attr', 'href')
.and(
'contain',
'https://community.vega.xyz/c/mainnet-validator-candidates'
);
});
});
it('should contain link for known issues on Github', function () {
cy.getByTestId('git-info').within(() => {
cy.contains('Known issues and feedback on')
.find('[data-testid="link"]')
.should(
'have.attr',
'href',
'https://github.com/vegaprotocol/feedback/discussions'
);
});
});
});
describe('Mobile view - navigation bar', function () {
before('Change to mobile resolution', function () {
cy.viewport('iphone-xr');
});
it('should have burger button', () => {
cy.getByTestId('button-menu-drawer').should('be.visible').click();
cy.getByTestId('menu-drawer').should('be.visible');
});
it('should have link for proposal page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/proposals"]')
.should('exist')
.and('have.text', 'Proposals');
});
});
it('should have link for validator page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/validators"]')
it('should have information on active nodes', function () {
cy.getByTestId('node-information')
.first()
.should('exist')
.and('have.text', 'Validators');
.should('contain.text', '2')
.and('contain.text', 'active nodes');
});
it('should have information on consensus nodes', function () {
cy.getByTestId('node-information')
.last()
.should('contain.text', '2')
.and('contain.text', 'consensus nodes');
});
it('should contain link to specific validators', function () {
cy.getByTestId('validators')
.should('have.length', '2')
.each(($validator) => {
cy.wrap($validator).find('a').should('have.attr', 'href');
});
});
it('should have link for rewards page', function () {
cy.getByTestId('home-rewards').within(() => {
cy.get('[href="/rewards"]')
.first()
.should('exist')
.and('have.text', 'See rewards');
});
});
it('should have link for withdrawal page', function () {
cy.getByTestId('home-vega-token').within(() => {
cy.get('[href="/token/withdraw"]')
.first()
.should('exist')
.and('have.text', 'Manage tokens');
});
});
});
it('should have link for rewards page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/rewards"]')
.first()
.should('exist')
.and('have.text', 'Rewards');
describe('Mobile view - navigation bar', function () {
before('Change to mobile resolution', function () {
cy.viewport('iphone-xr');
});
});
it('should have link for withdrawal page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/token/withdraw"]')
.first()
.should('exist')
.and('have.text', 'Withdraw');
});
});
after(function () {
cy.viewport(
Cypress.config('viewportWidth'),
Cypress.config('viewportHeight')
);
it('should have burger button', () => {
cy.getByTestId('button-menu-drawer').should('be.visible').click();
cy.getByTestId('menu-drawer').should('be.visible');
});
it('should have link for proposal page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/proposals"]')
.should('exist')
.and('have.text', 'Proposals');
});
});
it('should have link for validator page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/validators"]')
.first()
.should('exist')
.and('have.text', 'Validators');
});
});
it('should have link for rewards page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/rewards"]')
.first()
.should('exist')
.and('have.text', 'Rewards');
});
});
it('should have link for withdrawal page', function () {
cy.getByTestId('menu-drawer').within(() => {
cy.get('[href="/token/withdraw"]')
.first()
.should('exist')
.and('have.text', 'Withdraw');
});
});
after(function () {
cy.viewport(
Cypress.config('viewportWidth'),
Cypress.config('viewportHeight')
);
});
});
});
});
@@ -26,19 +26,19 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
cy.connectPublicKey(vegaWalletPubKey);
});
it('Able to connect public key using url', function () {
cy.getByTestId('exit-view').click();
cy.visit(`/?address=${vegaWalletPubKey}`);
verifyConnectedToPubKey();
});
it('Able to connect public key via wallet and view assets in wallet', function () {
it('Able to connect public key via wallet', function () {
verifyConnectedToPubKey();
cy.getByTestId('currency-title', { timeout: 10000 })
.should('have.length.at.least', 4)
.and('contain.text', 'USDC (fake)');
});
it('Able to connect public key using url', function () {
cy.getByTestId('exit-view').click();
cy.visit(`/?address=${vegaWalletPubKey}`);
verifyConnectedToPubKey();
});
it('Unable to submit proposal with public key', function () {
const expectedErrorTxt = `You are connected in a view only state for public key: ${vegaWalletPubKey}. In order to send transactions you must connect to a real wallet.`;
@@ -74,7 +74,7 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator stake', function () {
cy.getByTestId('total-stake')
cy.get('[col-id="stake"] > div > span > span')
.should('have.length.at.least', 1)
.each(($stake) => {
cy.wrap($stake).should('not.be.empty');
@@ -82,7 +82,7 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator stake tooltip', function () {
cy.getByTestId('total-stake').first().realHover();
cy.get('[col-id="stake"] > div > span > span').first().realHover();
cy.get(stakedByOperatorToolTip)
.invoke('text')
@@ -96,15 +96,17 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator normalised voting power', function () {
cy.getByTestId('normalised-voting-power')
cy.get('[col-id="normalisedVotingPower"] > div > span > span')
.should('have.length.at.least', 1)
.each(($vPower) => {
cy.wrap($vPower).should('not.be.empty');
});
});
it('Should be able to see validator normalised voting power tooltip', function () {
cy.getByTestId('normalised-voting-power').first().realHover();
it('Should be able to see validator voting power tooltip', function () {
cy.get('[col-id="normalisedVotingPower"] > div > span > span')
.first()
.realHover();
cy.get(unnormalisedVotingPowerToolTip)
.invoke('text')
@@ -116,7 +118,7 @@ context('Validators Page - verify elements on page', function () {
// 2002-SINC-018
it('Should be able to see validator total penalties', function () {
cy.getByTestId('total-penalty')
cy.get('[col-id="totalPenalties"] > div > span > span')
.should('have.length.at.least', 1)
.each(($penalties) => {
cy.wrap($penalties).should('contain.text', '0%');
@@ -124,7 +126,7 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator penalties tooltip', function () {
cy.getByTestId('total-penalty').realHover();
cy.get('[col-id="totalPenalties"] > div > span > span').realHover();
cy.get(performancePenaltyToolTip)
.invoke('text')
@@ -138,7 +140,7 @@ context('Validators Page - verify elements on page', function () {
});
it('Should be able to see validator pending stake', function () {
cy.getByTestId('total-pending-stake')
cy.get('[col-id="pendingStake"] > div > span')
.should('have.length.at.least', 1)
.each(($pendingStake) => {
cy.wrap($pendingStake).should('contain.text', '0.00');
@@ -84,7 +84,3 @@ export function verifyEthWalletAssociatedBalance(amount: string) {
.parent(txTimeout)
.should('contain', amount, txTimeout);
}
export function closeDialog() {
cy.getByTestId('dialog-close').click();
}
@@ -1,4 +1,4 @@
import { closeDialog, navigateTo, navigation } from './common.functions';
import { navigateTo, navigation } from './common.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from './staking.functions';
const newProposalButton = '[data-testid="new-proposal-link"]';
@@ -12,6 +12,7 @@ const voteButtons = '[data-testid="vote-buttons"]';
const dialogTitle = '[data-testid="dialog-title"]';
const proposalVoteDeadline = '[data-testid="proposal-vote-deadline"]';
const newProposalSubmitButton = '[data-testid="proposal-submit"]';
const dialogCloseButton = '[data-testid="dialog-close"]';
const epochTimeout = Cypress.env('epochTimeout');
const proposalTimeout = { timeout: 14000 };
@@ -124,7 +125,7 @@ export function voteForProposal(vote: string) {
'have.text',
'Transaction complete'
);
closeDialog();
cy.get(dialogCloseButton).click();
}
export function waitForProposalSync() {
@@ -175,7 +176,7 @@ export function waitForProposalSubmitted() {
'be.visible'
);
cy.contains('Proposal submitted', proposalTimeout).should('be.visible');
closeDialog();
cy.get(dialogCloseButton).click();
}
export function createRawProposal(proposerBalance?: string) {
@@ -1,4 +1,3 @@
import { closeDialog } from './common.functions';
import { vegaWalletTeardown } from './wallet-teardown.functions';
const tokenAmountInputBox = '[data-testid="token-amount-input"]';
@@ -15,10 +14,11 @@ const associateWalletRadioButton = '[data-testid="associate-radio-wallet"]';
const associateContractRadioButton = '[data-testid="associate-radio-contract"]';
const stakeMaximumTokens = '[data-testid="token-amount-use-maximum"]';
const stakeValidatorListPendingStake = '[col-id="pendingStake"]';
const stakeValidatorListTotalStake = 'total-stake';
const stakeValidatorListTotalShare = 'total-stake-share';
const stakeValidatorListTotalStake = '[col-id="stake"] > div > span';
const stakeValidatorListTotalShare = '[col-id="stakeShare"] > div > span';
const stakeValidatorListName = '[col-id="validator"]';
const vegaKeySelector = '#vega-key-selector';
const dialogCloseButton = '[data-testid="dialog-close"]';
const txTimeout = Cypress.env('txTimeout');
const epochTimeout = Cypress.env('epochTimeout');
@@ -54,7 +54,7 @@ export function stakingValidatorPageRemoveStake(stake: string) {
.and('contain', `Remove ${stake} $VEGA tokens at the end of epoch`)
.and('be.visible')
.click();
closeDialog();
cy.get(dialogCloseButton).click();
}
export function stakingPageAssociateTokens(
@@ -185,11 +185,11 @@ export function validateValidatorListTotalStakeAndShare(
cy.contains('Loading...', epochTimeout).should('not.exist');
waitForBeginningOfEpoch();
cy.get(`[row-id="${positionOnList}"]`).within(() => {
cy.getByTestId(stakeValidatorListTotalStake, epochTimeout).should(
cy.get(stakeValidatorListTotalStake, epochTimeout).should(
'have.text',
expectedTotalStake
);
cy.getByTestId(stakeValidatorListTotalShare, epochTimeout).should(
cy.get(stakeValidatorListTotalShare, epochTimeout).should(
'have.text',
expectedTotalShare
);
@@ -73,6 +73,9 @@ export async function vegaWalletTeardown() {
}
});
cy.get(vegaWalletContainer).within(() => {
cy.getByTestId('vega-wallet-balance-staked-validators', {
timeout: transactionTimeout,
}).should('not.exist');
cy.getByTestId('associated-amount', {
timeout: transactionTimeout,
}).contains('0.00', {
@@ -122,9 +125,6 @@ async function vegaWalletTeardownVesting(vestingContract: TokenVesting) {
log: false,
}).then((vestingAmount) => {
if (Number(vestingAmount) != 0) {
// Wait needed to allow time for ganache to process tx for stakingBridgeContract.remove_stake
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
cy.wrap(
vestingContract.remove_stake(String(vestingAmount), vegaWalletPubKey),
{ timeout: transactionTimeout, log: false }
-1
View File
@@ -13,7 +13,6 @@ NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
#Test configuration variables
CYPRESS_FAIRGROUND=false
-1
View File
@@ -5,7 +5,6 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_FAIRGROUND=false
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_CONFIG_URL=''
NX_VEGA_URL=http://localhost:3028/query
-1
View File
@@ -10,4 +10,3 @@ NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
-1
View File
@@ -11,4 +11,3 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40:87edc2605e544f888305d7fc4a9141bd@o286262.ingest.sentry.io/5882996
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
+11
View File
@@ -0,0 +1,11 @@
# App configuration variables
NX_VEGA_ENV=MIRROR
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
NX_VEGA_URL=https://api.n00.mainnet-mirror.vega.xyz/graphql
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_EXPLORER_URL=https://mirror.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_DELEGATIONS_PAGINATION=50
+8
View File
@@ -0,0 +1,8 @@
# App configuration variables
NX_VEGA_URL=https://api.sandbox.vega.xyz/graphql
NX_VEGA_ENV=SANDBOX
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
-1
View File
@@ -7,4 +7,3 @@ NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
-1
View File
@@ -8,4 +8,3 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
-1
View File
@@ -11,4 +11,3 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-testnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
-1
View File
@@ -8,4 +8,3 @@ NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
@@ -1,6 +1,9 @@
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
import { AnnouncementBanner } from '@vegaprotocol/announcements';
import {
ViewingAsBanner,
AnnouncementBanner,
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import React from 'react';
@@ -12,16 +15,18 @@ export interface TemplateSidebarProps {
}
export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
const { VEGA_ENV, ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
const { VEGA_ENV } = useEnvironment();
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
return (
<>
{ANNOUNCEMENTS_CONFIG_URL && (
<AnnouncementBanner
app="governance"
configUrl={ANNOUNCEMENTS_CONFIG_URL}
/>
)}
<AnnouncementBanner>
<div className="font-alpha calt uppercase text-center text-lg text-white">
<span className="pr-4">Wait no longer, SIM3 is here!</span>
<ExternalLink href="https://fairground.wtf/sim3">
Learn more
</ExternalLink>
</div>
</AnnouncementBanner>
<Nav theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
{isReadOnly ? (
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
+8 -4
View File
@@ -12,7 +12,6 @@ const TRUTHY = ['1', 'true'];
interface VegaContracts {
claimAddress: string;
lockedAddress: string;
tokenVestingAddress?: string;
}
const customClaimAddress = process.env['NX_CUSTOM_CLAIM_ADDRESS'] as string;
@@ -37,16 +36,21 @@ export const ContractAddresses: {
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
lockedAddress: '0x0', // TODO not deployed to this env
},
SANDBOX: {
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
lockedAddress: '0x0', // TODO not deployed to this env
},
TESTNET: {
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
lockedAddress: '0x0', // TODO not deployed to this env
},
MIRROR: {
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
lockedAddress: '0x0', // TODO not deployed to this env
},
VALIDATOR_TESTNET: {
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
lockedAddress: '0x0', // TODO not deployed to this env
// This is a fallback contract address for the validator testnet network which does not
// have a vesting contract address set and is therefore not in the ethereum config
tokenVestingAddress: '0xadFcb7f93a24F8743a8e548d74d2ecB373c92866',
},
MAINNET: {
claimAddress: '0x0ee1fb382caf98e86e97e51f9f42f8b4654020f3',
@@ -49,13 +49,6 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
signer = provider.getSigner();
}
const tokenVestingAddress =
config.token_vesting_contract?.address ||
ENV.addresses.tokenVestingAddress;
if (!tokenVestingAddress) {
throw new Error('No token vesting address found');
}
if (provider && config) {
const staking = new StakingBridge(
config.staking_bridge_contract.address,
@@ -70,7 +63,7 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
signer || provider
),
vesting: new TokenVesting(
tokenVestingAddress,
config.token_vesting_contract.address,
signer || provider
),
claim: new Claim(ENV.addresses.claimAddress, signer || provider),
@@ -221,9 +221,8 @@
"votePending": "Casting vote",
"voteError": "Something went wrong, and your vote was not seen by the network",
"back": "back",
"byTokenVote": "by token vote",
"byLiquidityVote": "by liquidity vote",
"byLPVote": "by LP vote",
"byTokenVote": "by Token vote",
"byLiquidityVote": "by Liquidity vote",
"youDidNotVote": "Voting has ended. You did not vote",
"voteState_Yes": "For",
"voteState_No": "Against",
@@ -602,14 +601,11 @@
"noValidators": "No validators",
"validator": "Validator",
"stake": "Stake",
"myStake": "My stake",
"stakeShare": "Stake share",
"stakedByOperator": "Staked by operator",
"stakedByDelegates": "Staked by delegates",
"stakedByMe": "Staked by me",
"totalStake": "Total stake",
"pendingStake": "Pending stake",
"myPendingStake": "My pending stake",
"totalPenalties": "Total penalties",
"noPenaltyDataFromLastEpoch": "No penalty data from last epoch",
"stakeNeededForPromotion": "Stake needed for promotion",
+2 -2
View File
@@ -12,14 +12,14 @@ import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
import Routes from '../routes';
import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useNodesQuery } from '../staking/home/__generated___/Nodes';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import { getNotRejectedProposals } from '../proposals/proposals/proposals-container';
import { Heading } from '../../components/heading';
import * as Schema from '@vegaprotocol/types';
import type { RouteChildProps } from '..';
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
import type { NodesFragmentFragment } from '../staking/home/__generated__/Nodes';
import type { NodesFragmentFragment } from '../staking/home/__generated___/Nodes';
const nodesToShow = 6;
@@ -86,7 +86,7 @@ describe('Proposal Votes Table', () => {
it('displays if an update market proposal will pass by token vote', () => {
renderComponent(updateMarketProposal, updateMarketProposalType);
expect(screen.getByText('👍 by token vote')).toBeInTheDocument();
expect(screen.getByText('👍 by Token vote')).toBeInTheDocument();
});
it('displays if an update market proposal will pass by LP vote', () => {
@@ -110,6 +110,6 @@ describe('Proposal Votes Table', () => {
}),
updateMarketProposalType
);
expect(screen.getByText('👍 by liquidity vote')).toBeInTheDocument();
expect(screen.getByText('👍 by Liquidity vote')).toBeInTheDocument();
});
});
@@ -136,82 +136,6 @@ describe('Proposals list item details', () => {
);
});
it('Renders proposal state: Update market proposal - set to pass by LP vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: '1000',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Set to pass by LP vote'
);
});
it('Renders proposal state: Update market proposal - set to pass by token vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(1000, 1000),
totalEquityLikeShareWeight: '0',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Set to pass by token vote'
);
});
it('Renders proposal state: Update market proposal - set to fail', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: '0',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to fail');
});
it('Renders proposal state: Open - 5 minutes left to vote', () => {
renderComponent(
generateProposal({
@@ -41,22 +41,12 @@ export const ProposalsListItemDetails = ({
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const state = proposal?.state;
const {
willPassByTokenVote,
willPassByLPVote,
majorityMet,
participationMet,
} = useVoteInformation({
proposal,
});
const { willPassByTokenVote, majorityMet, participationMet } =
useVoteInformation({
proposal,
});
const { t } = useTranslation();
const { voteState } = useUserVote(proposal?.id);
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
const updateMarketWillPass = willPassByTokenVote || willPassByLPVote;
const updateMarketVotePassMethod = willPassByTokenVote
? t('byTokenVote')
: t('byLPVote');
let proposalStatus: ReactNode;
let voteDetails: ReactNode;
let voteStatus: ReactNode;
@@ -138,19 +128,6 @@ export const ProposalsListItemDetails = ({
</>
);
voteStatus =
(isUpdateMarket &&
(updateMarketWillPass ? (
<>
{t('Set to')}{' '}
<StatusPass>
{t('pass')} {updateMarketVotePassMethod}
</StatusPass>
</>
) : (
<>
{t('Set to')} <StatusFail>{t('fail')}</StatusFail>
</>
))) ||
(!participationMet && <ParticipationNotReached />) ||
(!majorityMet && <MajorityNotReached />) ||
(willPassByTokenVote ? (
@@ -32,7 +32,6 @@ export const VoteDetails = ({
totalTokensPercentage,
participationMet,
totalTokensVoted,
totalLPTokensPercentage,
noPercentage,
noLPPercentage,
yesPercentage,
@@ -42,8 +41,6 @@ export const VoteDetails = ({
requiredMajorityPercentage,
requiredMajorityLPPercentage,
requiredParticipation,
requiredParticipationLP,
participationLPMet,
} = useVoteInformation({ proposal });
const { t } = useTranslation();
@@ -104,25 +101,6 @@ export const VoteDetails = ({
</tr>
</tbody>
</table>
<p className="mb-6">
{t('participation')}
{': '}
{participationLPMet ? (
<span className="text-vega-green mx-4">{t('met')}</span>
) : (
<span className="text-danger mx-4">{t('notMet')}</span>
)}{' '}
{formatNumber(totalLPTokensPercentage, defaultDecimals)}%
<span className="ml-4">
{requiredParticipationLP && (
<>
({formatNumber(requiredParticipationLP, defaultDecimals)}%{' '}
{t('governanceRequired')})
</>
)}
</span>
</p>
</section>
)}
<section data-testid="votes-table">
@@ -21,12 +21,6 @@ export const EpochIndividualRewards = () => {
first: Number(delegationsPagination),
}
: undefined,
// we can use the same value for rewardsPagination as delegationsPagination
rewardsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
},
skip: !pubKey,
});
@@ -21,14 +21,10 @@ fragment DelegationFields on Delegation {
epoch
}
query Rewards(
$partyId: ID!
$delegationsPagination: Pagination
$rewardsPagination: Pagination
) {
query Rewards($partyId: ID!, $delegationsPagination: Pagination) {
party(id: $partyId) {
id
rewardsConnection(pagination: $rewardsPagination) {
rewardsConnection {
edges {
node {
...RewardFields
@@ -10,7 +10,6 @@ export type DelegationFieldsFragment = { __typename?: 'Delegation', amount: stri
export type RewardsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
delegationsPagination?: Types.InputMaybe<Types.Pagination>;
rewardsPagination?: Types.InputMaybe<Types.Pagination>;
}>;
@@ -76,10 +75,10 @@ export const EpochFieldsFragmentDoc = gql`
}
`;
export const RewardsDocument = gql`
query Rewards($partyId: ID!, $delegationsPagination: Pagination, $rewardsPagination: Pagination) {
query Rewards($partyId: ID!, $delegationsPagination: Pagination) {
party(id: $partyId) {
id
rewardsConnection(pagination: $rewardsPagination) {
rewardsConnection {
edges {
node {
...RewardFields
@@ -120,7 +119,6 @@ ${DelegationFieldsFragmentDoc}`;
* variables: {
* partyId: // value for 'partyId'
* delegationsPagination: // value for 'delegationsPagination'
* rewardsPagination: // value for 'rewardsPagination'
* },
* });
*/
@@ -104,7 +104,7 @@ export const RewardsPage = () => {
</section>
)}
<section className="grid xl:grid-cols-[1fr_auto] gap-12 items-center mb-8">
<section className="grid xl:grid-cols-2 gap-12 items-center mb-8">
<div>
<SubHeading title={t('rewardsAndFeesReceived')} />
<p>
@@ -114,7 +114,7 @@ export const RewardsPage = () => {
</p>
</div>
<div className="w-[440px]">
<div className="max-w-[600px]">
<Toggle
name="epoch-reward-view-toggle"
toggles={[
@@ -7,10 +7,9 @@ query PreviousEpoch($epochId: ID) {
id
rewardScore {
rawValidatorScore
performanceScore
}
rankingScore {
stakeScore
performanceScore
}
}
}
@@ -8,7 +8,7 @@ export type PreviousEpochQueryVariables = Types.Exact<{
}>;
export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string, performanceScore: string } | null, rankingScore: { __typename?: 'RankingScore', stakeScore: string } } } | null> | null } | null } };
export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string } | null, rankingScore: { __typename?: 'RankingScore', performanceScore: string } } } | null> | null } | null } };
export const PreviousEpochDocument = gql`
@@ -21,10 +21,9 @@ export const PreviousEpochDocument = gql`
id
rewardScore {
rawValidatorScore
performanceScore
}
rankingScore {
stakeScore
performanceScore
}
}
}
@@ -0,0 +1,106 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type PreviousEpochQueryVariables = Types.Exact<{
epochId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
export type PreviousEpochQuery = {
__typename?: 'Query';
epoch: {
__typename?: 'Epoch';
id: string;
validatorsConnection?: {
__typename?: 'NodesConnection';
edges?: Array<{
__typename?: 'NodeEdge';
node: {
__typename?: 'Node';
id: string;
rewardScore?: {
__typename?: 'RewardScore';
rawValidatorScore: string;
} | null;
rankingScore: {
__typename?: 'RankingScore';
performanceScore: string;
};
};
} | null> | null;
} | null;
};
};
export const PreviousEpochDocument = gql`
query PreviousEpoch($epochId: ID) {
epoch(id: $epochId) {
id
validatorsConnection {
edges {
node {
id
rewardScore {
rawValidatorScore
}
rankingScore {
performanceScore
}
}
}
}
}
}
`;
/**
* __usePreviousEpochQuery__
*
* To run a query within a React component, call `usePreviousEpochQuery` and pass it any options that fit your needs.
* When your component renders, `usePreviousEpochQuery` 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 } = usePreviousEpochQuery({
* variables: {
* epochId: // value for 'epochId'
* },
* });
*/
export function usePreviousEpochQuery(
baseOptions?: Apollo.QueryHookOptions<
PreviousEpochQuery,
PreviousEpochQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<PreviousEpochQuery, PreviousEpochQueryVariables>(
PreviousEpochDocument,
options
);
}
export function usePreviousEpochLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
PreviousEpochQuery,
PreviousEpochQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<PreviousEpochQuery, PreviousEpochQueryVariables>(
PreviousEpochDocument,
options
);
}
export type PreviousEpochQueryHookResult = ReturnType<
typeof usePreviousEpochQuery
>;
export type PreviousEpochLazyQueryHookResult = ReturnType<
typeof usePreviousEpochLazyQuery
>;
export type PreviousEpochQueryResult = Apollo.QueryResult<
PreviousEpochQuery,
PreviousEpochQueryVariables
>;
@@ -0,0 +1,114 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type LinkingsFieldsFragment = {
__typename?: 'StakeLinking';
id: string;
txHash: string;
status: Types.StakeLinkingStatus;
};
export type PartyStakeLinkingsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type PartyStakeLinkingsQuery = {
__typename?: 'Query';
party?: {
__typename?: 'Party';
id: string;
stakingSummary: {
__typename?: 'StakingSummary';
linkings: {
__typename?: 'StakesConnection';
edges?: Array<{
__typename?: 'StakeLinkingEdge';
node: {
__typename?: 'StakeLinking';
id: string;
txHash: string;
status: Types.StakeLinkingStatus;
};
} | null> | null;
};
};
} | null;
};
export const LinkingsFieldsFragmentDoc = gql`
fragment LinkingsFields on StakeLinking {
id
txHash
status
}
`;
export const PartyStakeLinkingsDocument = gql`
query PartyStakeLinkings($partyId: ID!) {
party(id: $partyId) {
id
stakingSummary {
linkings {
edges {
node {
...LinkingsFields
}
}
}
}
}
}
${LinkingsFieldsFragmentDoc}
`;
/**
* __usePartyStakeLinkingsQuery__
*
* To run a query within a React component, call `usePartyStakeLinkingsQuery` and pass it any options that fit your needs.
* When your component renders, `usePartyStakeLinkingsQuery` 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 } = usePartyStakeLinkingsQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function usePartyStakeLinkingsQuery(
baseOptions: Apollo.QueryHookOptions<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>(PartyStakeLinkingsDocument, options);
}
export function usePartyStakeLinkingsLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>(PartyStakeLinkingsDocument, options);
}
export type PartyStakeLinkingsQueryHookResult = ReturnType<
typeof usePartyStakeLinkingsQuery
>;
export type PartyStakeLinkingsLazyQueryHookResult = ReturnType<
typeof usePartyStakeLinkingsLazyQuery
>;
export type PartyStakeLinkingsQueryResult = Apollo.QueryResult<
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables
>;
@@ -15,7 +15,7 @@ import {
} from '../../../hooks/transaction-reducer';
import Routes from '../../routes';
import { truncateMiddle } from '../../../lib/truncate-middle';
import type { LinkingsFieldsFragment } from './__generated__/PartyStakeLinkings';
import type { LinkingsFieldsFragment } from './__generated___/PartyStakeLinkings';
export const AssociateTransaction = ({
amount,
@@ -15,8 +15,8 @@ import type {
LinkingsFieldsFragment,
PartyStakeLinkingsQuery,
PartyStakeLinkingsQueryVariables,
} from './__generated__/PartyStakeLinkings';
import { PartyStakeLinkingsDocument } from './__generated__/PartyStakeLinkings';
} from './__generated___/PartyStakeLinkings';
import { PartyStakeLinkingsDocument } from './__generated___/PartyStakeLinkings';
export const useAddStake = (
address: string,
@@ -0,0 +1,149 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type NodesFragmentFragment = {
__typename?: 'Node';
avatarUrl?: string | null;
id: string;
name: string;
pubkey: string;
stakedByOperator: string;
stakedByDelegates: string;
stakedTotal: string;
pendingStake: string;
rankingScore: {
__typename?: 'RankingScore';
rankingScore: string;
stakeScore: string;
performanceScore: string;
votingPower: string;
status: Types.ValidatorStatus;
};
};
export type NodesQueryVariables = Types.Exact<{ [key: string]: never }>;
export type NodesQuery = {
__typename?: 'Query';
epoch: {
__typename?: 'Epoch';
id: string;
timestamps: {
__typename?: 'EpochTimestamps';
start?: any | null;
end?: any | null;
expiry?: any | null;
};
};
nodesConnection: {
__typename?: 'NodesConnection';
edges?: Array<{
__typename?: 'NodeEdge';
node: {
__typename?: 'Node';
avatarUrl?: string | null;
id: string;
name: string;
pubkey: string;
stakedByOperator: string;
stakedByDelegates: string;
stakedTotal: string;
pendingStake: string;
rankingScore: {
__typename?: 'RankingScore';
rankingScore: string;
stakeScore: string;
performanceScore: string;
votingPower: string;
status: Types.ValidatorStatus;
};
};
} | null> | null;
};
nodeData?: { __typename?: 'NodeData'; stakedTotal: string } | null;
};
export const NodesFragmentFragmentDoc = gql`
fragment NodesFragment on Node {
avatarUrl
id
name
pubkey
stakedByOperator
stakedByDelegates
stakedTotal
pendingStake
rankingScore {
rankingScore
stakeScore
performanceScore
votingPower
status
}
}
`;
export const NodesDocument = gql`
query Nodes {
epoch {
id
timestamps {
start
end
expiry
}
}
nodesConnection {
edges {
node {
...NodesFragment
}
}
}
nodeData {
stakedTotal
}
}
${NodesFragmentFragmentDoc}
`;
/**
* __useNodesQuery__
*
* To run a query within a React component, call `useNodesQuery` and pass it any options that fit your needs.
* When your component renders, `useNodesQuery` 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 } = useNodesQuery({
* variables: {
* },
* });
*/
export function useNodesQuery(
baseOptions?: Apollo.QueryHookOptions<NodesQuery, NodesQueryVariables>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<NodesQuery, NodesQueryVariables>(
NodesDocument,
options
);
}
export function useNodesLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<NodesQuery, NodesQueryVariables>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<NodesQuery, NodesQueryVariables>(
NodesDocument,
options
);
}
export type NodesQueryHookResult = ReturnType<typeof useNodesQuery>;
export type NodesLazyQueryHookResult = ReturnType<typeof useNodesLazyQuery>;
export type NodesQueryResult = Apollo.QueryResult<
NodesQuery,
NodesQueryVariables
>;
@@ -1,53 +1,36 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { EpochCountdown } from '../../../components/epoch-countdown';
import { useNodesQuery } from './__generated__/Nodes';
import { useStakingQuery } from '../__generated__/Staking';
import { usePreviousEpochQuery } from '../__generated__/PreviousEpoch';
import { useNodesQuery } from './__generated___/Nodes';
import { usePreviousEpochQuery } from '../__generated___/PreviousEpoch';
import { ValidatorTables } from './validator-tables';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { useVegaWallet } from '@vegaprotocol/wallet';
export const EpochData = () => {
// errorPolicy due to vegaprotocol/vega issue 5898
const { pubKey } = useVegaWallet();
const {
data: nodesData,
error: nodesError,
loading: nodesLoading,
refetch,
} = useNodesQuery();
const { data: userStakingData } = useStakingQuery({
variables: {
partyId: pubKey || '',
},
});
const { data, error, loading, refetch } = useNodesQuery();
const { data: previousEpochData } = usePreviousEpochQuery({
variables: {
epochId: (Number(nodesData?.epoch.id) - 1).toString(),
epochId: (Number(data?.epoch.id) - 1).toString(),
},
skip: !nodesData?.epoch.id,
skip: !data?.epoch.id,
});
useRefreshAfterEpoch(nodesData?.epoch.timestamps.expiry, refetch);
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
return (
<AsyncRenderer loading={nodesLoading} error={nodesError} data={nodesData}>
{nodesData?.epoch &&
nodesData.epoch.timestamps.start &&
nodesData?.epoch.timestamps.expiry && (
<AsyncRenderer loading={loading} error={error} data={data}>
{data?.epoch &&
data.epoch.timestamps.start &&
data?.epoch.timestamps.expiry && (
<div className="mb-10">
<EpochCountdown
id={nodesData.epoch.id}
startDate={new Date(nodesData.epoch.timestamps.start)}
endDate={new Date(nodesData.epoch.timestamps.expiry)}
id={data.epoch.id}
startDate={new Date(data.epoch.timestamps.start)}
endDate={new Date(data.epoch.timestamps.expiry)}
/>
</div>
)}
<ValidatorTables
nodesData={nodesData}
userStakingData={userStakingData}
previousEpochData={previousEpochData}
/>
<ValidatorTables data={data} previousEpochData={previousEpochData} />
</AsyncRenderer>
);
};
@@ -3,15 +3,14 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
import { ConsensusValidatorsTable } from './consensus-validators-table';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import { NodesDocument } from '../__generated__/Nodes';
import { PreviousEpochDocument } from '../../__generated__/PreviousEpoch';
import { NodesDocument } from '../__generated___/Nodes';
import { PreviousEpochDocument } from '../../__generated___/PreviousEpoch';
import * as Schema from '@vegaprotocol/types';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import type { MockedResponse } from '@apollo/client/testing';
import type { PartialDeep } from 'type-fest';
import type { NodesFragmentFragment } from '../__generated__/Nodes';
import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
import type { ValidatorsView } from './validator-tables';
import type { NodesFragmentFragment } from '../__generated___/Nodes';
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
const nodeFactory = (
overrides?: PartialDeep<NodesFragmentFragment>
@@ -81,10 +80,9 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
rewardScore: {
rawValidatorScore: '0.25',
performanceScore: '0.9998677767864936',
},
rankingScore: {
stakeScore: '0.2499583402766206',
performanceScore: '0.9998677767864936',
},
},
},
@@ -93,10 +91,9 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99',
rewardScore: {
rawValidatorScore: '0.3',
performanceScore: '1',
},
rankingScore: {
stakeScore: '0.25',
performanceScore: '1',
},
},
},
@@ -105,10 +102,9 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
id: '12c81b738e8051152e1afe44376ec37bca9216466e6d44cdd772194bad0ada81',
rewardScore: {
rawValidatorScore: '0.35',
performanceScore: '0.999629748500531',
},
rankingScore: {
stakeScore: '0.2312',
performanceScore: '0.999629748500531',
},
},
},
@@ -146,8 +142,7 @@ const MOCK_TOTAL_STAKE = '28832590188747439203824';
const renderValidatorsTable = (
data = MOCK_NODES,
previousEpochData = MOCK_PREVIOUS_EPOCH,
validatorsView: ValidatorsView = 'all'
previousEpochData = MOCK_PREVIOUS_EPOCH
) => {
return render(
<AppStateProvider initialState={{ decimals: 18 }}>
@@ -157,7 +152,6 @@ const renderValidatorsTable = (
data={data}
previousEpochData={previousEpochData}
totalStake={MOCK_TOTAL_STAKE}
validatorsView={validatorsView}
/>
</MockedProvider>
</MemoryRouter>
@@ -10,6 +10,7 @@ import {
getFormattedPerformanceScore,
getLastEpochScoreAndPerformance,
getNormalisedVotingPower,
getOverstakedAmount,
getOverstakingPenalty,
getPerformancePenalty,
getTotalPenalties,
@@ -18,9 +19,7 @@ import {
import {
defaultColDef,
NODE_LIST_GRID_STYLES,
PendingStakeRenderer,
stakedTotalPercentage,
StakeShareRenderer,
TotalPenaltiesRenderer,
TotalStakeRenderer,
ValidatorFields,
@@ -51,12 +50,10 @@ interface CanonisedConsensusNodeProps {
[ValidatorFields.STAKED_BY_OPERATOR]: string;
[ValidatorFields.PERFORMANCE_SCORE]: string;
[ValidatorFields.PERFORMANCE_PENALTY]: string;
[ValidatorFields.OVERSTAKED_AMOUNT]: string;
[ValidatorFields.OVERSTAKING_PENALTY]: string;
[ValidatorFields.TOTAL_PENALTIES]: string;
[ValidatorFields.PENDING_STAKE]: string;
[ValidatorFields.STAKED_BY_USER]: string | undefined;
[ValidatorFields.PENDING_USER_STAKE]: string | undefined;
[ValidatorFields.USER_STAKE_SHARE]: string | undefined;
}
const getRowHeight = (params: RowHeightParams) => {
@@ -64,7 +61,7 @@ const getRowHeight = (params: RowHeightParams) => {
// Note: this value will change if the height of the top third cell renderer changes
return 138;
}
return 68;
return 52;
};
const TopThirdCellRenderer = (
@@ -120,7 +117,6 @@ export const ConsensusValidatorsTable = ({
data,
previousEpochData,
totalStake,
validatorsView,
}: ValidatorsTableProps) => {
const { t } = useTranslation();
const {
@@ -133,7 +129,7 @@ export const ConsensusValidatorsTable = ({
const nodes = useMemo(() => {
if (!data || !previousEpochData) return [];
let canonisedNodes = data
const canonisedNodes = data
.sort((a, b) => {
const aVotingPower = new BigNumber(a.rankingScore.votingPower);
const bVotingPower = new BigNumber(b.rankingScore.votingPower);
@@ -158,15 +154,15 @@ export const ConsensusValidatorsTable = ({
rankingScore: { stakeScore, votingPower },
pendingStake,
votingPowerRanking,
stakedByUser,
pendingUserStake,
userStakeShare,
}) => {
const {
rawValidatorScore: previousEpochValidatorScore,
performanceScore: previousEpochPerformanceScore,
stakeScore: previousEpochStakeScore,
} = getLastEpochScoreAndPerformance(previousEpochData, id);
const { rawValidatorScore, performanceScore } =
getLastEpochScoreAndPerformance(previousEpochData, id);
const overstakedAmount = getOverstakedAmount(
rawValidatorScore,
stakedTotal,
totalStake
);
return {
id,
@@ -179,7 +175,7 @@ export const ConsensusValidatorsTable = ({
[ValidatorFields.NORMALISED_VOTING_POWER]:
getNormalisedVotingPower(votingPower),
[ValidatorFields.UNNORMALISED_VOTING_POWER]:
getUnnormalisedVotingPower(previousEpochValidatorScore),
getUnnormalisedVotingPower(rawValidatorScore),
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
[ValidatorFields.STAKED_BY_DELEGATES]: formatNumber(
toBigNum(stakedByDelegates, decimals),
@@ -189,45 +185,28 @@ export const ConsensusValidatorsTable = ({
toBigNum(stakedByOperator, decimals),
2
),
[ValidatorFields.PERFORMANCE_SCORE]: getFormattedPerformanceScore(
previousEpochPerformanceScore
).toString(),
[ValidatorFields.PERFORMANCE_PENALTY]: getPerformancePenalty(
previousEpochPerformanceScore
),
[ValidatorFields.PERFORMANCE_SCORE]:
getFormattedPerformanceScore(performanceScore).toString(),
[ValidatorFields.PERFORMANCE_PENALTY]:
getPerformancePenalty(performanceScore),
[ValidatorFields.OVERSTAKED_AMOUNT]: overstakedAmount.toString(),
[ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty(
previousEpochValidatorScore,
previousEpochStakeScore
overstakedAmount,
totalStake
),
[ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties(
previousEpochValidatorScore,
previousEpochPerformanceScore,
rawValidatorScore,
performanceScore,
stakedTotal,
totalStake
),
[ValidatorFields.PENDING_STAKE]: pendingStake,
[ValidatorFields.STAKED_BY_USER]: stakedByUser
? formatNumber(toBigNum(stakedByUser, decimals), 2)
: undefined,
[ValidatorFields.PENDING_USER_STAKE]: pendingUserStake,
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
? stakedTotalPercentage(userStakeShare)
: undefined,
decimals,
};
}
);
if (validatorsView === 'myStake') {
canonisedNodes = canonisedNodes.filter(
(node) => node[ValidatorFields.STAKED_BY_USER] !== undefined
);
}
if (
canonisedNodes.length < 3 ||
!hideTopThird ||
validatorsView === 'myStake'
) {
if (canonisedNodes.length < 3 || !hideTopThird) {
return canonisedNodes;
}
@@ -312,14 +291,7 @@ export const ConsensusValidatorsTable = ({
},
...remaining,
];
}, [
data,
decimals,
hideTopThird,
previousEpochData,
totalStake,
validatorsView,
]);
}, [data, decimals, hideTopThird, previousEpochData, totalStake]);
const ConsensusTable = forwardRef<AgGridReact>((_, gridRef) => {
const colDefs = useMemo<ColDef[]>(
@@ -339,7 +311,7 @@ export const ConsensusValidatorsTable = ({
return a > b ? 1 : -1;
},
pinned: 'left',
width: 260,
width: 240,
},
{
field: ValidatorFields.STAKE,
@@ -348,20 +320,6 @@ export const ConsensusValidatorsTable = ({
cellRenderer: TotalStakeRenderer,
width: 120,
},
{
field: ValidatorFields.PENDING_STAKE,
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
headerTooltip: t('PendingStakeDescription').toString(),
cellRenderer: PendingStakeRenderer,
width: 120,
},
{
field: ValidatorFields.STAKE_SHARE,
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
cellRenderer: StakeShareRenderer,
width: 120,
},
{
field: ValidatorFields.NORMALISED_VOTING_POWER,
headerName: t(ValidatorFields.NORMALISED_VOTING_POWER).toString(),
@@ -370,6 +328,12 @@ export const ConsensusValidatorsTable = ({
width: 200,
sort: 'desc',
},
{
field: ValidatorFields.STAKE_SHARE,
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
width: 100,
},
{
field: ValidatorFields.TOTAL_PENALTIES,
headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(),
@@ -377,6 +341,14 @@ export const ConsensusValidatorsTable = ({
cellRenderer: TotalPenaltiesRenderer,
width: 120,
},
{
field: ValidatorFields.PENDING_STAKE,
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
headerTooltip: t('PendingStakeDescription').toString(),
valueFormatter: ({ value }) =>
formatNumber(toBigNum(value, decimals), 2),
width: 110,
},
],
[]
);
@@ -0,0 +1,70 @@
import {
getLastEpochScoreAndPerformance,
getTotalPenalties,
} from '../../shared';
import { stakedTotalPercentage } from './shared';
const MOCK_PREVIOUS_EPOCH = {
epoch: {
id: '1',
validatorsConnection: {
edges: [
{
node: {
id: '0x123',
rewardScore: {
rawValidatorScore: '0.25',
},
rankingScore: {
performanceScore: '0.75',
},
},
},
],
},
},
};
describe('stakedTotalPercentage', () => {
it('should return the correct percentage as a string, 2dp', () => {
expect(stakedTotalPercentage('1.2345')).toBe('123.45%');
});
});
describe('totalPenalties', () => {
it('should return the correct penalty based on arbitrary values, test 1', () => {
expect(
getTotalPenalties(
getLastEpochScoreAndPerformance(MOCK_PREVIOUS_EPOCH, '0x123')
.rawValidatorScore,
'0.1',
'5000',
'100000'
)
).toBe('50.00%');
});
it('should return the correct penalty based on lower performance score than first test', () => {
expect(
getTotalPenalties(
getLastEpochScoreAndPerformance(MOCK_PREVIOUS_EPOCH, '0x123')
.rawValidatorScore,
'0.05',
'5000',
'100000'
)
).toBe('75.00%');
});
it('should return the correct penalty based on higher amount of stake than other tests (great penalty due to anti-whaling)', () => {
expect(
getTotalPenalties(
getLastEpochScoreAndPerformance(MOCK_PREVIOUS_EPOCH, '0x123')
.rawValidatorScore,
'0.1',
'5000',
'5500'
)
).toBe('97.25%');
});
});
@@ -10,12 +10,9 @@ import {
Tooltip,
TooltipCellComponent,
} from '@vegaprotocol/ui-toolkit';
import { BigNumber } from '../../../../lib/bignumber';
import type { NodesFragmentFragment } from '../__generated__/Nodes';
import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
import type { NodesFragmentFragment } from '../__generated___/Nodes';
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import type { StakingDelegationFieldsFragment } from '../../__generated__/Staking';
import type { ValidatorsView } from './validator-tables';
export enum ValidatorFields {
RANKING_INDEX = 'rankingIndex',
@@ -34,49 +31,12 @@ export enum ValidatorFields {
PERFORMANCE_PENALTY = 'performancePenalty',
OVERSTAKED_AMOUNT = 'overstakedAmount',
OVERSTAKING_PENALTY = 'overstakingPenalty',
// the following are additional fields added to the validator object displaying user data
STAKED_BY_USER = 'stakedByUser',
PENDING_USER_STAKE = 'pendingUserStake',
USER_STAKE_SHARE = 'userStakeShare',
}
export const addUserDataToValidator = (
validator: NodesFragmentFragment,
currentEpochUserStaking: StakingDelegationFieldsFragment | undefined,
nextEpochUserStaking: StakingDelegationFieldsFragment | undefined,
currentUserStakeAvailable: string
) => {
return {
...validator,
[ValidatorFields.STAKED_BY_USER]:
currentEpochUserStaking && Number(currentEpochUserStaking?.amount) > 0
? currentEpochUserStaking.amount
: undefined,
[ValidatorFields.PENDING_USER_STAKE]: nextEpochUserStaking
? new BigNumber(nextEpochUserStaking?.amount)
.minus(new BigNumber(currentEpochUserStaking?.amount || 0))
.toString()
: undefined,
[ValidatorFields.USER_STAKE_SHARE]:
currentEpochUserStaking && Number(currentEpochUserStaking.amount) > 0
? new BigNumber(currentEpochUserStaking.amount).dividedBy(
new BigNumber(currentUserStakeAvailable)
)
: undefined,
};
};
export type ValidatorWithUserData = NodesFragmentFragment & {
stakedByUser?: string;
pendingUserStake?: string;
userStakeShare?: string;
};
export interface ValidatorsTableProps {
data: ValidatorWithUserData[] | undefined;
data: NodesFragmentFragment[] | undefined;
previousEpochData: PreviousEpochQuery | undefined;
totalStake: string;
validatorsView: ValidatorsView;
}
// Custom styling to account for the scrollbar. This is needed because the
@@ -98,23 +58,19 @@ export const defaultColDef = {
resizable: true,
autoHeight: true,
comparator: (a: string, b: string) => parseFloat(a) - parseFloat(b),
cellStyle: { margin: '10px 0', padding: '0 12px' },
tooltipComponent: TooltipCellComponent,
cellStyle: { display: 'flex', alignItems: 'center', padding: '0 10px' },
};
interface ValidatorRendererProps {
data: {
id: string;
validator: { avatarUrl: string; name: string };
stakedByUser: string | undefined;
};
data: { id: string; validator: { avatarUrl: string; name: string } };
}
export const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
const { t } = useTranslation();
const { avatarUrl, name } = data.validator;
return (
<div className="w-[238px] grid grid-cols-[1fr_auto] gap-2 items-center">
<div className="grid grid-cols-[1fr_auto] gap-2 items-center">
<span className="flex overflow-hidden">
{avatarUrl && (
<img
@@ -127,19 +83,9 @@ export const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
<span>{name}</span>
</span>
<Link to={data.id}>
{data.stakedByUser ? (
<Button
data-testid="my-stake-btn"
size="sm"
className="text-vega-green border-vega-green"
>
{t('myStake')}
</Button>
) : (
<Button data-testid="stake-btn" size="sm" fill={true}>
{t('Stake')}
</Button>
)}
<Button size="sm" fill={true}>
{t('Stake')}
</Button>
</Link>
</div>
);
@@ -156,14 +102,8 @@ export const StakeNeededForPromotionRenderer = ({
data,
}: StakeNeededForPromotionRendererProps) => {
return (
<Tooltip
description={
<span data-testid="stake-needed-for-promotion-tooltip">
{data.stakeNeededForPromotionDescription}
</span>
}
>
<span data-testid="stake-needed-for-promotion">
<Tooltip description={data.stakeNeededForPromotionDescription}>
<span>
{data.stakeNeededForPromotion &&
formatNumber(data.stakeNeededForPromotion, 2)}
</span>
@@ -194,59 +134,7 @@ export const VotingPowerRenderer = ({ data }: VotingPowerRendererProps) => {
</>
}
>
<span data-testid="normalised-voting-power">
{data.normalisedVotingPower}
</span>
</Tooltip>
);
};
interface PendingStakeRendererProps {
data: {
pendingStake: string;
pendingUserStake: string | undefined;
};
}
export const PendingStakeRenderer = ({ data }: PendingStakeRendererProps) => {
const { t } = useTranslation();
const {
appState: { decimals },
} = useAppState();
return (
<Tooltip
description={
<>
<div data-testid="pending-stake-tooltip">
{t('pendingStake')}:{' '}
{formatNumber(toBigNum(data.pendingStake, decimals), decimals)}
</div>
{data.pendingUserStake && (
<div
className="text-vega-green border-t border-t-vega-dark-200 mt-1.5 pt-1"
data-testid="pending-user-stake-tooltip"
>
{t('myPendingStake')}:{' '}
{formatNumber(
toBigNum(data.pendingUserStake, decimals),
decimals
)}
</div>
)}
</>
}
>
<div className="flex flex-col">
{data.pendingUserStake && data.pendingStake !== '0' && (
<span data-testid="pending-user-stake" className="text-vega-green">
{formatNumber(toBigNum(data.pendingUserStake, decimals), 2)}
</span>
)}
<span data-testid="total-pending-stake">
{formatNumber(toBigNum(data.pendingStake, decimals), 2)}
</span>
</div>
<span>{data.normalisedVotingPower}</span>
</Tooltip>
);
};
@@ -256,7 +144,6 @@ interface TotalStakeRendererProps {
stake: string;
stakedByDelegates: string;
stakedByOperator: string;
stakedByUser: string | undefined;
};
}
@@ -278,52 +165,18 @@ export const TotalStakeRenderer = ({ data }: TotalStakeRendererProps) => {
<div data-testid="staked-delegates-tooltip">
{t('stakedByDelegates')}: {data.stakedByDelegates.toString()}
</div>
<div className="font-bold" data-testid="total-staked-tooltip">
{t('totalStake')}: {formattedStake}
<div data-testid="total-staked-tooltip">
{t('totalStake')}:{' '}
<span className="font-bold">{formattedStake}</span>
</div>
{data.stakedByUser && (
<div
className="text-vega-green border-t border-t-vega-dark-200 mt-1.5 pt-1"
data-testid="staked-by-user-tooltip"
>
{t('stakedByMe')}: {data.stakedByUser}
</div>
)}
</>
}
>
<div className="flex flex-col">
{data.stakedByUser && (
<span data-testid="user-stake" className="text-vega-green">
{data.stakedByUser}
</span>
)}
<span data-testid="total-stake">{formattedStake}</span>
</div>
<span>{formattedStake}</span>
</Tooltip>
);
};
interface StakeShareRendererProps {
data: {
stakeShare: string;
userStakeShare: string | undefined;
};
}
export const StakeShareRenderer = ({ data }: StakeShareRendererProps) => {
return (
<div className="flex flex-col">
{data.userStakeShare && (
<span data-testid="user-stake-share" className="text-vega-green">
{data.userStakeShare}
</span>
)}
<span data-testid="total-stake-share">{data.stakeShare}</span>
</div>
);
};
interface TotalPenaltiesRendererProps {
data: {
performanceScore: string;
@@ -356,7 +209,7 @@ export const TotalPenaltiesRenderer = ({
</>
}
>
<span data-testid="total-penalty">{data.totalPenalties}</span>
<span>{data.totalPenalties}</span>
</Tooltip>
);
};
@@ -7,6 +7,7 @@ import { BigNumber } from '../../../../lib/bignumber';
import {
getFormattedPerformanceScore,
getLastEpochScoreAndPerformance,
getOverstakedAmount,
getOverstakingPenalty,
getPerformancePenalty,
getTotalPenalties,
@@ -20,8 +21,6 @@ import {
ValidatorRenderer,
TotalPenaltiesRenderer,
TotalStakeRenderer,
StakeShareRenderer,
PendingStakeRenderer,
} from './shared';
import type { AgGridReact } from 'ag-grid-react';
import type { ColDef } from 'ag-grid-community';
@@ -39,7 +38,6 @@ export const StandbyPendingValidatorsTable = ({
totalStake,
stakeNeededForPromotion,
stakeNeededForPromotionDescription,
validatorsView,
}: StandbyPendingValidatorsTableProps) => {
const { t } = useTranslation();
const {
@@ -49,7 +47,7 @@ export const StandbyPendingValidatorsTable = ({
const gridRef = useRef<AgGridReact | null>(null);
let nodes = useMemo(() => {
const nodes = useMemo(() => {
if (!data) return [];
return data
@@ -77,25 +75,22 @@ export const StandbyPendingValidatorsTable = ({
rankingScore: { stakeScore },
pendingStake,
votingPowerRanking,
stakedByUser,
pendingUserStake,
userStakeShare,
}) => {
const {
rawValidatorScore: previousEpochValidatorScore,
performanceScore: previousEpochPerformanceScore,
stakeScore: previousEpochStakeScore,
} = getLastEpochScoreAndPerformance(previousEpochData, id);
const { rawValidatorScore, performanceScore } =
getLastEpochScoreAndPerformance(previousEpochData, id);
const overstakedAmount = getOverstakedAmount(
rawValidatorScore,
stakedTotal,
totalStake
);
let individualStakeNeededForPromotion,
individualStakeNeededForPromotionDescription;
if (stakeNeededForPromotion && previousEpochPerformanceScore) {
if (stakeNeededForPromotion && performanceScore) {
const stakedTotalBigNum = new BigNumber(stakedTotal);
const stakeNeededBigNum = new BigNumber(stakeNeededForPromotion);
const performanceScoreBigNum = new BigNumber(
previousEpochPerformanceScore
);
const performanceScoreBigNum = new BigNumber(performanceScore);
const calc = stakeNeededBigNum
.dividedBy(performanceScoreBigNum)
@@ -141,30 +136,22 @@ export const StandbyPendingValidatorsTable = ({
toBigNum(stakedByOperator, decimals),
2
),
[ValidatorFields.PERFORMANCE_SCORE]: getFormattedPerformanceScore(
previousEpochPerformanceScore
).toString(),
[ValidatorFields.PERFORMANCE_PENALTY]: getPerformancePenalty(
previousEpochPerformanceScore
),
[ValidatorFields.PERFORMANCE_SCORE]:
getFormattedPerformanceScore(performanceScore).toString(),
[ValidatorFields.PERFORMANCE_PENALTY]:
getPerformancePenalty(performanceScore),
[ValidatorFields.OVERSTAKED_AMOUNT]: overstakedAmount.toString(),
[ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty(
previousEpochValidatorScore,
previousEpochStakeScore
overstakedAmount,
totalStake
),
[ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties(
previousEpochValidatorScore,
previousEpochPerformanceScore,
rawValidatorScore,
performanceScore,
stakedTotal,
totalStake
),
[ValidatorFields.PENDING_STAKE]: pendingStake,
[ValidatorFields.STAKED_BY_USER]: stakedByUser
? formatNumber(toBigNum(stakedByUser, decimals), 2)
: undefined,
[ValidatorFields.PENDING_USER_STAKE]: pendingUserStake,
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
? stakedTotalPercentage(userStakeShare)
: undefined,
};
}
);
@@ -178,12 +165,6 @@ export const StandbyPendingValidatorsTable = ({
totalStake,
]);
if (validatorsView === 'myStake') {
nodes = nodes.filter(
(node) => node[ValidatorFields.STAKED_BY_USER] !== undefined
);
}
const StandbyPendingTable = forwardRef<AgGridReact>((_, gridRef) => {
const colDefs = useMemo<ColDef[]>(
() => [
@@ -199,7 +180,7 @@ export const StandbyPendingValidatorsTable = ({
cellRenderer: ValidatorRenderer,
comparator: ({ name: a }, { name: b }) => Math.sign(a - b),
pinned: 'left',
width: 260,
width: 240,
},
{
field: ValidatorFields.STAKE,
@@ -208,20 +189,6 @@ export const StandbyPendingValidatorsTable = ({
cellRenderer: TotalStakeRenderer,
width: 120,
},
{
field: ValidatorFields.PENDING_STAKE,
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
headerTooltip: t('PendingStakeDescription').toString(),
cellRenderer: PendingStakeRenderer,
width: 120,
},
{
field: ValidatorFields.STAKE_SHARE,
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
cellRenderer: StakeShareRenderer,
width: 100,
},
{
field: ValidatorFields.STAKE_NEEDED_FOR_PROMOTION,
headerName: t(ValidatorFields.STAKE_NEEDED_FOR_PROMOTION).toString(),
@@ -232,6 +199,12 @@ export const StandbyPendingValidatorsTable = ({
width: 210,
sort: 'asc',
},
{
field: ValidatorFields.STAKE_SHARE,
headerName: t(ValidatorFields.STAKE_SHARE).toString(),
headerTooltip: t('StakeShareDescription').toString(),
width: 100,
},
{
field: ValidatorFields.TOTAL_PENALTIES,
headerName: t(ValidatorFields.TOTAL_PENALTIES).toString(),
@@ -239,6 +212,14 @@ export const StandbyPendingValidatorsTable = ({
cellRenderer: TotalPenaltiesRenderer,
width: 120,
},
{
field: ValidatorFields.PENDING_STAKE,
headerName: t(ValidatorFields.PENDING_STAKE).toString(),
headerTooltip: t('PendingStakeDescription').toString(),
valueFormatter: ({ value }) =>
formatNumber(toBigNum(value, decimals), 2),
width: 110,
},
],
[]
);
@@ -249,7 +230,7 @@ export const StandbyPendingValidatorsTable = ({
domLayout="autoHeight"
style={{ width: '100%' }}
customThemeParams={NODE_LIST_GRID_STYLES}
rowHeight={68}
rowHeight={52}
defaultColDef={defaultColDef}
tooltipShowDelay={0}
animateRows={true}
@@ -1,30 +1,27 @@
import { useMemo, useState } from 'react';
import { useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import BigNumber from 'bignumber.js';
import { ConsensusValidatorsTable } from './consensus-validators-table';
import { StandbyPendingValidatorsTable } from './standby-pending-validators-table';
import * as Schema from '@vegaprotocol/types';
import { formatNumber } from '../../../../lib/format-number';
import {
createDocsLinks,
removePaginationWrapper,
toBigNum,
} from '@vegaprotocol/utils';
import { useEnvironment } from '@vegaprotocol/environment';
import { Link as UTLink, Toggle } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '../../../../lib/format-number';
import { Link as UTLink } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { useEnvironment } from '@vegaprotocol/environment';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { addUserDataToValidator } from './shared';
import { ConsensusValidatorsTable } from './consensus-validators-table';
import { StandbyPendingValidatorsTable } from './standby-pending-validators-table';
import type { NodesQuery, NodesFragmentFragment } from '../__generated__/Nodes';
import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
import type { StakingQuery } from '../../__generated__/Staking';
import type { StakingDelegationFieldsFragment } from '../../__generated__/Staking';
import type { ValidatorWithUserData } from './shared';
import type {
NodesQuery,
NodesFragmentFragment,
} from '../__generated___/Nodes';
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
import BigNumber from 'bignumber.js';
export interface ValidatorsTableProps {
nodesData: NodesQuery | undefined;
userStakingData: StakingQuery | undefined;
data: NodesQuery | undefined;
previousEpochData: PreviousEpochQuery | undefined;
}
@@ -34,11 +31,8 @@ interface SortedValidatorsProps {
pendingValidators: NodesFragmentFragment[];
}
export type ValidatorsView = 'all' | 'myStake';
export const ValidatorTables = ({
nodesData,
userStakingData,
data,
previousEpochData,
}: ValidatorsTableProps) => {
const { t } = useTranslation();
@@ -46,69 +40,25 @@ export const ValidatorTables = ({
const {
appState: { decimals },
} = useAppState();
const [validatorsView, setValidatorsView] = useState<ValidatorsView>('all');
const totalStake = useMemo(
() => nodesData?.nodeData?.stakedTotal || '0',
[nodesData?.nodeData?.stakedTotal]
() => data?.nodeData?.stakedTotal || '0',
[data?.nodeData?.stakedTotal]
);
const epochId = useMemo(() => nodesData?.epoch.id, [nodesData?.epoch.id]);
const currentUserStakeAvailable = useMemo(
() => userStakingData?.party?.stakingSummary.currentStakeAvailable || '0',
[userStakingData?.party?.stakingSummary.currentStakeAvailable]
);
let stakeNeededForPromotion = undefined;
let delegations: StakingDelegationFieldsFragment[] | undefined = undefined;
if (userStakingData) {
delegations = removePaginationWrapper(
userStakingData?.party?.delegationsConnection?.edges
);
}
const { consensusValidators, standbyValidators, pendingValidators } = useMemo(
() =>
removePaginationWrapper(nodesData?.nodesConnection.edges).reduce(
removePaginationWrapper(data?.nodesConnection.edges).reduce(
(acc: SortedValidatorsProps, validator) => {
const validatorId = validator.id;
const currentDelegation = delegations?.find(
(d) => d.node.id === validatorId && d.epoch === Number(epochId)
);
const nextDelegation = delegations?.find(
(d) => d.node.id === validatorId && d.epoch === Number(epochId) + 1
);
switch (validator.rankingScore?.status) {
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT:
acc.consensusValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
acc.consensusValidators.push(validator);
break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ:
acc.standbyValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
acc.standbyValidators.push(validator);
break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_PENDING:
acc.pendingValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
acc.pendingValidators.push(validator);
}
return acc;
},
@@ -118,12 +68,7 @@ export const ValidatorTables = ({
pendingValidators: [],
}
),
[
currentUserStakeAvailable,
delegations,
epochId,
nodesData?.nodesConnection.edges,
]
[data?.nodesConnection.edges]
);
if (
@@ -131,7 +76,7 @@ export const ValidatorTables = ({
(standbyValidators.length || pendingValidators.length)
) {
const lowestRankingConsensusScore = consensusValidators.reduce(
(lowest: ValidatorWithUserData, validator: ValidatorWithUserData) => {
(lowest: NodesFragmentFragment, validator: NodesFragmentFragment) => {
if (
Number(validator.rankingScore.rankingScore) <
Number(lowest.rankingScore.rankingScore)
@@ -153,42 +98,19 @@ export const ValidatorTables = ({
).toString();
}
return (
<section data-testid="validator-tables">
<div className="grid w-full justify-end">
<div className="w-[400px]">
<Toggle
name="validators-view-toggle"
toggles={[
{
label: t('ALL VALIDATORS'),
value: 'all',
},
{
label: t('STAKED BY ME'),
value: 'myStake',
},
]}
checkedValue={validatorsView}
onChange={(e) =>
setValidatorsView(e.target.value as ValidatorsView)
}
/>
</div>
</div>
<div data-testid="validator-tables">
{consensusValidators.length > 0 && (
<div className="mb-10">
<>
<SubHeading title={t('status-tendermint')} />
<ConsensusValidatorsTable
data={consensusValidators}
previousEpochData={previousEpochData}
totalStake={totalStake}
validatorsView={validatorsView}
/>
</div>
</>
)}
{standbyValidators.length > 0 && (
<div className="mb-10">
<>
<SubHeading title={t('status-ersatz')} />
<p>
<Trans
@@ -204,9 +126,8 @@ export const ValidatorTables = ({
totalStake={totalStake}
stakeNeededForPromotion={stakeNeededForPromotion}
stakeNeededForPromotionDescription="StakeNeededForPromotionStandbyDescription"
validatorsView={validatorsView}
/>
</div>
</>
)}
{pendingValidators.length > 0 && (
<>
@@ -234,10 +155,9 @@ export const ValidatorTables = ({
totalStake={totalStake}
stakeNeededForPromotion={stakeNeededForPromotion}
stakeNeededForPromotionDescription="StakeNeededForPromotionCandidateDescription"
validatorsView={validatorsView}
/>
</>
)}
</section>
</div>
);
};
@@ -23,17 +23,6 @@ fragment StakingNodeFields on Node {
}
}
fragment StakingDelegationFields on Delegation {
amount
epoch
node {
id
}
party {
id
}
}
query Staking($partyId: ID!, $delegationsPagination: Pagination) {
party(id: $partyId) {
id
@@ -43,7 +32,11 @@ query Staking($partyId: ID!, $delegationsPagination: Pagination) {
delegationsConnection(pagination: $delegationsPagination) {
edges {
node {
...StakingDelegationFields
amount
epoch
node {
id
}
}
}
}
@@ -5,15 +5,13 @@ import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type StakingNodeFieldsFragment = { __typename?: 'Node', id: string, name: string, pubkey: string, infoUrl: string, location: string, ethereumAddress: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } };
export type StakingDelegationFieldsFragment = { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string }, party: { __typename?: 'Party', id: string } };
export type StakingQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
delegationsPagination?: Types.InputMaybe<Types.Pagination>;
}>;
export type StakingQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string }, party: { __typename?: 'Party', id: string } } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } }, nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, pubkey: string, infoUrl: string, location: string, ethereumAddress: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } } } | null> | null }, nodeData?: { __typename?: 'NodeData', stakedTotal: string, totalNodes: number, inactiveNodes: number, uptime: number } | null };
export type StakingQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string } } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } }, nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, pubkey: string, infoUrl: string, location: string, ethereumAddress: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null, rankingScore: { __typename?: 'RankingScore', rankingScore: string, stakeScore: string, performanceScore: string, votingPower: string, status: Types.ValidatorStatus } } } | null> | null }, nodeData?: { __typename?: 'NodeData', stakedTotal: string, totalNodes: number, inactiveNodes: number, uptime: number } | null };
export const StakingNodeFieldsFragmentDoc = gql`
fragment StakingNodeFields on Node {
@@ -41,18 +39,6 @@ export const StakingNodeFieldsFragmentDoc = gql`
}
}
`;
export const StakingDelegationFieldsFragmentDoc = gql`
fragment StakingDelegationFields on Delegation {
amount
epoch
node {
id
}
party {
id
}
}
`;
export const StakingDocument = gql`
query Staking($partyId: ID!, $delegationsPagination: Pagination) {
party(id: $partyId) {
@@ -63,7 +49,11 @@ export const StakingDocument = gql`
delegationsConnection(pagination: $delegationsPagination) {
edges {
node {
...StakingDelegationFields
amount
epoch
node {
id
}
}
}
}
@@ -90,8 +80,7 @@ export const StakingDocument = gql`
uptime
}
}
${StakingDelegationFieldsFragmentDoc}
${StakingNodeFieldsFragmentDoc}`;
${StakingNodeFieldsFragmentDoc}`;
/**
* __useStakingQuery__
@@ -20,8 +20,8 @@ import NodeContainer from './nodes-container';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import { Heading, SubHeading } from '../../../components/heading';
import Routes from '../../routes';
import type { StakingQuery } from '../__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated__/PreviousEpoch';
import type { StakingQuery } from './__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
interface StakingNodeProps {
data?: StakingQuery;
@@ -116,6 +116,13 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
t('validatorTitle', { nodeName: t('validatorTitleFallback') })
}
/>
<section className="mb-4">
<ValidatorTable
node={nodeInfo}
stakedTotal={addDecimal(data?.nodeData?.stakedTotal || '0', decimals)}
previousEpochData={previousEpochData}
/>
</section>
{data?.epoch.timestamps.start && data?.epoch.timestamps.expiry && (
<section className="mb-10">
<EpochCountdown
@@ -150,13 +157,6 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
<ConnectToVega />
</>
)}
<section className="mb-4">
<ValidatorTable
node={nodeInfo}
stakedTotal={addDecimal(data?.nodeData?.stakedTotal || '0', decimals)}
previousEpochData={previousEpochData}
/>
</section>
</div>
);
};
@@ -4,11 +4,11 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { useTranslation } from 'react-i18next';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { SplashLoader } from '../../../components/splash-loader';
import { useStakingQuery } from '../__generated__/Staking';
import { usePreviousEpochQuery } from '../__generated__/PreviousEpoch';
import { useStakingQuery } from './__generated__/Staking';
import { usePreviousEpochQuery } from '../__generated___/PreviousEpoch';
import type { ReactElement } from 'react';
import type { StakingQuery } from '../__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated__/PreviousEpoch';
import type { StakingQuery } from './__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
// TODO should only request a single node. When migrating from deprecated APIs we should address this.
@@ -20,6 +20,7 @@ import { SubHeading } from '../../../components/heading';
import {
getLastEpochScoreAndPerformance,
getNormalisedVotingPower,
getOverstakedAmount,
getOverstakingPenalty,
getPerformancePenalty,
getTotalPenalties,
@@ -27,8 +28,8 @@ import {
getStakePercentage,
} from '../shared';
import type { ReactNode } from 'react';
import type { StakingNodeFieldsFragment } from '../__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated__/PreviousEpoch';
import type { StakingNodeFieldsFragment } from './__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
const statuses = {
[Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ]: 'status-ersatz',
@@ -74,9 +75,15 @@ export const ValidatorTable = ({
const stakedOnNode = toBigNum(node.stakedTotal, decimals);
const { rawValidatorScore, performanceScore, stakeScore } =
const { rawValidatorScore, performanceScore } =
getLastEpochScoreAndPerformance(previousEpochData, node.id);
const overstakedAmount = getOverstakedAmount(
rawValidatorScore,
stakedTotal,
node.stakedTotal
);
const stakePercentage = getStakePercentage(total, stakedOnNode);
const totalPenaltiesAmount = getTotalPenalties(
@@ -104,7 +111,7 @@ export const ValidatorTable = ({
<div className="my-12" data-testid="validator-table">
<SubHeading title={t('profile')} />
<RoundedWrapper paddingBottom={true}>
<RoundedWrapper>
<KeyValueTable data-testid="validator-table-profile">
<KeyValueTableRow>
<span>{t('id')}</span>
@@ -149,7 +156,7 @@ export const ValidatorTable = ({
</div>
<SubHeading title={t('ADDRESS')} />
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-address">
<KeyValueTableRow>
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
@@ -180,7 +187,7 @@ export const ValidatorTable = ({
</RoundedWrapper>
<SubHeading title={t('STAKE')} />
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-stake">
<KeyValueTableRow>
<span>{t('STAKED BY OPERATOR')}</span>
@@ -231,14 +238,14 @@ export const ValidatorTable = ({
</RoundedWrapper>
<SubHeading title={t('PENALTIES')} />
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-penalties">
<KeyValueTableRow>
<span>{t('OVERSTAKED PENALTY')}</span>
<Tooltip description={t('OverstakedPenaltyDescription')}>
<span data-testid="overstaking-penalty">
{getOverstakingPenalty(rawValidatorScore, stakeScore)}
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
</span>
</Tooltip>
</KeyValueTableRow>
@@ -263,7 +270,7 @@ export const ValidatorTable = ({
</RoundedWrapper>
<SubHeading title={t('VOTING POWER')} />
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<RoundedWrapper marginBottomLarge={true}>
<KeyValueTable data-testid="validator-table-voting-power">
<KeyValueTableRow>
<span>{t('UNNORMALISED VOTING POWER')}</span>
@@ -23,7 +23,7 @@ export const YourStake = ({
return (
<div data-testid="your-stake">
<SubHeading title={t('Your stake')} />
<RoundedWrapper paddingBottom={true}>
<RoundedWrapper>
<KeyValueTable>
<KeyValueTableRow>
{t('Your Stake On Node (This Epoch)')}
@@ -4,6 +4,7 @@ import {
getNormalisedVotingPower,
getUnnormalisedVotingPower,
getOverstakingPenalty,
getOverstakedAmount,
getFormattedPerformanceScore,
getPerformancePenalty,
getTotalPenalties,
@@ -21,10 +22,9 @@ describe('getLastEpochScoreAndPerformance', () => {
id: '0x123',
rewardScore: {
rawValidatorScore: '0.25',
performanceScore: '0.75',
},
rankingScore: {
stakeScore: '0.25',
performanceScore: '0.75',
},
},
},
@@ -33,10 +33,9 @@ describe('getLastEpochScoreAndPerformance', () => {
id: '0x234',
rewardScore: {
rawValidatorScore: '0.35',
performanceScore: '0.85',
},
rankingScore: {
stakeScore: '0.25',
performanceScore: '0.85',
},
},
},
@@ -51,14 +50,12 @@ describe('getLastEpochScoreAndPerformance', () => {
).toEqual({
rawValidatorScore: '0.25',
performanceScore: '0.75',
stakeScore: '0.25',
});
expect(
getLastEpochScoreAndPerformance(mockPreviousEpochData, '0x234')
).toEqual({
rawValidatorScore: '0.35',
performanceScore: '0.85',
stakeScore: '0.25',
});
});
});
@@ -82,34 +79,40 @@ describe('getUnnormalisedVotingPower', () => {
});
describe('getOverstakingPenalty', () => {
it('returns "0%" when both arguments are null or undefined', () => {
expect(getOverstakingPenalty(null, null)).toBe('0%');
expect(getOverstakingPenalty(undefined, undefined)).toBe('0%');
expect(getOverstakingPenalty(null, undefined)).toBe('0%');
expect(getOverstakingPenalty(undefined, null)).toBe('0%');
it('should return the overstaking penalty', () => {
expect(
getOverstakingPenalty(new BigNumber(100), Number(1000).toString())
).toEqual('10.00%');
expect(
getOverstakingPenalty(new BigNumber(500), Number(2000).toString())
).toEqual('25.00%');
});
});
describe('getOverstakedAmount', () => {
it('should return the overstaked amount', () => {
expect(
// If a validator score is 0, any amount staked on the node is considered overstaked
getOverstakedAmount('0', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(20));
expect(
getOverstakedAmount('0.05', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(15));
expect(
getOverstakedAmount('0.1', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(10));
expect(
getOverstakedAmount('0.15', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(5));
expect(
getOverstakedAmount('0.2', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(0));
});
it('returns "0%" when one argument is null or undefined', () => {
expect(getOverstakingPenalty('10', null)).toBe('0%');
expect(getOverstakingPenalty(null, '20')).toBe('0%');
expect(getOverstakingPenalty('10', undefined)).toBe('0%');
expect(getOverstakingPenalty(undefined, '20')).toBe('0%');
});
it('returns "0%" when validatorScore or stakeScore is zero', () => {
expect(getOverstakingPenalty('0', '20')).toBe('0%');
expect(getOverstakingPenalty('10', '0')).toBe('0%');
});
it('returns the correct overstaking penalty', () => {
expect(getOverstakingPenalty('0.18', '0.2')).toBe('10.00%');
expect(getOverstakingPenalty('0.2', '0.2')).toBe('0.00%');
expect(getOverstakingPenalty('0.04', '0.2')).toBe('80.00%');
});
it('handles string numbers with decimals', () => {
expect(getOverstakingPenalty('7.5', '15')).toBe('50.00%');
expect(getOverstakingPenalty('12.5', '25')).toBe('50.00%');
it('should return 0 if the overstaked amount is negative', () => {
expect(
getOverstakedAmount('0.8', Number(100).toString(), Number(20).toString())
).toEqual(new BigNumber(0));
});
});
+20 -16
View File
@@ -2,7 +2,7 @@ import {
formatNumberPercentage,
removePaginationWrapper,
} from '@vegaprotocol/utils';
import type { PreviousEpochQuery } from './__generated__/PreviousEpoch';
import type { PreviousEpochQuery } from './__generated___/PreviousEpoch';
import { BigNumber } from '../../lib/bignumber';
export const getLastEpochScoreAndPerformance = (
@@ -15,8 +15,7 @@ export const getLastEpochScoreAndPerformance = (
return {
rawValidatorScore: validator?.rewardScore?.rawValidatorScore,
performanceScore: validator?.rewardScore?.performanceScore,
stakeScore: validator?.rankingScore?.stakeScore,
performanceScore: validator?.rankingScore?.performanceScore,
};
};
@@ -43,26 +42,31 @@ export const getPerformancePenalty = (performanceScore?: string) =>
2
);
export const getOverstakingPenalty = (
export const getOverstakedAmount = (
validatorScore: string | null | undefined,
stakeScore: string | null | undefined
totalStake: string,
stakedOnNode: string
) => {
if (!validatorScore || !stakeScore) {
return '0%';
}
const toReturn = validatorScore
? new BigNumber(stakedOnNode).minus(
new BigNumber(validatorScore).times(new BigNumber(totalStake))
)
: new BigNumber(0);
return toReturn.isNegative() ? new BigNumber(0) : toReturn;
};
export const getOverstakingPenalty = (
overstakedAmount: BigNumber,
stakedOnNode: string
) => {
// avoid division by zero
if (
new BigNumber(validatorScore).isZero() ||
new BigNumber(stakeScore).isZero()
) {
return '0%';
if (new BigNumber(stakedOnNode).isZero() || overstakedAmount.isZero()) {
return '0';
}
return formatNumberPercentage(
new BigNumber(1)
.minus(new BigNumber(validatorScore).dividedBy(new BigNumber(stakeScore)))
.times(100),
overstakedAmount.dividedBy(new BigNumber(stakedOnNode)).times(100),
2
);
};

Some files were not shown because too many files have changed in this diff Show More