Compare commits

..
Author SHA1 Message Date
Dexter ba45e090f3 chore: make log level customisable 2023-04-19 17:16:18 +01:00
Ciaran McGhie 48072eb4c3 chore(react-helpers): ugprade react-helpers to 0.2.0 and ui-toolkit to 0.11.0 (#3469) 2023-04-19 16:07:35 +01:00
Sam Keen ae92ff2580 feat(governance): protocol upgrade proposals list and details (#3363) 2023-04-19 13:44:36 +01:00
dexturr c99b7fbb02 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-19 12:07:50 +00:00
Joe Tsang a9b1767a42 fix(governance): fix flaky tests due to wallet teardown (#3460) 2023-04-19 11:54:04 +01:00
Ciaran McGhie 3cd60ad3e6 chore(ui-toolkit): bump ui toolkit to 0.10.0 (#3458) 2023-04-19 09:46:57 +01:00
Art 2f4bc9484b feat(environment): github releases (#3426) 2023-04-19 09:42:37 +01:00
dexturr d4f5643799 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-19 06:09:43 +00:00
dexturr 72c49e62a8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-19 00:14:26 +00:00
dexturr 0988edc129 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-18 18:07:34 +00:00
botond 4d266963ae feat(governance): paginate rewards (#3391) 2023-04-18 14:40:57 +01:00
Bartłomiej Głownia ec12c7ecfd fix(trading): update cached Order objects (#3450) 2023-04-18 13:56:13 +01:00
m.ray 045dace274 feat(trading): add liquidity provision link to orders table (#3432) 2023-04-18 13:55:11 +01:00
Sam KeenandMatthew Russell 2346b2f1a0 feat(governance): full width Governance and Explorer header (#3453)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-04-18 13:06:30 +01:00
Bartłomiej Głownia 31859cb779 chore(trading): maintain always up to date active orders list (#3430) 2023-04-18 12:49:42 +02:00
dexturr 20507ecdd1 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-18 06:08:58 +00:00
dexturr 13ba661ea0 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-18 00:16:31 +00:00
dexturr 03f5b9fc7b chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-17 18:07:00 +00:00
dexturr b9fed65615 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-17 12:10:37 +00:00
Mikołaj Młodzikowski d671e51edf fix(ci): adjust timeouts 2023-04-17 12:45:30 +02:00
Mikołaj Młodzikowski 4f3295d3fc feat(ci): rebuild yarn on failed build 2023-04-17 11:24:53 +02:00
dexturr 7be7bab1ca chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-17 06:09:49 +00:00
dexturr 61380f06b9 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-17 00:14:49 +00:00
dexturr c113856d63 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-16 18:07:16 +00:00
dexturr 90884beee8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-16 12:07:18 +00:00
dexturr d9d2e428f4 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-16 06:09:55 +00:00
dexturr ba8d54b761 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-16 00:15:24 +00:00
dexturr dce4edb943 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-15 18:06:56 +00:00
dexturr b468a0fd84 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-15 12:07:02 +00:00
dexturr c98fbe7453 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-15 06:06:58 +00:00
dexturr d20928ec06 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-15 00:15:18 +00:00
dexturr 289559fe4f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-14 18:09:58 +00:00
Mikołaj Młodzikowski 071fd6128c feat: fix releasing dist 2023-04-14 15:45:14 +02:00
Mikołaj Młodzikowski 554d07a31b feat(ci): fix regressions detected after merging to develop (#3442) 2023-04-14 15:00:20 +02:00
dexturr 48b62380b0 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-14 12:10:09 +00:00
Mikołaj Młodzikowski a9f4604bad feat(governance,trading,explorer): pipelines improvements (#3422) 2023-04-14 11:47:41 +02:00
Sam Keen 54f2519a62 feat(governance): toggles design review tweaks (#3431) 2023-04-14 09:22:22 +01:00
dexturr 260e7bf9cf chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-14 06:09:41 +00:00
dexturr 83db796cdb chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-14 00:14:08 +00:00
m.ray 2f6800212e fix(trading): fix order toast intent on amendments (#3433) 2023-04-13 13:20:28 -07:00
dexturr 77e16ad1b7 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-13 18:09:10 +00:00
dexturr dcce983842 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-13 12:10:33 +00:00
Maciek 53ce9bfaa2 chore(assets): make query for asset list lighter (#3425) 2023-04-13 11:42:09 +00:00
Edd 6ae34d296f fix(explorer): tweak footer to show more detail on smaller screens (#3418) 2023-04-13 10:49:42 +01:00
Joe Tsang ee2aafb99c fix(governance): failing nightly tests (#3351) 2023-04-13 10:46:01 +01:00
dexturr e8fdb63323 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-13 06:09:41 +00:00
dexturr 3df9270b57 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-13 00:13:51 +00:00
dexturr ab26667265 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-12 18:08:22 +00:00
m.ray 9fc6b255c8 fix(orders): update order toast intent and title (#3424) 2023-04-12 15:19:15 +00:00
dexturr 91f61d8bb5 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-12 12:09:32 +00:00
Bartłomiej Głowniaandm.ray bbfda65bcc feat(withdraws): improve ux surrounding withdraw balances thresholds and delays (#3402)
Co-authored-by: m.ray <16125548+MadalinaRaicu@users.noreply.github.com>
2023-04-12 11:21:24 +02:00
Mikołaj Młodzikowski 45f2e926c3 feat(governance,trading,explorer): split steps by logic (#3387) 2023-04-12 09:38:26 +02:00
dexturr ccadbc2cb6 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-12 06:08:40 +00:00
dexturr 7128acb7b8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-12 00:14:18 +00:00
dexturr 357bdaa4f3 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-11 18:07:51 +00:00
124 changed files with 44472 additions and 1108 deletions
+85 -5
View File
@@ -4,14 +4,54 @@ on:
push:
branches:
- release/*
- develop
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
- reopened
- edited
- synchronize
jobs:
node-modules:
runs-on: ubuntu-22.04
name: 'Cache yarn modules'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Cache node modules
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
# comment out "resotre-keys" if you need to rebuild yarn from 0
restore-keys: |
${{ runner.os }}-cache-node-modules-
- name: Setup node
uses: actions/setup-node@v3
if: steps.cache.outputs.cache-hit != 'true'
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: yarn install
if: steps.cache.outputs.cache-hit != 'true'
run: yarn install --pure-lockfile
lint-pr-title:
needs: node-modules
if: ${{ github.event_name == 'pull_request' }}
name: Verify PR title
uses: ./.github/workflows/lint-pr.yml
secrets: inherit
lint-test-build:
timeout-minutes: 20
needs: node-modules
runs-on: ubuntu-22.04
name: '(CI) lint + unit test + build'
steps:
@@ -27,8 +67,11 @@ jobs:
# 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: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v3
@@ -48,7 +91,7 @@ jobs:
run: yarn nx affected:test
- name: Build affected
run: yarn nx affected:build
run: yarn nx affected:build || (yarn install && yarn nx affected:build)
# See affected apps
- name: See affected apps
@@ -95,6 +138,43 @@ jobs:
with:
projects: ${{ needs.lint-test-build.outputs.projects }}
dist-check:
runs-on: ubuntu-latest
needs: publish-dist
if: ${{ github.event_name == 'pull_request' }}
name: '(CD) comment preview links'
steps:
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: Previews
- name: Inject slug/short variables
if: ${{ steps.fc.outputs.comment-id == 0 }}
uses: rlespinasse/github-slug-action@v4
with:
prefix: CI_
- name: Create comment
if: ${{ steps.fc.outputs.comment-id == 0 }}
uses: peter-evans/create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
Previews
- explorer https://explorer.${{ env.CI_GITHUB_REF_NAME }}.vega.rocks
- trading https://trading.${{ env.CI_GITHUB_REF_NAME }}.vega.rocks
- governance https://governance.${{ env.CI_GITHUB_REF_NAME }}.vega.rocks
cypress-check:
name: '(CI) cypress - check'
runs-on: ubuntu-latest
needs: cypress
steps:
- run: echo Done!
# Report single result at the end, to avoid mess with required checks in PR
cypress-result:
if: ${{ always() }}
@@ -2,15 +2,11 @@
name: Verify PR title
on:
pull_request:
types:
- opened
- ready_for_review
- reopened
- edited
- synchronize
workflow_call:
jobs:
lint_pr:
timeout-minutes: 10
runs-on: ubuntu-22.04
steps:
- name: Checkout
@@ -23,8 +19,11 @@ jobs:
# 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: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
- name: Check PR title
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
+92 -29
View File
@@ -15,6 +15,7 @@ jobs:
app: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.app }}
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
- name: Check out code
uses: actions/checkout@v3
@@ -36,66 +37,131 @@ jobs:
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}
- 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: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
# https://docs.github.com/en/actions/learn-github-actions/contexts
- name: Define variables
run: |
envName=''
dockerfile="dist.Dockerfile"
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}
domain="vega.rocks"
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
if [[ "${{ github.ref }}" =~ .*mainnet.* ]]; then
domain="vega.community"
if [[ "${{ matrix.app }}" = "trading" ]]; then
dockerfile="ipfs.Dockerfile"
fi
fi
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
envName="stagnet3"
fi
bucketName="${{ matrix.app }}.${envName}.${domain}"
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
fi
nodeVersion=$(cat .nvmrc | head -n 1)
echo ENV_NAME=${envName} >> $GITHUB_ENV
echo NODE_VERSION=${nodeVersion} >> $GITHUB_ENV
echo DOCKERFILE=docker/${dockerfile} >> $GITHUB_ENV
- name: Build local dist
if: ${{ env.DOCKERFILE != 'docker/ipfs.Dockerfile' }}
run: |
flags=""
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
if [[ "${{ env.ENV_NAME }}" != "ops-vega" ]]; then
flags="--env=${{ env.ENV_NAME }}"
fi
fi
if [ "${{ matrix.app }}" = "trading" ]; then
yarn nx export trading $flags || (yarn install && yarn nx export trading $flags)
DIST_LOCATION=dist/apps/trading/exported
else
yarn nx build ${{ matrix.app }} $flags || (yarn install && yarn nx build ${{ matrix.app }} $flags)
DIST_LOCATION=dist/apps/${{ matrix.app }}
fi
mv $DIST_LOCATION dist-result
tree dist-result
- name: Build and export to local Docker
id: docker_build
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
uses: docker/build-push-action@v3
with:
context: .
file: ${{ env.DOCKERFILE }}
load: true
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.nodeVersion }}
ENV_NAME=${{ steps.tags.outputs.envName || '' }}
NODE_VERSION=${{ env.NODE_VERSION }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
- name: Image digest
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
run: echo ${{ steps.docker_build.outputs.digest }}
- name: Sanity check docker image
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
run: |
echo "Check ipfs-hash"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat ipfs-hash
if [[ "${{ env.DOCKERFILE }}" = "docker/ipfs.Dockerfile" ]]; then
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
fi
echo "List html directory"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local ls -lah
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree .'
echo "Copy dist to local filesystem"
- name: Copy dist to local filesystem
if: ${{ env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' }}
run: |
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
echo "check local dist files"
tree dist/html
mv dist/html dist-result
- name: Publish dist as docker image
uses: docker/build-push-action@v3
if: ${{ github.event_name == 'pull_request' }}
with:
context: .
file: ${{ env.DOCKERFILE }}
push: true
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.nodeVersion }}
NODE_VERSION=${{ env.NODE_VERSION }}
ENV_NAME=${{ env.ENV_NAME }}
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'
# bucket creation in github.com/vegaprotocol/terraform//frontend
- name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master
if: ${{ github.event_name == 'push' }}
with:
args: --acl private --follow-symlinks --delete
env:
AWS_S3_BUCKET: ${{ env.BUCKET_NAME }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: 'eu-west-1'
SOURCE_DIR: 'dist-result'
- name: Add preview label
uses: actions-ecosystem/action-add-labels@v1
@@ -103,6 +169,3 @@ jobs:
with:
labels: ${{ matrix.app }}-preview
number: ${{ github.event.number }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
+1 -1
View File
@@ -3,7 +3,7 @@ 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
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
# App flags
NX_EXPLORER_ASSETS=1
+1 -1
View File
@@ -4,4 +4,4 @@ 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
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
+6 -4
View File
@@ -38,14 +38,16 @@ const DialogsContainer = () => {
export const Layout = () => {
const isHome = Boolean(useMatch(Routes.HOME));
const { ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
const fixedWidthClasses = 'w-full max-w-[1500px] mx-auto';
return (
<>
<div
className={classNames(
'max-w-[1500px] min-h-[100vh]',
'min-h-screen',
'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',
'border-vega-light-200 dark:border-vega-dark-200',
'antialiased text-black dark:text-white',
'overflow-hidden relative'
)}
@@ -59,13 +61,13 @@ export const Layout = () => {
)}
<Header />
</div>
<div>
<div className={fixedWidthClasses}>
<main className="p-4">
{!isHome && <BreadcrumbsContainer className="mb-4" />}
<Outlet />
</main>
</div>
<div>
<div className={fixedWidthClasses}>
<Footer />
</div>
</div>
+1 -1
View File
@@ -13,7 +13,7 @@ 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_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
#Test configuration variables
@@ -44,7 +44,8 @@ describe(
function () {
before('connect wallets and set approval limit', function () {
cy.visit('/');
vegaWalletSetSpecifiedApprovalAmount('1000');
ethereumWalletConnect();
cy.associateTokensToVegaWallet('1');
});
beforeEach('visit proposals tab', function () {
@@ -213,6 +214,7 @@ describe(
// 3001-VOTE-042, 3001-VOTE-057, 3001-VOTE-058, 3001-VOTE-059, 3001-VOTE-060
it('Newly created proposal details - ability to increase associated tokens - by voting again after association', function () {
vegaWalletSetSpecifiedApprovalAmount('1000');
createRawProposal();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getSubmittedProposalFromProposalList(rawProposal.rationale.title)
@@ -13,7 +13,6 @@ import {
createUpdateNetworkProposalTxBody,
createFreeFormProposalTxBody,
} from '../../support/proposal.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
@@ -33,10 +32,6 @@ context(
before('Connect wallets and set approval', function () {
cy.visit('/');
vegaWalletSetSpecifiedApprovalAmount('1000');
cy.connectVegaWallet();
ethereumWalletConnect();
ensureSpecifiedUnstakedTokensAreAssociated('1');
cy.clearLocalStorage();
});
beforeEach('visit proposals', function () {
@@ -114,7 +109,7 @@ context(
navigateTo(navigation.proposals);
cy.reload();
waitForSpinner();
cy.get(openProposals).within(() => {
cy.get(openProposals, { timeout: 6000 }).within(() => {
cy.contains(proposalTitle)
.parentsUntil('[data-testid="proposals-list-item"]')
.within(() => cy.get(viewProposalButton).click());
@@ -232,7 +232,7 @@ context(
it('Unable to create a freeform proposal - when json parent section contains unexpected field', function () {
const errorMsg =
'Invalid params: the transaction does not use a valid Vega command: unknown field unexpected" in vega.commands.v1.ProposalSubmission';
'Invalid params: the transaction does not use a valid Vega command: unknown field "unexpected" in vega.commands.v1.ProposalSubmission';
// 3001-VOTE-038 3002-PROP-013 3002-PROP-014
goToMakeNewProposal(governanceProposalType.RAW);
@@ -313,7 +313,7 @@ context(
it('Unable to vote on a proposal - when vega wallet disconnected - option to connect from within', function () {
createRawProposal();
cy.get('[data-testid="manage-vega-wallet"]').click();
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="disconnect"]').click();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getSubmittedProposalFromProposalList(
@@ -218,7 +218,7 @@ context(
it('Unable to submit new market proposal with missing/invalid fields', function () {
const errorMsg =
'Invalid params: the transaction is not a valid Vega command: unknown field "filters" in vega.DataSourceDefinition';
'Invalid params: the transaction does not use a valid Vega command: unknown field "invalid" in vega.NewMarket';
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.get(newProposalSubmitButton).should('be.visible').click();
@@ -436,7 +436,7 @@ context(
});
});
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();
@@ -12,7 +12,6 @@ import {
generateFreeFormProposalTitle,
getProposalIdFromList,
getProposalInformationFromTable,
getSortOrderOfSuppliedArray,
getSubmittedProposalFromProposalList,
goToMakeNewProposal,
governanceProposalType,
@@ -24,9 +23,9 @@ import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/stakin
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
const proposalDetailsTitle = '[data-testid="proposal-title"]';
const openProposals = '[data-testid="open-proposals"]';
const voteStatus = '[data-testid="vote-status"]';
const proposalClosingDate = '[data-testid="vote-details"]';
const viewProposalButton = '[data-testid="view-proposal-btn"]';
describe('Governance flow for proposal list', { tags: '@slow' }, function () {
@@ -45,16 +44,8 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
});
it('Newly created proposals list - proposals closest to closing date appear higher in list', function () {
const minCloseDays = 2;
const maxCloseDays = 3;
// 3001-VOTE-005
const proposalDays = [
minCloseDays + 1,
maxCloseDays,
minCloseDays + 3,
minCloseDays + 2,
];
const proposalDays = [364, 50, 2];
for (let index = 0; index < proposalDays.length; index++) {
goToMakeNewProposal(governanceProposalType.RAW);
enterRawProposalBody(
@@ -64,19 +55,12 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
waitForProposalSync();
}
const arrayOfProposals: string[] = [];
navigateTo(navigation.proposals);
cy.get(proposalDetailsTitle)
.each((proposalTitleElement) => {
arrayOfProposals.push(proposalTitleElement.text());
})
.then(() => {
cy.wrap(getSortOrderOfSuppliedArray(arrayOfProposals)).should(
'equal',
'descending'
);
});
cy.get(openProposals).within(() => {
cy.get(proposalClosingDate).first().should('contain.text', 'year');
cy.get(proposalClosingDate).should('contain.text', 'months');
cy.get(proposalClosingDate).last().should('contain.text', 'days');
});
});
it('Newly created proposals list - able to filter by proposerID to show it in list', function () {
@@ -25,11 +25,11 @@ const vegaWalletUnstakedBalance =
'[data-testid="vega-wallet-balance-unstaked"]';
const txTimeout = Cypress.env('txTimeout');
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
const ethWalletAssociateButton = '[data-testid="associate-btn"]';
const ethWalletAssociateButton = '[data-testid="associate-btn"]:visible';
const associateWalletRadioButton = '[data-testid="associate-radio-wallet"]';
const tokenAmountInputBox = '[data-testid="token-amount-input"]';
const tokenSubmitButton = '[data-testid="token-input-submit-button"]';
const ethWalletDissociateButton = '[href="/token/disassociate"]';
const ethWalletDissociateButton = '[href="/token/disassociate"]:visible';
const vestingContractSection = '[data-testid="vega-in-vesting-contract"]';
const vegaInWalletSection = '[data-testid="vega-in-wallet"]';
const connectedVegaKey = '[data-testid="connected-vega-key"]';
@@ -78,12 +78,12 @@ context(
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
6
);
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
// 0005-ETXN-002
verifyEthWalletAssociatedBalance('2.0');
@@ -111,12 +111,12 @@ context(
stakingPageDisassociateTokens('2');
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
6
);
validateWalletCurrency('Associated', '2.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '0.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
cy.getByTestId('eth-wallet-associated-balances', txTimeout).should(
'not.exist'
);
@@ -192,12 +192,12 @@ context(
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
6
);
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
verifyEthWalletAssociatedBalance('2.0');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet).within(() => {
@@ -210,12 +210,12 @@ context(
});
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
6
);
validateWalletCurrency('Associated', '2.00');
validateWalletCurrency('Pending association', '1.00');
validateWalletCurrency('Total associated after pending', '1.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
verifyEthWalletAssociatedBalance('1.0');
verifyEthWalletTotalAssociatedBalance('1.0');
});
@@ -266,7 +266,7 @@ context(
// 1004-ASSO-008
// 1004-ASSO-010
// No warning visible as described in AC, but the button is disabled
cy.get(ethWalletAssociateButton).first().click();
cy.get(ethWalletAssociateButton).click();
cy.get(associateWalletRadioButton, { timeout: 30000 }).click();
cy.get(tokenSubmitButton, txTimeout).should('be.disabled'); // button disabled with no input
cy.get(tokenAmountInputBox, { timeout: 10000 }).type('6500000');
@@ -278,12 +278,12 @@ context(
vegaWalletAssociate('2');
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
6
);
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
validateWalletCurrency('Associated', '2.00');
});
@@ -294,24 +294,24 @@ context(
});
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
6
);
validateWalletCurrency('Associated', '2.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '0.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
validateWalletCurrency('Associated', '0.00');
});
it('Able to associate tokens to different public key of connected vega wallet', function () {
cy.get(ethWalletAssociateButton).first().click();
cy.get(ethWalletAssociateButton).click();
cy.get(associateWalletRadioButton).click();
cy.get(connectedVegaKey).should(
'have.text',
Cypress.env('vegaWalletPublicKey')
);
cy.get('[data-testid="manage-vega-wallet"]').click();
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
cy.get(connectedVegaKey).should(
'have.text',
@@ -15,11 +15,13 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it('should display announcement banner', function () {
it.skip('should display announcement banner', function () {
cy.getByTestId('app-announcement')
.should('be.visible')
.should('contain.text', 'TEST ANNOUNCEMENT!')
.within(() => {
cy.getByTestId('external-link').should('exist');
cy.getByTestId('external-link')
.should('have.attr', 'href', 'https://fairground.wtf')
.and('have.text', 'CLICK LINK');
});
cy.getByTestId('app-announcement-close').should('be.visible').click();
cy.getByTestId('app-announcement').should('not.exist');
@@ -333,6 +333,7 @@ context(
.parent()
.siblings()
.invoke('text')
.should('have.length.above', 3)
.then((el) => {
const value = parseFloat(el);
cy.wrap(value).should('be.gte', parseFloat(expectedAmount));
@@ -150,22 +150,11 @@ export function waitForProposalSync() {
});
}
export function getSortOrderOfSuppliedArray(suppliedArray: string[]) {
const tempArray = [];
for (let index = 1; index < suppliedArray.length; index++) {
tempArray.push(
suppliedArray[index - 1].localeCompare(suppliedArray[index])
);
}
if (tempArray.every((n) => n <= 0)) return 'ascending';
else if (tempArray.every((n) => n >= 0)) return 'descending';
else return 'unsorted';
}
export function goToMakeNewProposal(proposalType: string) {
navigateTo(navigation.proposals);
cy.get(newProposalButton).should('be.visible').click();
cy.url().should('include', '/proposals/propose');
cy.get(navigation.pageSpinner, { timeout: 20000 }).should('not.exist');
cy.get('li').should('contain.text', proposalType).and('be.visible');
cy.get('li').contains(proposalType).click();
}
@@ -8,6 +8,7 @@ import {
} from '@vegaprotocol/smart-contracts';
import { ethers, Wallet } from 'ethers';
const associatedAmountInWallet = '[data-testid="associated-amount"]:visible';
const vegaWalletContainer = 'aside [data-testid="vega-wallet"]';
const vegaWalletMnemonic = Cypress.env('vegaWalletMnemonic');
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey');
@@ -59,7 +60,7 @@ export async function faucetAsset(assetEthAddress: string) {
}
export async function vegaWalletTeardown() {
cy.get('[data-testid="associated-amount"]')
cy.get(associatedAmountInWallet)
.should('be.visible')
.invoke('text')
.then((associatedAmount) => {
@@ -68,12 +69,12 @@ export async function vegaWalletTeardown() {
$body.find('[data-testid="eth-wallet-associated-balances"]').length ||
associatedAmount != '0.00'
) {
vegaWalletTeardownVesting(vestingContract);
vegaWalletTeardownStaking(stakingBridgeContract);
vegaWalletTeardownVesting(vestingContract);
}
});
cy.get(vegaWalletContainer).within(() => {
cy.getByTestId('associated-amount', {
cy.get(associatedAmountInWallet, {
timeout: transactionTimeout,
}).contains('0.00', {
timeout: transactionTimeout,
@@ -90,7 +91,7 @@ export async function vegaWalletSetSpecifiedApprovalAmount(
await promiseWithTimeout(
token.approve(
ethStakingBridgeContractAddress,
resetAmount.concat('000000000000000000')
resetAmount + '0'.repeat(18)
),
10 * 60 * 1000,
'set approval amount'
@@ -104,13 +105,25 @@ async function vegaWalletTeardownStaking(stakingBridgeContract: StakingBridge) {
{ timeout: transactionTimeout, log: false }
).then((stakeBalance) => {
if (Number(stakeBalance) != 0) {
cy.wrap(
stakingBridgeContract.remove_stake(
String(stakeBalance),
vegaWalletPubKey
),
{ timeout: transactionTimeout, log: false }
);
cy.get(vegaWalletContainer).within(() => {
cy.getByTestId('currency-value')
.invoke('text')
.then(($associatedAmount) => {
cy.wrap(
stakingBridgeContract.remove_stake(
String(stakeBalance),
vegaWalletPubKey
),
{ timeout: transactionTimeout, log: false }
);
cy.get("[data-testid='currency-value']")
.first()
.invoke('text', {
timeout: transactionTimeout,
})
.should('not.eq', $associatedAmount);
});
});
}
});
}
@@ -122,9 +135,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 -1
View File
@@ -5,7 +5,7 @@ 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_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_VEGA_CONFIG_URL=''
NX_VEGA_URL=http://localhost:3028/query
@@ -1,15 +1,19 @@
import classNames from 'classnames';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { ReactNode } from 'react';
import { AnnouncementBanner } from '@vegaprotocol/announcements';
import { Nav } from '../nav';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import React from 'react';
interface AppLayoutProps {
children: ReactNode;
}
export const AppLayout = ({ children }: AppLayoutProps) => {
const { VEGA_ENV, ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
const { isReadOnly } = useVegaWallet();
const AppLayoutClasses = classNames(
'app w-full max-w-[1500px] mx-auto grid min-h-full',
'border-neutral-700 lg:border-l lg:border-r',
'app w-full max-w-[1500px] mx-auto grid',
'lg:text-body-large',
{
'grid-rows-[repeat(2,min-content)_1fr_min-content]': !isReadOnly,
@@ -17,5 +21,18 @@ export const AppLayout = ({ children }: AppLayoutProps) => {
}
);
return <div className={AppLayoutClasses}>{children}</div>;
return (
<div className="min-h-full">
<div className="lg:text-body-large">
{ANNOUNCEMENTS_CONFIG_URL && (
<AnnouncementBanner
app="governance"
configUrl={ANNOUNCEMENTS_CONFIG_URL}
/>
)}
<Nav theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
</div>
<div className={AppLayoutClasses}>{children}</div>
</div>
);
};
@@ -1,28 +1,16 @@
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
import { AnnouncementBanner } from '@vegaprotocol/announcements';
import { useVegaWallet } from '@vegaprotocol/wallet';
import React from 'react';
import { Nav } from '../nav';
export interface TemplateSidebarProps {
children: React.ReactNode;
sidebar: React.ReactNode[];
}
export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
const { VEGA_ENV, ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
return (
<>
{ANNOUNCEMENTS_CONFIG_URL && (
<AnnouncementBanner
app="governance"
configUrl={ANNOUNCEMENTS_CONFIG_URL}
/>
)}
<Nav theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
{isReadOnly ? (
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
) : null}
@@ -1,9 +1,8 @@
import type { ObservableQuery } from '@apollo/client';
import { useEffect } from 'react';
export const useRefreshAfterEpoch = (
epochExpiry: string | undefined,
refetch: ObservableQuery['refetch']
refetch: () => void
) => {
return useEffect(() => {
const epochInterval = setInterval(() => {
+18 -4
View File
@@ -443,8 +443,8 @@
"rewardType": "Reward type",
"rewardsAndFeesReceived": "Rewards and fees received",
"ThisDoesNotIncludeFeesReceivedForMakersOrLiquidityProviders": "This does not include fees received for makers or liquidity providers",
"totalDistributed": "TOTAL DISTRIBUTED",
"earnedByMe": "EARNED BY ME",
"totalDistributed": "Total distributed",
"earnedByMe": "Earned by me",
"noRewardsHaveBeenDistributedYet": "NO REWARDS HAVE BEEN DISTRIBUTED YET",
"rewardsColAssetHeader": "ASSET",
"rewardsColStakingHeader": "STAKING",
@@ -595,6 +595,7 @@
"noPercentage": "No percentage",
"proposalTerms": "Proposal terms",
"currentlySetTo": "Currently expected to ",
"currently": "currently",
"finalOutcomeMayDiffer": "Final outcome may differ",
"votingPower": "Voting power",
"normalisedVotingPower": "Normalised voting power",
@@ -767,7 +768,7 @@
"performancePenalty": "Performance penalty",
"overstaked": "Overstaked",
"overstakedPenalty": "Overstaked penalty",
"homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject.",
"homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject. Network upgrades are proposed and approved by validators.",
"homeProposalsButtonText": "Browse, vote, and propose",
"homeValidatorsIntro": "Vega runs on a delegated proof of stake blockchain, where validators earn fees for validating block transactions. Tokenholders can nominate validators by staking tokens to them.",
"homeValidatorsButtonText": "Browse, and stake",
@@ -775,5 +776,18 @@
"homeRewardsButtonText": "See rewards",
"homeVegaTokenIntro": "VEGA Token is a governance asset used to make and vote on proposals, and nominate validators.",
"homeVegaTokenButtonText": "Manage tokens",
"downloadProposalJson": "Download proposal as JSON"
"downloadProposalJson": "Download proposal as JSON",
"networkUpgrade": "Network Upgrade",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED": "Approved",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING": "Pending",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED": "Rejected",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED": "Unspecified",
"vegaRelease{release}": "Vega Release {{release}}",
"upgradeBlockHeight": "Upgrade block height",
"vegaReleaseTag": "Vega release tag",
"approvalStatus": "Approval status",
"approvers": "Approvers",
"approval (% validator voting power)": "approval (% validator voting power)",
"67% voting power required": "67% voting power required",
"Token": "Token"
}
@@ -0,0 +1,15 @@
export const calculateEpochOffset = ({
epochId,
page,
size,
}: {
epochId: number;
page: number;
size: number;
}) => {
// offset the epoch by the current page number times the page size while making sure it doesn't go below the minimum epoch value
return {
fromEpoch: Math.max(0, epochId - size * page) + 1,
toEpoch: epochId - size * page + size,
};
};
+88 -20
View File
@@ -10,23 +10,31 @@ import {
import { useDocumentTitle } from '../../hooks/use-document-title';
import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
import { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
import Routes from '../routes';
import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import { getNotRejectedProposals } from '../proposals/proposals/proposals-container';
import { useProtocolUpgradesQuery } from '../proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import {
getNotRejectedProposals,
getNotRejectedProtocolUpgradeProposals,
} 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 { ProtocolUpgradeProposalFieldsFragment } from '../proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals';
const nodesToShow = 6;
const HomeProposals = ({
proposals,
protocolUpgradeProposals,
}: {
proposals: ProposalFieldsFragment[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
}) => {
const { t } = useTranslation();
@@ -47,6 +55,10 @@ const HomeProposals = ({
data-testid="home-proposal-list"
className="grid md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-6"
>
{protocolUpgradeProposals.map((proposal, index) => (
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
))}
{proposals.map((proposal) => (
<ProposalsListItem key={proposal.id} proposal={proposal} />
))}
@@ -107,20 +119,7 @@ const HomeNodes = ({
{trimmedActiveNodes.map(({ id, avatarUrl, name }) => (
<div key={id} data-testid="validators" className="col-span-2">
<Link to={`${Routes.VALIDATORS}/${id}`}>
<RoundedWrapper paddingBottom={true} border={false}>
<div className="flex items-center justify-center m-[-1rem] p-4 bg-neutral-900 hover:bg-neutral-800">
{avatarUrl && (
<img
className="h-6 w-6 rounded-full mr-2"
src={avatarUrl}
alt={`Avatar icon for ${name}`}
/>
)}
<span className="text-sm">{name}</span>
</div>
</RoundedWrapper>
</Link>
<ValidatorDetailsLink id={id} avatarUrl={avatarUrl} name={name} />
</div>
))}
</div>
@@ -136,6 +135,35 @@ const HomeNodes = ({
);
};
interface ValidatorDetailsLinkProps {
id: string;
avatarUrl: string | null | undefined;
name: string;
}
export const ValidatorDetailsLink = ({
id,
avatarUrl,
name,
}: ValidatorDetailsLinkProps) => {
return (
<Link to={`${Routes.VALIDATORS}/${id}`}>
<RoundedWrapper paddingBottom={true} border={false}>
<div className="flex items-center justify-center m-[-1rem] p-3 bg-neutral-900 hover:bg-neutral-800">
{avatarUrl && (
<img
className="h-6 w-6 rounded-full mr-2"
src={avatarUrl}
alt={`Avatar icon for ${name}`}
/>
)}
<span className="text-sm">{name}</span>
</div>
</RoundedWrapper>
</Link>
);
};
const GovernanceHome = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
const { t } = useTranslation();
@@ -149,6 +177,16 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
errorPolicy: 'ignore',
});
const {
data: protocolUpgradesData,
loading: protocolUpgradesLoading,
error: protocolUpgradesError,
} = useProtocolUpgradesQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
});
const {
data: validatorsData,
error: validatorsError,
@@ -163,11 +201,38 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
proposalsData
? getNotRejectedProposals<ProposalFieldsFragment>(
proposalsData.proposalsConnection
).slice(0, 3)
)
: [],
[proposalsData]
);
const protocolUpgradeProposals = useMemo(
() =>
protocolUpgradesData
? getNotRejectedProtocolUpgradeProposals<ProtocolUpgradeProposalFieldsFragment>(
protocolUpgradesData.protocolUpgradeProposals
).filter(
(p) =>
Number(p.upgradeBlockHeight) >
Number(protocolUpgradesData.lastBlockHeight)
)
: [],
[protocolUpgradesData]
);
const totalProposalsDesired = 4;
const protocolUpgradeProposalsToShow = protocolUpgradeProposals.slice(
0,
totalProposalsDesired
);
const proposalsToShow =
protocolUpgradeProposalsToShow.length === totalProposalsDesired
? []
: proposals.slice(
0,
totalProposalsDesired - protocolUpgradeProposalsToShow.length
);
const activeNodes = removePaginationWrapper(
validatorsData?.nodesConnection.edges
);
@@ -182,11 +247,14 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
return (
<AsyncRenderer
loading={proposalsLoading || validatorsLoading}
error={proposalsError || validatorsError}
data={proposalsData && validatorsData}
loading={proposalsLoading || protocolUpgradesLoading || validatorsLoading}
error={proposalsError || protocolUpgradesError || validatorsError}
data={proposalsData && protocolUpgradesData && validatorsData}
>
<HomeProposals proposals={proposals} />
<HomeProposals
proposals={proposalsToShow}
protocolUpgradeProposals={protocolUpgradeProposalsToShow}
/>
<HomeNodes
activeNodes={activeNodes}
@@ -63,7 +63,7 @@ const renderComponent = (proposals: ProposalQuery['proposal'][]) => (
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalsList proposals={proposals} />
<ProposalsList proposals={proposals} protocolUpgradeProposals={[]} />
</VegaWalletContext.Provider>
</AppStateProvider>
</MockedProvider>
@@ -3,17 +3,21 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Heading, SubHeading } from '../../../../components/heading';
import { ProposalsListItem } from '../proposals-list-item';
import { ProtocolUpgradeProposalsListItem } from '../protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
import { ProposalsListFilter } from '../proposals-list-filter';
import Routes from '../../../routes';
import { Button } from '@vegaprotocol/ui-toolkit';
import { Link } from 'react-router-dom';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import { ExternalLinks } from '@vegaprotocol/utils';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
interface ProposalsListProps {
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
lastBlockHeight?: string;
}
interface SortedProposalsProps {
@@ -21,7 +25,16 @@ interface SortedProposalsProps {
closed: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
}
export const ProposalsList = ({ proposals }: ProposalsListProps) => {
interface SortedProtocolUpgradeProposalsProps {
open: ProtocolUpgradeProposalFieldsFragment[];
closed: ProtocolUpgradeProposalFieldsFragment[];
}
export const ProposalsList = ({
proposals,
protocolUpgradeProposals,
lastBlockHeight,
}: ProposalsListProps) => {
const { t } = useTranslation();
const [filterString, setFilterString] = useState('');
const sortedProposals = proposals.reduce(
@@ -39,6 +52,21 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
}
);
const sortedProtocolUpgradeProposals = protocolUpgradeProposals.reduce(
(acc: SortedProtocolUpgradeProposalsProps, proposal) => {
if (Number(proposal?.upgradeBlockHeight) > Number(lastBlockHeight)) {
acc.open.push(proposal);
} else {
acc.closed.push(proposal);
}
return acc;
},
{
open: [],
closed: [],
}
);
const filterPredicate = (
p: ProposalFieldsFragment | ProposalQuery['proposal']
) =>
@@ -65,7 +93,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
</div>
<p className="mb-8">
{t(
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network.`
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.`
)}{' '}
<ExternalLink
data-testid="proposal-documentation-link"
@@ -80,8 +108,15 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
)}
<section className="-mx-4 p-4 mb-8 bg-neutral-800">
<SubHeading title={t('openProposals')} />
{sortedProposals.open.length > 0 ? (
{sortedProposals.open.length > 0 ||
sortedProtocolUpgradeProposals.open.length > 0 ? (
<ul data-testid="open-proposals">
{sortedProtocolUpgradeProposals.open.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
<ProposalsListItem key={proposal?.id} proposal={proposal} />
))}
@@ -94,8 +129,16 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
</section>
<section>
<SubHeading title={t('closedProposals')} />
{sortedProposals.closed.length > 0 ? (
{sortedProposals.closed.length > 0 ||
sortedProtocolUpgradeProposals.closed.length > 0 ? (
<ul data-testid="closed-proposals">
{sortedProtocolUpgradeProposals.closed.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
{sortedProposals.closed.filter(filterPredicate).map((proposal) => (
<ProposalsListItem key={proposal?.id} proposal={proposal} />
))}
@@ -0,0 +1 @@
export * from './protocol-update-proposal-detail-approvals';
@@ -0,0 +1,84 @@
import { useTranslation } from 'react-i18next';
import {
RoundedWrapper,
KeyValueTable,
KeyValueTableRow,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { ValidatorDetailsLink } from '../../../home';
import { getNormalisedVotingPower } from '../../../staking/shared';
import type { NodesFragmentFragment } from '../../../staking/home/__generated__/Nodes';
export interface ProtocolUpdateProposalDetailApprovalsProps {
totalConsensusValidators: number;
// Consensus validators that have approved the proposal
consensusApprovals: NodesFragmentFragment[];
consensusApprovalsVotingPowerPercentage: string;
}
export const ProtocolUpdateProposalDetailApprovals = ({
totalConsensusValidators,
consensusApprovals,
consensusApprovalsVotingPowerPercentage,
}: ProtocolUpdateProposalDetailApprovalsProps) => {
const { t } = useTranslation();
return (
<>
<div className="mb-10">
<SubHeading title={t('approvalStatus')} />
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="protocol-upgrade-approval-status">
<KeyValueTableRow noBorder={true}>
<span>{`${consensusApprovalsVotingPowerPercentage} ${t(
'approval (% validator voting power)'
)}`}</span>
<span>({t('67% voting power required')})</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
</div>
<div className="mb-10">
<SubHeading
title={`${t('approvers')} (${
consensusApprovals.length
}/${totalConsensusValidators} validators)`}
/>
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="protocol-upgrade-approvers">
<KeyValueTableRow>
<div className="mb-2">{t('validator')}</div>
<div className="text-white mb-2">{t('votingPower')}</div>
</KeyValueTableRow>
{consensusApprovals.map((validator, index) => (
<KeyValueTableRow
noBorder={index === consensusApprovals?.length - 1}
key={validator.pubkey}
>
<div
className="-mb-3 mt-1"
data-testid={`validator-${validator.id}`}
>
<ValidatorDetailsLink
id={validator.id}
avatarUrl={validator.avatarUrl}
name={validator.name}
/>
</div>
<span>
{getNormalisedVotingPower(
validator.rankingScore.votingPower,
2
)}
</span>
</KeyValueTableRow>
))}
</KeyValueTable>
</RoundedWrapper>
</div>
</>
);
};
@@ -0,0 +1,86 @@
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import * as Schema from '@vegaprotocol/types';
import { ProtocolUpdateProposalDetailApprovals } from './protocol-update-proposal-detail-approvals';
import { getNormalisedVotingPower } from '../../../staking/shared';
import type { ProtocolUpdateProposalDetailApprovalsProps } from './protocol-update-proposal-detail-approvals';
import type { NodesFragmentFragment } from '../../../staking/home/__generated__/Nodes';
describe('ProtocolUpdateProposalDetailApprovals', () => {
const mockValidators: NodesFragmentFragment[] = [
{
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
name: 'Marvin',
avatarUrl:
'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
pubkey:
'6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b2917',
stakedTotal: '14182454495731682635157',
stakedByOperator: '1000000000000000000000',
stakedByDelegates: '13182454495731682635157',
pendingStake: '0',
rankingScore: {
rankingScore: '0.67845061012234727427532760837568',
stakeScore: '0.3392701644525644',
performanceScore: '0.9998677767864936',
votingPower: '3500',
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
__typename: 'RankingScore',
},
__typename: 'Node',
},
];
const defaultProps = {
totalConsensusValidators: 3,
consensusApprovals: mockValidators,
consensusApprovalsVotingPowerPercentage: '75%',
};
const renderComponent = (
props: Partial<ProtocolUpdateProposalDetailApprovalsProps> = {}
) => {
const mergedProps = {
...defaultProps,
...props,
};
return render(
<BrowserRouter>
<ProtocolUpdateProposalDetailApprovals {...mergedProps} />
</BrowserRouter>
);
};
it('renders without crashing', () => {
renderComponent();
expect(
screen.getByTestId('protocol-upgrade-approval-status')
).toBeInTheDocument();
expect(
screen.getByTestId('protocol-upgrade-approvers')
).toBeInTheDocument();
});
it('renders the correct number of consensus approvers', () => {
renderComponent();
const validators = screen.getAllByTestId(/validator-/i);
expect(validators.length).toBe(mockValidators.length);
});
it('renders the correct approval status with the required percentage', () => {
renderComponent();
expect(screen.getByText(/75% approval/i)).toBeInTheDocument();
expect(screen.getByText(/67% voting power required/i)).toBeInTheDocument();
});
it('renders the correct voting power for each validator', () => {
renderComponent();
mockValidators.forEach((validator) => {
const normalisedVotingPower = getNormalisedVotingPower(
validator.rankingScore.votingPower,
2
);
expect(screen.getByText(normalisedVotingPower)).toBeInTheDocument();
});
});
});
@@ -0,0 +1 @@
export * from './protocol-upgrade-proposal-detail-header';
@@ -0,0 +1,18 @@
import { render } from '@testing-library/react';
import { ProtocolUpgradeProposalDetailHeader } from './protocol-upgrade-proposal-detail-header';
describe('ProtocolUpgradeProposalDetailHeader', () => {
it('should render successfully', () => {
const { baseElement } = render(
<ProtocolUpgradeProposalDetailHeader releaseTag="v1.0.0" />
);
expect(baseElement).toBeTruthy();
});
it('should render the release tag', () => {
const { getByText } = render(
<ProtocolUpgradeProposalDetailHeader releaseTag="v1.0.0" />
);
expect(getByText('Vega Release v1.0.0')).toBeTruthy();
});
});
@@ -0,0 +1,21 @@
import { useTranslation } from 'react-i18next';
import { Intent, Lozenge } from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../../../components/heading';
export interface ProtocolUpgradeProposalDetailHeaderProps {
releaseTag: string;
}
export const ProtocolUpgradeProposalDetailHeader = ({
releaseTag,
}: ProtocolUpgradeProposalDetailHeaderProps) => {
const { t } = useTranslation();
return (
<>
<Heading title={t('vegaRelease{release}', { release: releaseTag })} />
<div className="mb-10">
<Lozenge variant={Intent.Success}>{t('networkUpgrade')}</Lozenge>
</div>
</>
);
};
@@ -0,0 +1 @@
export * from './protocol-upgrade-proposal-detail-info';
@@ -0,0 +1,44 @@
// jest tests for the protocol upgrade proposal detail info component
import { render } from '@testing-library/react';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import { ProtocolUpgradeProposalDetailInfo } from './protocol-upgrade-proposal-detail-info';
const renderComponent = () =>
render(
<ProtocolUpgradeProposalDetailInfo
proposal={{
vegaReleaseTag: 'v0.1.234',
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
upgradeBlockHeight: '12345',
approvers: [],
}}
/>
);
describe('ProtocolUpgradeProposalDetailInfo', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
it('should render the upgrade block height', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('protocol-upgrade-block-height')).toHaveTextContent(
'12345'
);
});
it('should render the state', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('protocol-upgrade-state')).toHaveTextContent('Pending');
});
it('should render the vega release tag', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('protocol-upgrade-release-tag')).toHaveTextContent(
'v0.1.234'
);
});
});
@@ -0,0 +1,58 @@
import { useTranslation } from 'react-i18next';
import {
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
export interface ProtocolUpgradeProposalDetailInfoProps {
proposal: ProtocolUpgradeProposalFieldsFragment;
lastBlockHeight?: string;
}
export const ProtocolUpgradeProposalDetailInfo = ({
proposal,
lastBlockHeight,
}: ProtocolUpgradeProposalDetailInfoProps) => {
const { t } = useTranslation();
return (
<div className="mb-10">
<SubHeading title={t('proposal')} />
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="protocol-upgrade-proposal-details">
<KeyValueTableRow>
<span className="uppercase">{t('upgradeBlockHeight')}</span>
<span data-testid="protocol-upgrade-block-height">
{proposal.upgradeBlockHeight}{' '}
{lastBlockHeight && (
<>
({t('currently')} {lastBlockHeight})
</>
)}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span className="uppercase">{t('state')}</span>
<span data-testid="protocol-upgrade-state">
{t(`${proposal.status}`)}
</span>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span className="uppercase">{t('vegaReleaseTag')}</span>
<span data-testid="protocol-upgrade-release-tag">
{proposal.vegaReleaseTag}
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
</div>
);
};
@@ -0,0 +1,69 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { ProtocolUpgradeProposalsListItem } from './protocol-upgrade-proposals-list-item';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
const proposal = {
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
vegaReleaseTag: 'v1.0.0',
upgradeBlockHeight: '12345',
} as ProtocolUpgradeProposalFieldsFragment;
const renderComponent = (proposal: ProtocolUpgradeProposalFieldsFragment) =>
render(
<BrowserRouter>
<ProtocolUpgradeProposalsListItem proposal={proposal} />
</BrowserRouter>
);
describe('ProtocolUpgradeProposalsListItem', () => {
it('renders the correct status icon for each proposal status', () => {
const statuses = [
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED,
icon: 'protocol-upgrade-proposal-status-icon-rejected',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
icon: 'protocol-upgrade-proposal-status-icon-pending',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED,
icon: 'protocol-upgrade-proposal-status-icon-approved',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED,
icon: 'protocol-upgrade-proposal-status-icon-unspecified',
},
];
statuses.forEach(({ status, icon }) => {
renderComponent({ ...proposal, status });
const statusIcon = screen.getByTestId(icon);
expect(statusIcon).toBeInTheDocument();
});
});
it('renders the correct Vega release tag', () => {
renderComponent(proposal);
const releaseTag = screen.getByTestId(
'protocol-upgrade-proposal-release-tag'
);
expect(releaseTag).toHaveTextContent(proposal.vegaReleaseTag);
});
it('renders the correct upgrade block height', () => {
renderComponent(proposal);
const blockHeight = screen.getByTestId(
'protocol-upgrade-proposal-block-height'
);
expect(blockHeight).toHaveTextContent(proposal.upgradeBlockHeight);
});
});
@@ -0,0 +1,124 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
Button,
Icon,
Intent,
Lozenge,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { stripFullStops } from '@vegaprotocol/utils';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import { SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react';
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import Routes from '../../../routes';
interface ProtocolProposalsListItemProps {
proposal: ProtocolUpgradeProposalFieldsFragment;
}
export const ProtocolUpgradeProposalsListItem = ({
proposal,
}: ProtocolProposalsListItemProps) => {
const { t } = useTranslation();
if (!proposal || !proposal.upgradeBlockHeight) return null;
let proposalStatusIcon: ReactNode;
switch (proposal.status) {
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED:
proposalStatusIcon = (
<div data-testid="protocol-upgrade-proposal-status-icon-rejected">
<Icon name={'cross'} />
</div>
);
break;
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING:
proposalStatusIcon = (
<div data-testid="protocol-upgrade-proposal-status-icon-pending">
<Icon name={'time'} />
</div>
);
break;
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED:
proposalStatusIcon = (
<div data-testid="protocol-upgrade-proposal-status-icon-approved">
<Icon name={'tick'} />
</div>
);
break;
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED:
proposalStatusIcon = (
<div data-testid="protocol-upgrade-proposal-status-icon-unspecified">
<Icon name={'disable'} />
</div>
);
break;
}
return (
<li
id={proposal.upgradeBlockHeight}
data-testid="protocol-upgrade-proposals-list-item"
>
<RoundedWrapper paddingBottom={true} heightFull={true}>
<div
data-testid="protocol-upgrade-proposal-title"
className="text-sm mb-2"
>
<SubHeading title={`Vega release ${proposal.vegaReleaseTag}`} />
</div>
<div className="text-sm">
<div
data-testid="protocol-upgrade-proposal-type"
className="flex items-center gap-2 mb-4"
>
<Lozenge variant={Intent.Success}>{t('networkUpgrade')}</Lozenge>
</div>
<div
data-testid="protocol-upgrade-proposal-release-tag"
className="mb-2"
>
<span className="pr-2">{t('vegaReleaseTag')}</span>
<Lozenge>{proposal.vegaReleaseTag}</Lozenge>
</div>
<div
data-testid="protocol-upgrade-proposal-block-height"
className="mb-2"
>
<span className="pr-2">{t('upgradeBlockHeight')}</span>
<Lozenge>{proposal.upgradeBlockHeight}</Lozenge>
</div>
<div className="grid grid-cols-[1fr_auto] mt-3 items-start gap-2">
<div className="col-start-1 row-start-1 text-white">
<div
data-testid="protocol-upgrade-proposal-status"
className="flex items-center gap-2"
>
<span>{t(`${proposal.status}`)}</span>
<span>{proposalStatusIcon}</span>
</div>
</div>
<div className="col-start-2 row-start-2 justify-self-end">
<Link
to={`${Routes.PROPOSALS}/protocol-upgrade/${stripFullStops(
proposal.vegaReleaseTag
)}`}
>
<Button data-testid="view-proposal-btn" size="sm">
{t('View')}
</Button>
</Link>
</div>
</div>
</div>
</RoundedWrapper>
</li>
);
};
@@ -5,13 +5,18 @@ import { useTranslation } from 'react-i18next';
import { SplashLoader } from '../../../components/splash-loader';
import { ProposalsList } from '../components/proposals-list';
import { useProposalsQuery } from './__generated__/Proposals';
import type { ProposalFieldsFragment } from './__generated__/Proposals';
import type { NodeConnection, NodeEdge } from '@vegaprotocol/utils';
import { getNodes } from '@vegaprotocol/utils';
import flow from 'lodash/flow';
import { ProposalState } from '@vegaprotocol/types';
import {
ProposalState,
ProtocolUpgradeProposalStatus,
} from '@vegaprotocol/types';
import type { NodeConnection, NodeEdge } from '@vegaprotocol/utils';
import type { ProposalFieldsFragment } from './__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import orderBy from 'lodash/orderBy';
import { useProtocolUpgradesQuery } from '../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
const orderByDate = (arr: ProposalFieldsFragment[]) =>
orderBy(
@@ -20,6 +25,15 @@ const orderByDate = (arr: ProposalFieldsFragment[]) =>
['desc', 'desc']
);
const orderByUpgradeBlockHeight = (
arr: ProtocolUpgradeProposalFieldsFragment[]
) =>
orderBy(
arr,
[(p) => p?.upgradeBlockHeight, (p) => p.vegaReleaseTag],
['desc', 'desc']
);
export function getNotRejectedProposals<T extends ProposalFieldsFragment>(
data?: NodeConnection<NodeEdge<T>> | null
): T[] {
@@ -32,6 +46,21 @@ export function getNotRejectedProposals<T extends ProposalFieldsFragment>(
])(data);
}
export function getNotRejectedProtocolUpgradeProposals<
T extends ProtocolUpgradeProposalFieldsFragment
>(data?: NodeConnection<NodeEdge<T>> | null): T[] {
return flow([
(data) =>
getNodes<ProtocolUpgradeProposalFieldsFragment>(data, (p) =>
p
? p.status !==
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED
: false
),
orderByUpgradeBlockHeight,
])(data);
}
export const ProposalsContainer = () => {
const { t } = useTranslation();
const { data, loading, error } = useProposalsQuery({
@@ -40,6 +69,16 @@ export const ProposalsContainer = () => {
errorPolicy: 'ignore',
});
const {
data: protocolUpgradesData,
loading: protocolUpgradesLoading,
error: protocolUpgradesError,
} = useProtocolUpgradesQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
});
const proposals = useMemo(
() =>
getNotRejectedProposals<ProposalFieldsFragment>(
@@ -48,15 +87,25 @@ export const ProposalsContainer = () => {
[data]
);
if (error) {
const protocolUpgradeProposals = useMemo(
() =>
protocolUpgradesData
? getNotRejectedProtocolUpgradeProposals<ProtocolUpgradeProposalFieldsFragment>(
protocolUpgradesData.protocolUpgradeProposals
)
: [],
[protocolUpgradesData]
);
if (error || protocolUpgradesError) {
return (
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
<pre>{error.message}</pre>
<pre>{error?.message || protocolUpgradesError?.message}</pre>
</Callout>
);
}
if (loading) {
if (loading || protocolUpgradesLoading) {
return (
<Splash>
<SplashLoader />
@@ -64,5 +113,11 @@ export const ProposalsContainer = () => {
);
}
return <ProposalsList proposals={proposals} />;
return (
<ProposalsList
proposals={proposals}
protocolUpgradeProposals={protocolUpgradeProposals}
lastBlockHeight={protocolUpgradesData?.lastBlockHeight}
/>
);
};
@@ -0,0 +1,17 @@
fragment ProtocolUpgradeProposalFields on ProtocolUpgradeProposal {
upgradeBlockHeight
vegaReleaseTag
approvers
status
}
query ProtocolUpgrades {
lastBlockHeight
protocolUpgradeProposals {
edges {
node {
...ProtocolUpgradeProposalFields
}
}
}
}
@@ -0,0 +1,59 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ProtocolUpgradeProposalFieldsFragment = { __typename?: 'ProtocolUpgradeProposal', upgradeBlockHeight: string, vegaReleaseTag: string, approvers: Array<string>, status: Types.ProtocolUpgradeProposalStatus };
export type ProtocolUpgradesQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ProtocolUpgradesQuery = { __typename?: 'Query', lastBlockHeight: string, protocolUpgradeProposals?: { __typename?: 'ProtocolUpgradeProposalConnection', edges?: Array<{ __typename?: 'ProtocolUpgradeProposalEdge', node: { __typename?: 'ProtocolUpgradeProposal', upgradeBlockHeight: string, vegaReleaseTag: string, approvers: Array<string>, status: Types.ProtocolUpgradeProposalStatus } }> | null } | null };
export const ProtocolUpgradeProposalFieldsFragmentDoc = gql`
fragment ProtocolUpgradeProposalFields on ProtocolUpgradeProposal {
upgradeBlockHeight
vegaReleaseTag
approvers
status
}
`;
export const ProtocolUpgradesDocument = gql`
query ProtocolUpgrades {
lastBlockHeight
protocolUpgradeProposals {
edges {
node {
...ProtocolUpgradeProposalFields
}
}
}
}
${ProtocolUpgradeProposalFieldsFragmentDoc}`;
/**
* __useProtocolUpgradesQuery__
*
* To run a query within a React component, call `useProtocolUpgradesQuery` and pass it any options that fit your needs.
* When your component renders, `useProtocolUpgradesQuery` 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 } = useProtocolUpgradesQuery({
* variables: {
* },
* });
*/
export function useProtocolUpgradesQuery(baseOptions?: Apollo.QueryHookOptions<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>(ProtocolUpgradesDocument, options);
}
export function useProtocolUpgradesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>(ProtocolUpgradesDocument, options);
}
export type ProtocolUpgradesQueryHookResult = ReturnType<typeof useProtocolUpgradesQuery>;
export type ProtocolUpgradesLazyQueryHookResult = ReturnType<typeof useProtocolUpgradesLazyQuery>;
export type ProtocolUpgradesQueryResult = Apollo.QueryResult<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>;
@@ -0,0 +1 @@
export { ProtocolUpgradeProposalContainer as default } from './protocol-upgrade-proposal-container';
@@ -0,0 +1,78 @@
import { useEffect, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { removePaginationWrapper, stripFullStops } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import { ProtocolUpgradeProposal } from './protocol-upgrade-proposal';
import { ProposalNotFound } from '../components/proposal-not-found';
import { useProtocolUpgradesQuery } from './__generated__/ProtocolUpgradeProposals';
import { useNodesQuery } from '../../staking/home/__generated__/Nodes';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
export const ProtocolUpgradeProposalContainer = () => {
const params = useParams<{ proposalReleaseTag: string }>();
const { data, loading, error, refetch } = useProtocolUpgradesQuery({
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
skip: !params.proposalReleaseTag,
});
useEffect(() => {
const interval = setInterval(refetch, 1000);
return () => clearInterval(interval);
}, [refetch]);
const {
data: nodesData,
error: nodesError,
loading: nodesLoading,
refetch: nodesRefetch,
} = useNodesQuery();
useRefreshAfterEpoch(nodesData?.epoch.timestamps.expiry, nodesRefetch);
const protocolUpgradeProposal = useMemo(() => {
if (!data?.protocolUpgradeProposals) return null;
const proposals = removePaginationWrapper(
data.protocolUpgradeProposals.edges
);
return proposals.find(
({ vegaReleaseTag }) =>
params.proposalReleaseTag &&
params.proposalReleaseTag === stripFullStops(vegaReleaseTag)
);
}, [data, params.proposalReleaseTag]);
const consensusValidators = useMemo(() => {
if (!nodesData?.nodesConnection.edges) return null;
const nodes = removePaginationWrapper(nodesData.nodesConnection.edges);
return nodes.filter(
({ rankingScore: { status } }) =>
status === Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT
);
}, [nodesData]);
return (
<AsyncRenderer
loading={loading || nodesLoading}
error={error || nodesError}
data={{ ...data, ...nodesData }}
>
{protocolUpgradeProposal ? (
<ProtocolUpgradeProposal
proposal={protocolUpgradeProposal}
lastBlockHeight={data?.lastBlockHeight}
consensusValidators={consensusValidators}
/>
) : (
<ProposalNotFound />
)}
</AsyncRenderer>
);
};
@@ -0,0 +1,134 @@
import { render } from '@testing-library/react';
import * as Schema from '@vegaprotocol/types';
import {
ProtocolUpgradeProposal,
getConsensusApprovals,
getConsensusApprovalsVotingPower,
getConsensusApprovalsVotingPowerPercentage,
} from './protocol-upgrade-proposal';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import { getNormalisedVotingPower } from '../../staking/shared';
import type { NodesFragmentFragment } from '../../staking/home/__generated__/Nodes';
import type { ProtocolUpgradeProposalFieldsFragment } from './__generated__/ProtocolUpgradeProposals';
const mockProposal: ProtocolUpgradeProposalFieldsFragment = {
vegaReleaseTag: 'v0.1.234',
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
upgradeBlockHeight: '12345',
approvers: [],
};
const mockConsensusValidators: NodesFragmentFragment[] = [
{
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
name: 'Marvin',
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Marvin-TV-3.jpg',
pubkey: '6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b2917',
stakedTotal: '14182454495731682635157',
stakedByOperator: '1000000000000000000000',
stakedByDelegates: '13182454495731682635157',
pendingStake: '0',
rankingScore: {
rankingScore: '0.67845061012234727427532760837568',
stakeScore: '0.3392701644525644',
performanceScore: '0.9998677767864936',
votingPower: '3500',
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
__typename: 'RankingScore',
},
__typename: 'Node',
},
{
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b5',
name: 'Barvin',
avatarUrl: 'https://upload.wikimedia.org/wikipedia/en/2/25/Barvin-TV-3.jpg',
pubkey: '6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b291b',
stakedTotal: '1418245449573168263',
stakedByOperator: '100000000000000000',
stakedByDelegates: '1318245449573168263',
pendingStake: '0',
rankingScore: {
rankingScore: '0.5',
stakeScore: '0.3',
performanceScore: '0.98',
votingPower: '2500',
status: Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT,
__typename: 'RankingScore',
},
__typename: 'Node',
},
];
const renderComponent = () =>
render(
<ProtocolUpgradeProposal proposal={mockProposal} consensusValidators={[]} />
);
describe('ProtocolUpgradeProposal', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
});
describe('getConsensusApprovals', () => {
it('returns an empty array when there are no matching approvers', () => {
const result = getConsensusApprovals(mockConsensusValidators, {
...mockProposal,
approvers: [],
});
expect(result).toEqual([]);
});
it('returns the correct consensus approvals when there are matching approvers', () => {
const expectedApprovals = mockConsensusValidators[1];
const result = getConsensusApprovals(mockConsensusValidators, {
...mockProposal,
approvers: [
'6abc23391a9f888ab240415bf63d6844b03fc360be822f4a1d2cd832d87b291b',
],
});
expect(result.length).toEqual(1);
expect(result[0].id).toEqual(expectedApprovals.id);
});
});
describe('getConsensusApprovalsVotingPower', () => {
it('returns 0 when the input array is empty', () => {
const result = getConsensusApprovalsVotingPower([]);
expect(result).toBe(0);
});
it('returns the correct sum of voting power for a given array of consensus validators', () => {
const expectedSum = mockConsensusValidators.reduce(
(acc, curr) => acc + Number(curr.rankingScore.votingPower),
0
);
const result = getConsensusApprovalsVotingPower(mockConsensusValidators);
expect(result).toBe(expectedSum);
});
});
describe('getConsensusApprovalsVotingPowerPercentage', () => {
it('returns "0%" when the input value is 0 or less', () => {
const resultZero = getConsensusApprovalsVotingPowerPercentage(0);
const resultNegative = getConsensusApprovalsVotingPowerPercentage(-10);
expect(resultZero).toBe('0%');
expect(resultNegative).toBe('0%');
});
it('returns the correct percentage string when the input value is greater than 0', () => {
const inputValue = 12345;
const expectedPercentage = getNormalisedVotingPower(
inputValue.toString(),
2
);
const result = getConsensusApprovalsVotingPowerPercentage(inputValue);
expect(result).toBe(expectedPercentage);
});
});
@@ -0,0 +1,83 @@
import { useMemo } from 'react';
import { ProtocolUpgradeProposalDetailHeader } from '../components/protocol-upgrade-proposal-detail-header';
import { ProtocolUpdateProposalDetailApprovals } from '../components/protocol-upgrade-proposal-detail-approvals';
import { ProtocolUpgradeProposalDetailInfo } from '../components/protocol-upgrade-proposal-detail-info';
import { getNormalisedVotingPower } from '../../staking/shared';
import type { ProtocolUpgradeProposalFieldsFragment } from './__generated__/ProtocolUpgradeProposals';
import type { NodesFragmentFragment } from '../../staking/home/__generated__/Nodes';
export interface ProtocolUpgradeProposalProps {
proposal: ProtocolUpgradeProposalFieldsFragment;
consensusValidators: NodesFragmentFragment[] | null;
lastBlockHeight?: string;
}
export const getConsensusApprovals = (
consensusValidators: NodesFragmentFragment[],
proposal: ProtocolUpgradeProposalFieldsFragment
) =>
consensusValidators?.filter(({ pubkey }) =>
proposal.approvers?.includes(pubkey)
);
export const getConsensusApprovalsVotingPower = (
consensusValidators: NodesFragmentFragment[]
) =>
consensusValidators?.reduce<number>(
(acc: number, curr: NodesFragmentFragment) => {
return acc + Number(curr.rankingScore.votingPower);
},
0
);
export const getConsensusApprovalsVotingPowerPercentage = (
consensusApprovalsVotingPower: number
) =>
consensusApprovalsVotingPower > 0
? getNormalisedVotingPower(consensusApprovalsVotingPower.toString(), 2)
: '0%';
export const ProtocolUpgradeProposal = ({
proposal,
lastBlockHeight,
consensusValidators,
}: ProtocolUpgradeProposalProps) => {
const consensusApprovals = useMemo(
() => getConsensusApprovals(consensusValidators || [], proposal),
[consensusValidators, proposal]
);
const consensusApprovalsVotingPower = useMemo(
() => getConsensusApprovalsVotingPower(consensusApprovals || []),
[consensusApprovals]
);
const consensusApprovalsVotingPowerPercentage = useMemo(
() =>
getConsensusApprovalsVotingPowerPercentage(consensusApprovalsVotingPower),
[consensusApprovalsVotingPower]
);
return (
<section data-testid="protocol-upgrade-proposal">
<ProtocolUpgradeProposalDetailHeader
releaseTag={proposal.vegaReleaseTag}
/>
<ProtocolUpgradeProposalDetailInfo
proposal={proposal}
lastBlockHeight={lastBlockHeight}
/>
{consensusValidators && consensusApprovals && (
<ProtocolUpdateProposalDetailApprovals
consensusApprovals={consensusApprovals}
consensusApprovalsVotingPowerPercentage={
consensusApprovalsVotingPowerPercentage
}
totalConsensusValidators={consensusValidators.length}
/>
)}
</section>
);
};
@@ -3,7 +3,7 @@ import { AppStateProvider } from '../../../contexts/app-state/app-state-provider
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
const mockData = {
epoch: '4441',
epoch: 4441,
rewards: [
{
asset: 'tDAI',
@@ -1,32 +1,43 @@
import { useMemo } from 'react';
import { useMemo, useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { AsyncRenderer, Pagination } from '@vegaprotocol/ui-toolkit';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { EpochFieldsFragment } from '../home/__generated__/Rewards';
import { useRewardsQuery } from '../home/__generated__/Rewards';
import { ENV } from '../../../config';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
export const EpochIndividualRewards = () => {
const EPOCHS_PAGE_SIZE = 10;
type EpochTotalRewardsProps = {
currentEpoch: EpochFieldsFragment;
};
export const EpochIndividualRewards = ({
currentEpoch,
}: EpochTotalRewardsProps) => {
// we start from the previous epoch when displaying rewards data, because the current one has no calculated data while ongoing
const epochId = Number(currentEpoch.id) - 1;
const totalPages = Math.ceil(epochId / EPOCHS_PAGE_SIZE);
const [page, setPage] = useState(1);
const { t } = useTranslation();
const { pubKey } = useVegaWallet();
const { delegationsPagination } = ENV;
const { data, loading, error } = useRewardsQuery({
const { data, loading, error, refetch } = useRewardsQuery({
notifyOnNetworkStatusChange: true,
variables: {
partyId: pubKey || '',
fromEpoch: epochId - EPOCHS_PAGE_SIZE,
toEpoch: epochId,
delegationsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
// we can use the same value for rewardsPagination as delegationsPagination
rewardsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
},
skip: !pubKey,
});
@@ -39,8 +50,37 @@ export const EpochIndividualRewards = () => {
const epochIndividualRewardSummaries = useMemo(() => {
if (!data?.party) return [];
return generateEpochIndividualRewardsList(rewards);
}, [data?.party, rewards]);
return generateEpochIndividualRewardsList({
rewards,
epochId,
page,
size: EPOCHS_PAGE_SIZE,
});
}, [data?.party, epochId, page, rewards]);
const refetchData = useCallback(
async (toPage?: number) => {
const targetPage = toPage ?? page;
await refetch({
partyId: pubKey || '',
...calculateEpochOffset({ epochId, page, size: EPOCHS_PAGE_SIZE }),
delegationsPagination: delegationsPagination
? {
first: Number(delegationsPagination),
}
: undefined,
});
setPage(targetPage);
},
[epochId, page, refetch, delegationsPagination, pubKey]
);
useEffect(() => {
// when the epoch changes, we want to refetch the data to update the current page
if (data) {
refetchData();
}
}, [epochId, data, refetchData]);
return (
<AsyncRenderer
@@ -53,17 +93,24 @@ export const EpochIndividualRewards = () => {
{t('Connected Vega key')}:{' '}
<span className="text-white">{pubKey}</span>
</p>
{epochIndividualRewardSummaries.length ? (
epochIndividualRewardSummaries.map(
(epochIndividualRewardSummary) => (
<EpochIndividualRewardsTable
data={epochIndividualRewardSummary}
/>
)
{epochIndividualRewardSummaries.map(
(epochIndividualRewardSummary) => (
<EpochIndividualRewardsTable
data={epochIndividualRewardSummary}
/>
)
) : (
<p>{t('noRewards')}</p>
)}
<Pagination
isLoading={loading}
hasPrevPage={page > 1}
hasNextPage={page < totalPages}
onBack={() => refetchData(page - 1)}
onNext={() => refetchData(page + 1)}
onFirst={() => refetchData(1)}
onLast={() => refetchData(totalPages)}
>
{t('Page')} {page}
</Pagination>
</div>
)}
/>
@@ -43,6 +43,16 @@ describe('generateEpochIndividualRewardsList', () => {
epoch: { id: '1' },
};
const reward5: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '150',
percentageOfTotal: '0.15',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD' },
party: { id: 'blah' },
epoch: { id: '3' },
};
const rewardWrongType: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '50',
@@ -54,20 +64,38 @@ describe('generateEpochIndividualRewardsList', () => {
};
it('should return an empty array if no rewards are provided', () => {
expect(generateEpochIndividualRewardsList([])).toEqual([]);
expect(
generateEpochIndividualRewardsList({ rewards: [], epochId: 1 })
).toEqual([
{
epoch: 1,
rewards: [],
},
]);
});
it('should filter out any rewards of the wrong type', () => {
const result = generateEpochIndividualRewardsList([rewardWrongType]);
const result = generateEpochIndividualRewardsList({
rewards: [rewardWrongType],
epochId: 1,
});
expect(result).toEqual([]);
expect(result).toEqual([
{
epoch: 1,
rewards: [],
},
]);
});
it('should return reward in the correct format', () => {
const result = generateEpochIndividualRewardsList([reward1]);
const result = generateEpochIndividualRewardsList({
rewards: [reward1],
epochId: 1,
});
expect(result[0]).toEqual({
epoch: '1',
epoch: 1,
rewards: [
{
asset: 'USD',
@@ -105,21 +133,24 @@ describe('generateEpochIndividualRewardsList', () => {
it('should return an array sorted by epoch descending', () => {
const rewards = [reward1, reward2, reward3, reward4];
const result1 = generateEpochIndividualRewardsList(rewards);
const result1 = generateEpochIndividualRewardsList({ rewards, epochId: 2 });
expect(result1[0].epoch).toEqual('2');
expect(result1[1].epoch).toEqual('1');
expect(result1[0].epoch).toEqual(2);
expect(result1[1].epoch).toEqual(1);
const reorderedRewards = [reward4, reward3, reward2, reward1];
const result2 = generateEpochIndividualRewardsList(reorderedRewards);
const result2 = generateEpochIndividualRewardsList({
rewards: reorderedRewards,
epochId: 2,
});
expect(result2[0].epoch).toEqual('2');
expect(result2[1].epoch).toEqual('1');
expect(result2[0].epoch).toEqual(2);
expect(result2[1].epoch).toEqual(1);
});
it('correctly calculates the total value of rewards for an asset', () => {
const rewards = [reward1, reward4];
const result = generateEpochIndividualRewardsList(rewards);
const result = generateEpochIndividualRewardsList({ rewards, epochId: 1 });
expect(result[0].rewards[0].totalAmount).toEqual('200');
});
@@ -127,11 +158,11 @@ describe('generateEpochIndividualRewardsList', () => {
it('returns data in the expected shape', () => {
// Just sanity checking the whole structure here
const rewards = [reward1, reward2, reward3, reward4];
const result = generateEpochIndividualRewardsList(rewards);
const result = generateEpochIndividualRewardsList({ rewards, epochId: 2 });
expect(result).toEqual([
{
epoch: '2',
epoch: 2,
rewards: [
{
asset: 'GBP',
@@ -196,7 +227,165 @@ describe('generateEpochIndividualRewardsList', () => {
],
},
{
epoch: '1',
epoch: 1,
rewards: [
{
asset: 'USD',
totalAmount: '200',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
},
]);
});
it('returns data correctly for the requested epoch range', () => {
const rewards = [reward1, reward2, reward3, reward4, reward5];
const resultPageOne = generateEpochIndividualRewardsList({
rewards,
epochId: 3,
page: 1,
size: 2,
});
expect(resultPageOne).toEqual([
{
epoch: 3,
rewards: [
{
asset: 'USD',
totalAmount: '150',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '150',
percentageOfTotal: '0.15',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
},
{
epoch: 2,
rewards: [
{
asset: 'GBP',
totalAmount: '200',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '200',
percentageOfTotal: '0.2',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
{
asset: 'EUR',
totalAmount: '50',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '50',
percentageOfTotal: '0.05',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
},
]);
const resultPageTwo = generateEpochIndividualRewardsList({
rewards,
epochId: 3,
page: 2,
size: 2,
});
expect(resultPageTwo).toEqual([
{
epoch: 1,
rewards: [
{
asset: 'USD',
@@ -2,9 +2,10 @@ import { BigNumber } from '../../../lib/bignumber';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { RewardFieldsFragment } from '../home/__generated__/Rewards';
import type { AccountType } from '@vegaprotocol/types';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
export interface EpochIndividualReward {
epoch: string;
epoch: number;
rewards: {
asset: string;
totalAmount: string;
@@ -27,11 +28,29 @@ const emptyRowAccountTypes = accountTypes.map((type) => [
},
]);
export const generateEpochIndividualRewardsList = (
rewards: RewardFieldsFragment[]
) => {
export const generateEpochIndividualRewardsList = ({
rewards,
epochId,
page = 1,
size = 10,
}: {
rewards: RewardFieldsFragment[];
epochId: number;
page?: number;
size?: number;
}) => {
const map: Map<string, EpochIndividualReward> = new Map();
const { fromEpoch, toEpoch } = calculateEpochOffset({ epochId, page, size });
for (let i = toEpoch; i >= fromEpoch; i--) {
map.set(i.toString(), {
epoch: i,
rewards: [],
});
}
// We take the rewards and aggregate them by epoch and asset.
const epochIndividualRewards = rewards.reduce((map, reward) => {
const epochIndividualRewards = rewards.reduce((acc, reward) => {
const epochId = reward.epoch.id;
const assetName = reward.asset.name;
const rewardType = reward.rewardType;
@@ -40,14 +59,14 @@ export const generateEpochIndividualRewardsList = (
// if the rewardType is not of a type we display in the table, we skip it.
if (!accountTypes.includes(rewardType)) {
return map;
return acc;
}
if (!map.has(epochId)) {
map.set(epochId, { epoch: epochId, rewards: [] });
if (!acc.has(epochId)) {
return acc;
}
const epoch = map.get(epochId);
const epoch = acc.get(epochId);
let asset = epoch?.rewards.find((r) => r.asset === assetName);
@@ -76,8 +95,8 @@ export const generateEpochIndividualRewardsList = (
});
}
return map;
}, new Map<string, EpochIndividualReward>());
return acc;
}, map);
return Array.from(epochIndividualRewards.values()).sort(
(a, b) => Number(b.epoch) - Number(a.epoch)
@@ -1,44 +1,64 @@
import { render } from '@testing-library/react';
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
import type {
AggregatedEpochRewardSummary,
RewardType,
RewardItem,
} from './generate-epoch-total-rewards-list';
import { AccountType } from '@vegaprotocol/types';
const assetId =
'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663';
const rewardsList = [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '295',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
];
const rewards: Map<RewardType, RewardItem> = new Map();
rewardsList.forEach((r) => {
rewards.set(r.rewardType, r);
});
const assetRewards: Map<
AggregatedEpochRewardSummary['assetId'],
AggregatedEpochRewardSummary
> = new Map();
assetRewards.set(assetId, {
assetId,
name: 'tDAI TEST',
rewards,
totalAmount: '295',
});
const mockData = {
epoch: 4431,
assetRewards: [
{
assetId:
'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '295',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '295',
},
],
assetRewards,
};
describe('EpochTotalRewardsTable', () => {
@@ -48,17 +48,19 @@ export const EpochTotalRewardsTable = ({
}: EpochTotalRewardsGridProps) => {
return (
<RewardsTable dataTestId="epoch-total-rewards-table" epoch={data.epoch}>
{data.assetRewards.map(({ name, rewards, totalAmount }, i) => (
<div className="contents" key={i}>
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
{name}
{Array.from(data.assetRewards.values()).map(
({ name, rewards, totalAmount }, i) => (
<div className="contents" key={i}>
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
{name}
</div>
{Array.from(rewards.values()).map(({ rewardType, amount }, i) => (
<RewardItem key={i} dataTestId={rewardType} value={amount} />
))}
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
{rewards.map(({ rewardType, amount }, i) => (
<RewardItem key={i} dataTestId={rewardType} value={amount} />
))}
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
))}
)
)}
</RewardsTable>
);
};
@@ -1,21 +1,62 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { AsyncRenderer, Pagination } from '@vegaprotocol/ui-toolkit';
import type { EpochFieldsFragment } from '../home/__generated__/Rewards';
import { useEpochAssetsRewardsQuery } from '../home/__generated__/Rewards';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { generateEpochTotalRewardsList } from './generate-epoch-total-rewards-list';
import { NoRewards } from '../no-rewards';
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
export const EpochTotalRewards = () => {
const EPOCHS_PAGE_SIZE = 10;
type EpochTotalRewardsProps = {
currentEpoch: EpochFieldsFragment;
};
export const EpochTotalRewards = ({ currentEpoch }: EpochTotalRewardsProps) => {
// we start from the previous epoch when displaying rewards data, because the current one has no calculated data while ongoing
const epochId = Number(currentEpoch.id) - 1;
const totalPages = Math.ceil(epochId / EPOCHS_PAGE_SIZE);
const { t } = useTranslation();
const [page, setPage] = useState(1);
const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({
notifyOnNetworkStatusChange: true,
variables: {
epochRewardSummariesPagination: {
first: 10,
epochRewardSummariesFilter: {
fromEpoch: epochId - EPOCHS_PAGE_SIZE,
},
},
});
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
const epochTotalRewardSummaries = generateEpochTotalRewardsList(data) || [];
const refetchData = useCallback(
async (toPage?: number) => {
const targetPage = toPage ?? page;
await refetch({
epochRewardSummariesFilter: calculateEpochOffset({
epochId,
page: targetPage,
size: EPOCHS_PAGE_SIZE,
}),
});
setPage(targetPage);
},
[epochId, page, refetch]
);
useEffect(() => {
// when the epoch changes, we want to refetch the data to update the current page
if (data) {
refetchData();
}
}, [epochId, data, refetchData]);
const epochTotalRewardSummaries =
generateEpochTotalRewardsList({
data,
epochId,
page,
size: EPOCHS_PAGE_SIZE,
}) || [];
return (
<AsyncRenderer
@@ -27,15 +68,22 @@ export const EpochTotalRewards = () => {
className="max-w-full overflow-auto"
data-testid="epoch-rewards-total"
>
{epochTotalRewardSummaries.length === 0 ? (
<NoRewards />
) : (
<>
{epochTotalRewardSummaries.map((epochTotalSummary, index) => (
<EpochTotalRewardsTable data={epochTotalSummary} key={index} />
))}
</>
{Array.from(epochTotalRewardSummaries.values()).map(
(epochTotalSummary, index) => (
<EpochTotalRewardsTable data={epochTotalSummary} key={index} />
)
)}
<Pagination
isLoading={loading}
hasPrevPage={page > 1}
hasNextPage={page < totalPages}
onBack={() => refetchData(page - 1)}
onNext={() => refetchData(page + 1)}
onFirst={() => refetchData(1)}
onLast={() => refetchData(totalPages)}
>
{t('Page')} {page}
</Pagination>
</div>
)}
/>
@@ -3,13 +3,23 @@ import { AccountType } from '@vegaprotocol/types';
describe('generateEpochAssetRewardsList', () => {
it('should return an empty array if data is undefined', () => {
const result = generateEpochTotalRewardsList(undefined);
const result = generateEpochTotalRewardsList({ epochId: 1 });
expect(result).toEqual([]);
expect(result).toEqual(
new Map([
[
'1',
{
epoch: 1,
assetRewards: new Map(),
},
],
])
);
});
it('should return an empty array if empty data is provided', () => {
const epochData = {
it('should return an empty map if empty data is provided', () => {
const data = {
assetsConnection: {
edges: [],
},
@@ -23,13 +33,23 @@ describe('generateEpochAssetRewardsList', () => {
},
};
const result = generateEpochTotalRewardsList(epochData);
const result = generateEpochTotalRewardsList({ data, epochId: 1 });
expect(result).toEqual([]);
expect(result).toEqual(
new Map([
[
'1',
{
epoch: 1,
assetRewards: new Map(),
},
],
])
);
});
it('should return an empty array if no epochRewardSummaries are provided', () => {
const epochData = {
it('should return an empty map if no epochRewardSummaries are provided', () => {
const data = {
assetsConnection: {
edges: [
{
@@ -56,13 +76,23 @@ describe('generateEpochAssetRewardsList', () => {
},
};
const result = generateEpochTotalRewardsList(epochData);
const result = generateEpochTotalRewardsList({ data, epochId: 1 });
expect(result).toEqual([]);
expect(result).toEqual(
new Map([
[
'1',
{
epoch: 1,
assetRewards: new Map(),
},
],
])
);
});
it('should return an array of unnamed assets if no asset names are provided (should not happen)', () => {
const epochData = {
it('should return a map of unnamed assets if no asset names are provided (should not happen)', () => {
const data = {
assetsConnection: {
edges: [],
},
@@ -85,50 +115,80 @@ describe('generateEpochAssetRewardsList', () => {
},
};
const result = generateEpochTotalRewardsList(epochData);
const result = generateEpochTotalRewardsList({ data, epochId: 1 });
expect(result).toEqual([
{
epoch: 1,
assetRewards: [
expect(result).toEqual(
new Map([
[
'1',
{
assetId: '1',
name: '',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '123',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '123',
epoch: 1,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: '',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '123',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '123',
},
],
]),
},
],
},
]);
])
);
});
it('should return an array of aggregated epoch summaries', () => {
const epochData = {
it('should return the aggregated epoch summaries', () => {
const data = {
assetsConnection: {
edges: [
{
@@ -180,81 +240,425 @@ describe('generateEpochAssetRewardsList', () => {
},
};
const result = generateEpochTotalRewardsList(epochData);
const result = generateEpochTotalRewardsList({ data, epochId: 2 });
expect(result).toEqual([
{
epoch: 1,
assetRewards: [
expect(result).toEqual(
new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '223',
epoch: 1,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '223',
},
],
]),
},
],
[
'2',
{
epoch: 2,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '5',
},
],
]),
},
],
])
);
});
it('should return the requested range for aggregated epoch summaries', () => {
const data = {
assetsConnection: {
edges: [
{
node: {
id: '1',
name: 'Asset 1',
},
},
{
node: {
id: '2',
name: 'Asset 2',
},
},
],
},
{
epoch: 2,
assetRewards: [
epochRewardSummaries: {
edges: [
{
assetId: '1',
name: 'Asset 1',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '5',
node: {
epoch: 1,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
},
{
node: {
epoch: 1,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100',
},
},
{
node: {
epoch: 2,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '6',
},
},
{
node: {
epoch: 2,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '27',
},
},
{
node: {
epoch: 3,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '15',
},
},
],
},
]);
epoch: {
timestamps: {
expiry: null,
},
},
};
const resultPageOne = generateEpochTotalRewardsList({
data,
epochId: 3,
page: 1,
size: 2,
});
expect(resultPageOne).toEqual(
new Map([
[
'2',
{
epoch: 2,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '33',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '33',
},
],
]),
},
],
[
'3',
{
epoch: 3,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '15',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '15',
},
],
]),
},
],
])
);
const resultPageTwo = generateEpochTotalRewardsList({
data,
epochId: 3,
page: 2,
size: 2,
});
expect(resultPageTwo).toEqual(
new Map([
[
'1',
{
epoch: 1,
assetRewards: new Map([
[
'1',
{
assetId: '1',
name: 'Asset 1',
rewards: new Map([
[
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
{
rewardType:
AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
],
[
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
{
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
]),
totalAmount: '223',
},
],
]),
},
],
])
);
});
});
@@ -5,127 +5,97 @@ import type {
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { AccountType } from '@vegaprotocol/types';
import { BigNumber } from '../../../lib/bignumber';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
interface EpochSummaryWithNamedReward extends EpochRewardSummaryFieldsFragment {
name: string;
}
export interface AggregatedEpochRewardSummary {
export type RewardType = EpochRewardSummaryFieldsFragment['rewardType'];
export type RewardItem = Pick<
EpochRewardSummaryFieldsFragment,
'rewardType' | 'amount'
>;
export type AggregatedEpochRewardSummary = {
assetId: EpochRewardSummaryFieldsFragment['assetId'];
name: EpochSummaryWithNamedReward['name'];
rewards: {
rewardType: EpochRewardSummaryFieldsFragment['rewardType'];
amount: EpochRewardSummaryFieldsFragment['amount'];
}[];
rewards: Map<RewardType, RewardItem>;
totalAmount: string;
}
};
export interface EpochTotalSummary {
export type EpochTotalSummary = {
epoch: EpochRewardSummaryFieldsFragment['epoch'];
assetRewards: AggregatedEpochRewardSummary[];
}
assetRewards: Map<
EpochRewardSummaryFieldsFragment['assetId'],
AggregatedEpochRewardSummary
>;
};
const emptyRowAccountTypes = Object.keys(RowAccountTypes).map((type) => ({
rewardType: type as AccountType,
amount: '0',
}));
const emptyRowAccountTypes: Map<RewardType, RewardItem> = new Map();
export const generateEpochTotalRewardsList = (
epochData: EpochAssetsRewardsQuery | undefined
) => {
Object.keys(RowAccountTypes).forEach((type) => {
emptyRowAccountTypes.set(type as AccountType, {
rewardType: type as AccountType,
amount: '0',
});
});
export const generateEpochTotalRewardsList = ({
data,
epochId,
page = 1,
size = 10,
}: {
data?: EpochAssetsRewardsQuery | undefined;
epochId: number;
page?: number;
size?: number;
}) => {
const epochRewardSummaries = removePaginationWrapper(
epochData?.epochRewardSummaries?.edges
data?.epochRewardSummaries?.edges
);
const assets = removePaginationWrapper(epochData?.assetsConnection?.edges);
const assets = removePaginationWrapper(data?.assetsConnection?.edges);
// Because the epochRewardSummaries don't have the asset name, we need to find it in the assets list
const epochSummariesWithNamedReward: EpochSummaryWithNamedReward[] =
epochRewardSummaries.map((epochReward) => ({
...epochReward,
name:
assets.find((asset) => asset.id === epochReward.assetId)?.name || '',
}));
const map: Map<string, EpochTotalSummary> = new Map();
const { fromEpoch, toEpoch } = calculateEpochOffset({ epochId, page, size });
// Aggregating the epoch summaries by epoch number
const aggregatedEpochSummariesByEpochNumber =
epochSummariesWithNamedReward.reduce((acc, epochReward) => {
const epoch = epochReward.epoch;
const epochSummaryIndex = acc.findIndex(
(epochSummary) => epochSummary[0].epoch === epoch
);
if (epochSummaryIndex === -1) {
acc.push([epochReward]);
} else {
acc[epochSummaryIndex].push(epochReward);
}
return acc;
}, [] as EpochSummaryWithNamedReward[][]);
// Now aggregate the array of arrays of epoch summaries by asset rewards.
const epochTotalRewards: EpochTotalSummary[] =
aggregatedEpochSummariesByEpochNumber.map((epochSummaries) => {
const assetRewards = epochSummaries.reduce((acc, epochSummary) => {
const assetRewardIndex = acc.findIndex(
(assetReward) =>
assetReward.assetId === epochSummary.assetId &&
assetReward.name === epochSummary.name
);
if (assetRewardIndex === -1) {
acc.push({
assetId: epochSummary.assetId,
name: epochSummary.name,
rewards: [
...emptyRowAccountTypes.map((emptyRowAccountType) => {
if (
emptyRowAccountType.rewardType === epochSummary.rewardType
) {
return {
rewardType: epochSummary.rewardType,
amount: epochSummary.amount,
};
} else {
return emptyRowAccountType;
}
}),
],
totalAmount: epochSummary.amount,
});
} else {
acc[assetRewardIndex].rewards = acc[assetRewardIndex].rewards.map(
(reward) => {
if (reward.rewardType === epochSummary.rewardType) {
return {
rewardType: epochSummary.rewardType,
amount: (
Number(reward.amount) + Number(epochSummary.amount)
).toString(),
};
} else {
return reward;
}
}
);
acc[assetRewardIndex].totalAmount = (
Number(acc[assetRewardIndex].totalAmount) +
Number(epochSummary.amount)
).toString();
}
return acc;
}, [] as AggregatedEpochRewardSummary[]);
return {
epoch: epochSummaries[0].epoch,
assetRewards: assetRewards.sort((a, b) => {
return new BigNumber(b.totalAmount).comparedTo(a.totalAmount);
}),
};
for (let i = toEpoch; i >= fromEpoch; i--) {
map.set(i.toString(), {
epoch: i,
assetRewards: new Map(),
});
}
return epochTotalRewards;
return epochRewardSummaries.reduce((acc, reward) => {
const epoch = acc.get(reward.epoch.toString());
if (epoch) {
const matchingAsset = assets.find((asset) => asset.id === reward.assetId);
const assetWithRewards = epoch.assetRewards.get(reward.assetId);
const rewards =
assetWithRewards?.rewards || new Map(emptyRowAccountTypes);
const rewardItem = rewards?.get(reward.rewardType);
const amount = (
(Number(rewardItem?.amount) || 0) + Number(reward.amount)
).toString();
rewards?.set(reward.rewardType, {
rewardType: reward.rewardType,
amount,
});
epoch.assetRewards.set(reward.assetId, {
assetId: reward.assetId,
name: matchingAsset?.name || '',
rewards: rewards || new Map(emptyRowAccountTypes),
totalAmount: (
Number(reward.amount) + Number(assetWithRewards?.totalAmount || 0)
).toString(),
});
}
return acc;
}, map);
};
@@ -23,12 +23,18 @@ fragment DelegationFields on Delegation {
query Rewards(
$partyId: ID!
$delegationsPagination: Pagination
$fromEpoch: Int
$toEpoch: Int
$rewardsPagination: Pagination
$delegationsPagination: Pagination
) {
party(id: $partyId) {
id
rewardsConnection(pagination: $rewardsPagination) {
rewardsConnection(
fromEpoch: $fromEpoch
toEpoch: $toEpoch
pagination: $rewardsPagination
) {
edges {
node {
...RewardFields
@@ -43,14 +49,6 @@ query Rewards(
}
}
}
epoch {
id
timestamps {
start
end
expiry
}
}
}
fragment EpochRewardSummaryFields on EpochRewardSummary {
@@ -60,7 +58,10 @@ fragment EpochRewardSummaryFields on EpochRewardSummary {
rewardType
}
query EpochAssetsRewards($epochRewardSummariesPagination: Pagination) {
query EpochAssetsRewards(
$epochRewardSummariesFilter: RewardSummaryFilter
$epochRewardSummariesPagination: Pagination
) {
assetsConnection {
edges {
node {
@@ -69,18 +70,16 @@ query EpochAssetsRewards($epochRewardSummariesPagination: Pagination) {
}
}
}
epochRewardSummaries(pagination: $epochRewardSummariesPagination) {
epochRewardSummaries(
filter: $epochRewardSummariesFilter
pagination: $epochRewardSummariesPagination
) {
edges {
node {
...EpochRewardSummaryFields
}
}
}
epoch {
timestamps {
expiry
}
}
}
fragment EpochFields on Epoch {
@@ -9,21 +9,24 @@ export type DelegationFieldsFragment = { __typename?: 'Delegation', amount: stri
export type RewardsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
delegationsPagination?: Types.InputMaybe<Types.Pagination>;
fromEpoch?: Types.InputMaybe<Types.Scalars['Int']>;
toEpoch?: Types.InputMaybe<Types.Scalars['Int']>;
rewardsPagination?: Types.InputMaybe<Types.Pagination>;
delegationsPagination?: Types.InputMaybe<Types.Pagination>;
}>;
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } } };
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null };
export type EpochRewardSummaryFieldsFragment = { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType };
export type EpochAssetsRewardsQueryVariables = Types.Exact<{
epochRewardSummariesFilter?: Types.InputMaybe<Types.RewardSummaryFilter>;
epochRewardSummariesPagination?: Types.InputMaybe<Types.Pagination>;
}>;
export type EpochAssetsRewardsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string } } | null> | null } | null, epochRewardSummaries?: { __typename?: 'EpochRewardSummaryConnection', edges?: Array<{ __typename?: 'EpochRewardSummaryEdge', node: { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType } } | null> | null } | null, epoch: { __typename?: 'Epoch', timestamps: { __typename?: 'EpochTimestamps', expiry?: any | null } } };
export type EpochAssetsRewardsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string } } | null> | null } | null, epochRewardSummaries?: { __typename?: 'EpochRewardSummaryConnection', edges?: Array<{ __typename?: 'EpochRewardSummaryEdge', node: { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType } } | null> | null } | null };
export type EpochFieldsFragment = { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } };
@@ -76,10 +79,14 @@ export const EpochFieldsFragmentDoc = gql`
}
`;
export const RewardsDocument = gql`
query Rewards($partyId: ID!, $delegationsPagination: Pagination, $rewardsPagination: Pagination) {
query Rewards($partyId: ID!, $fromEpoch: Int, $toEpoch: Int, $rewardsPagination: Pagination, $delegationsPagination: Pagination) {
party(id: $partyId) {
id
rewardsConnection(pagination: $rewardsPagination) {
rewardsConnection(
fromEpoch: $fromEpoch
toEpoch: $toEpoch
pagination: $rewardsPagination
) {
edges {
node {
...RewardFields
@@ -94,14 +101,6 @@ export const RewardsDocument = gql`
}
}
}
epoch {
id
timestamps {
start
end
expiry
}
}
}
${RewardFieldsFragmentDoc}
${DelegationFieldsFragmentDoc}`;
@@ -119,8 +118,10 @@ ${DelegationFieldsFragmentDoc}`;
* const { data, loading, error } = useRewardsQuery({
* variables: {
* partyId: // value for 'partyId'
* delegationsPagination: // value for 'delegationsPagination'
* fromEpoch: // value for 'fromEpoch'
* toEpoch: // value for 'toEpoch'
* rewardsPagination: // value for 'rewardsPagination'
* delegationsPagination: // value for 'delegationsPagination'
* },
* });
*/
@@ -136,7 +137,7 @@ export type RewardsQueryHookResult = ReturnType<typeof useRewardsQuery>;
export type RewardsLazyQueryHookResult = ReturnType<typeof useRewardsLazyQuery>;
export type RewardsQueryResult = Apollo.QueryResult<RewardsQuery, RewardsQueryVariables>;
export const EpochAssetsRewardsDocument = gql`
query EpochAssetsRewards($epochRewardSummariesPagination: Pagination) {
query EpochAssetsRewards($epochRewardSummariesFilter: RewardSummaryFilter, $epochRewardSummariesPagination: Pagination) {
assetsConnection {
edges {
node {
@@ -145,18 +146,16 @@ export const EpochAssetsRewardsDocument = gql`
}
}
}
epochRewardSummaries(pagination: $epochRewardSummariesPagination) {
epochRewardSummaries(
filter: $epochRewardSummariesFilter
pagination: $epochRewardSummariesPagination
) {
edges {
node {
...EpochRewardSummaryFields
}
}
}
epoch {
timestamps {
expiry
}
}
}
${EpochRewardSummaryFieldsFragmentDoc}`;
@@ -172,6 +171,7 @@ export const EpochAssetsRewardsDocument = gql`
* @example
* const { data, loading, error } = useEpochAssetsRewardsQuery({
* variables: {
* epochRewardSummariesFilter: // value for 'epochRewardSummariesFilter'
* epochRewardSummariesPagination: // value for 'epochRewardSummariesPagination'
* },
* });
@@ -114,7 +114,7 @@ export const RewardsPage = () => {
</p>
</div>
<div className="w-[440px]">
<div className="w-[360px]">
<Toggle
name="epoch-reward-view-toggle"
toggles={[
@@ -136,11 +136,15 @@ export const RewardsPage = () => {
</section>
{toggleRewardsView === 'total' ? (
<EpochTotalRewards />
epochData?.epoch ? (
<EpochTotalRewards currentEpoch={epochData?.epoch} />
) : null
) : (
<section>
{pubKey && pubKeys?.length ? (
<EpochIndividualRewards />
epochData?.epoch ? (
<EpochIndividualRewards currentEpoch={epochData?.epoch} />
) : null
) : (
<ConnectToSeeRewards />
)}
@@ -109,6 +109,13 @@ const LazyProposal = React.lazy(
)
);
const LazyProtocolUpgradeProposal = React.lazy(
() =>
import(
/* webpackChunkName: "route-governance-protocol-upgrade-proposal", webpackPrefetch: true */ './proposals/protocol-upgrade'
)
);
const LazyProposalsList = React.lazy(
() =>
import(
@@ -257,6 +264,10 @@ const routerConfig = [
},
{ path: 'proposals', element: <LazyProposalsList /> },
{ path: ':proposalId', element: <LazyProposal /> },
{
path: 'protocol-upgrade/:proposalReleaseTag',
element: <LazyProtocolUpgradeProposal />,
},
{ path: 'rejected', element: <LazyRejectedProposalsList /> },
],
},
@@ -155,16 +155,16 @@ export const ValidatorTables = ({
return (
<section data-testid="validator-tables">
<div className="grid w-full justify-end">
<div className="w-[400px]">
<div className="w-[340px]">
<Toggle
name="validators-view-toggle"
toggles={[
{
label: t('ALL VALIDATORS'),
label: t('All validators'),
value: 'all',
},
{
label: t('STAKED BY ME'),
label: t('Staked by me'),
value: 'myStake',
},
]}
+2 -2
View File
@@ -20,8 +20,8 @@ export const getLastEpochScoreAndPerformance = (
};
};
export const getNormalisedVotingPower = (votingPower: string) =>
formatNumberPercentage(new BigNumber(votingPower).dividedBy(100), 2);
export const getNormalisedVotingPower = (votingPower: string, decimals = 2) =>
formatNumberPercentage(new BigNumber(votingPower).dividedBy(100), decimals);
export const getUnnormalisedVotingPower = (
validatorScore: string | null | undefined
File diff suppressed because it is too large Load Diff
@@ -41,6 +41,12 @@ describe('accounts', { tags: '@smoke' }, () => {
.should('have.text', '100,001.01');
});
it('asset detail should be properly rendered', () => {
cy.getByTestId('Collateral').click();
cy.getByTestId('asset').contains('tEURO').click();
cy.get('[data-testid$="_label"]').should('have.length', 16);
});
describe('sorting by ag-grid columns should work well', () => {
it('sorting by asset', () => {
cy.getByTestId('Collateral').click();
+2 -2
View File
@@ -11,7 +11,7 @@ NX_VEGA_URL=http://localhost:3028/query
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
@@ -29,7 +29,10 @@ import { LiquidityContainer } from '../liquidity/liquidity';
import { useNavigate } from 'react-router-dom';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import {
useMarketClickHandler,
useMarketLiquidityClickHandler,
} from '../../lib/hooks/use-market-click-handler';
type MarketDependantView =
| typeof CandlesChartContainer
@@ -79,6 +82,7 @@ const MarketBottomPanel = memo(
({ marketId, pinnedAsset }: BottomPanelProps) => {
const { screenSize } = useScreenDimensions();
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
return 'xxxl' === screenSize ? (
<ResizableGrid proportionalLayout minSize={200}>
@@ -94,6 +98,7 @@ const MarketBottomPanel = memo(
<TradingViews.Orders
marketId={marketId}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
enforceBottomPlaceholder
/>
</VegaWalletContainer>
@@ -150,6 +155,7 @@ const MarketBottomPanel = memo(
<TradingViews.Orders
marketId={marketId}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
enforceBottomPlaceholder
/>
</VegaWalletContainer>
@@ -293,6 +299,7 @@ interface TradePanelsProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}
@@ -303,12 +310,16 @@ export const TradePanels = ({
onClickCollateral,
pinnedAsset,
}: TradePanelsProps) => {
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [view, setView] = useState<TradingView>('Candles');
const renderView = () => {
const Component = memo<{
marketId: string;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}>(TradingViews[view]);
@@ -325,6 +336,8 @@ export const TradePanels = ({
onSelect={onSelect}
onClickCollateral={onClickCollateral}
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
/>
);
};
@@ -19,7 +19,10 @@ import { usePageTitleStore } from '../../stores';
import { LedgerContainer } from '@vegaprotocol/ledger';
import { AccountsContainer } from '../../components/accounts-container';
import { AccountHistoryContainer } from './account-history-container';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import {
useMarketClickHandler,
useMarketLiquidityClickHandler,
} from '../../lib/hooks/use-market-click-handler';
export const Portfolio = () => {
const { updateTitle } = usePageTitleStore((store) => ({
@@ -31,6 +34,7 @@ export const Portfolio = () => {
}, [updateTitle]);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const wrapperClasses = 'h-full max-h-full flex flex-col';
return (
@@ -54,7 +58,10 @@ export const Portfolio = () => {
</Tab>
<Tab id="orders" name={t('Orders')}>
<VegaWalletContainer>
<OrderListContainer onMarketClick={onMarketClick} />
<OrderListContainer
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="fills" name={t('Fills')}>
@@ -19,3 +19,21 @@ export const useMarketClickHandler = (replace = false) => {
[navigate, marketId, replace, isMarketPage]
);
};
export const useMarketLiquidityClickHandler = (replace = false) => {
const navigate = useNavigate();
const { marketId } = useParams();
const { pathname } = useLocation();
const isLiquidityPage = pathname.match(/^\/liquidity\/(.+)/);
return useCallback(
(selectedId: string, metaKey?: boolean) => {
const link = Links[Routes.LIQUIDITY](selectedId);
if (metaKey) {
window.open(`/#${link}`, '_blank');
} else if (selectedId !== marketId || !isLiquidityPage) {
navigate(link, { replace });
}
},
[navigate, marketId, replace, isLiquidityPage]
);
};
@@ -7,7 +7,11 @@ import {
} from '@vegaprotocol/types';
import type { VegaStoredTxState } from '@vegaprotocol/wallet';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import { VegaTransactionDetails } from './use-vega-transaction-toasts';
import {
VegaTransactionDetails,
getVegaTransactionContentIntent,
} from './use-vega-transaction-toasts';
import { Intent } from '@vegaprotocol/ui-toolkit';
jest.mock('@vegaprotocol/assets', () => {
const A1 = {
@@ -278,3 +282,27 @@ describe('VegaTransactionDetails', () => {
expect(queryByTestId('toast-panel')?.textContent).toEqual(details);
});
});
describe('getVegaTransactionContentIntent', () => {
it('returns the correct intent for a transaction', () => {
expect(getVegaTransactionContentIntent(withdraw).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(submitOrder).intent).toBe(
Intent.Success
);
expect(getVegaTransactionContentIntent(editOrder).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(cancelOrder).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(cancelAll).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(closePosition).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(batch).intent).toBe(Intent.Primary);
});
});
@@ -547,7 +547,11 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
return (
<>
<ToastHeading>{t('Confirmed')}</ToastHeading>
<ToastHeading>
{tx.order?.status
? getOrderToastTitle(tx.order.status)
: t('Confirmed')}
</ToastHeading>
<p>{t('Your transaction has been confirmed ')}</p>
{tx.txHash && (
<p className="break-all">
@@ -634,25 +638,8 @@ export const useVegaTransactionToasts = () => {
);
const fromVegaTransaction = (tx: VegaStoredTxState): Toast => {
let content: ToastContent;
const closeAfter = isFinal(tx) ? CLOSE_AFTER : undefined;
if (tx.status === VegaTxStatus.Requested) {
content = <VegaTxRequestedToastContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Pending) {
content = <VegaTxPendingToastContentProps tx={tx} />;
}
if (tx.status === VegaTxStatus.Complete) {
content = <VegaTxCompleteToastsContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Error) {
content = <VegaTxErrorToastContent tx={tx} />;
}
// Transaction can be successful but the order can be rejected by the network
const intent =
(tx.order && getOrderToastIntent(tx.order.status)) ||
intentMap[tx.status];
const { intent, content } = getVegaTransactionContentIntent(tx);
return {
id: `vega-${tx.id}`,
@@ -676,3 +663,27 @@ export const useVegaTransactionToasts = () => {
}
);
};
export const getVegaTransactionContentIntent = (tx: VegaStoredTxState) => {
let content: ToastContent;
if (tx.status === VegaTxStatus.Requested) {
content = <VegaTxRequestedToastContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Pending) {
content = <VegaTxPendingToastContentProps tx={tx} />;
}
if (tx.status === VegaTxStatus.Complete) {
content = <VegaTxCompleteToastsContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Error) {
content = <VegaTxErrorToastContent tx={tx} />;
}
// Transaction can be successful but the order can be rejected by the network
const intent =
(tx.order &&
!isOrderAmendmentTransaction(tx.body) &&
getOrderToastIntent(tx.order.status)) ||
intentMap[tx.status];
return { intent, content };
};
+15
View File
@@ -33,6 +33,8 @@ import { ViewingBanner } from '../components/viewing-banner';
import { Banner } from '../components/banner';
import { AppLoader, DynamicLoader } from '../components/app-loader';
import { Navbar } from '../components/navbar';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { activeOrdersProvider } from '@vegaprotocol/orders';
const DEFAULT_TITLE = t('Welcome to Vega trading!');
@@ -95,6 +97,7 @@ function AppBody({ Component }: AppProps) {
<ToastsManager />
<InitializeHandlers />
<MaybeConnectEagerly />
<PartyData />
</div>
);
}
@@ -127,6 +130,18 @@ function VegaTradingApp(props: AppProps) {
export default VegaTradingApp;
const PartyData = () => {
const { pubKey } = useVegaWallet();
const variables = { partyId: pubKey || '' };
const skip = !pubKey;
useDataProvider({
dataProvider: activeOrdersProvider,
variables,
skip,
});
return null;
};
const MaybeConnectEagerly = () => {
useVegaEagerConnect(Connectors);
useEthereumEagerConnect();
+15 -3
View File
@@ -31,7 +31,7 @@ export enum Routes {
MARKET = '/markets',
MARKETS = '/markets/all',
PORTFOLIO = '/portfolio',
LIQUIDITY = 'liquidity/:marketId',
LIQUIDITY = '/liquidity',
}
type ConsoleLinks = { [r in Routes]: (...args: string[]) => string };
@@ -41,8 +41,10 @@ export const Links: ConsoleLinks = {
marketId ? trimEnd(`${Routes.MARKET}/${marketId}`, '/') : Routes.MARKET,
[Routes.MARKETS]: () => Routes.MARKETS,
[Routes.PORTFOLIO]: () => Routes.PORTFOLIO,
[Routes.LIQUIDITY]: (marketId: string) =>
Routes.LIQUIDITY.replace(':marketId', marketId),
[Routes.LIQUIDITY]: (marketId: string | null | undefined) =>
marketId
? trimEnd(`${Routes.LIQUIDITY}/${marketId}`, '/')
: Routes.LIQUIDITY,
};
const routerConfig: RouteObject[] = [
@@ -70,6 +72,16 @@ const routerConfig: RouteObject[] = [
{
path: Routes.LIQUIDITY,
element: <LazyLiquidity />,
children: [
{
index: true,
element: <LazyLiquidity />,
},
{
path: ':marketId',
element: <LazyLiquidity />,
},
],
},
{
path: Routes.PORTFOLIO,
+6
View File
@@ -0,0 +1,6 @@
FROM --platform=amd64 nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
EXPOSE 80
WORKDIR /usr/share/nginx/html
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -rf /usr/share/nginx/html/*
COPY ./dist-result/ /usr/share/nginx/html/
+3 -5
View File
@@ -5,7 +5,9 @@ export PATH="/app/node_modules/.bin:$PATH"
flags="--network-timeout 100000 --pure-lockfile"
if [[ ! -z "${ENV_NAME}" ]]; then
flags="--env=${ENV_NAME} $flags"
if [[ "${ENV_NAME}" != "ops-vega" ]]; then
flags="--env=${ENV_NAME} $flags"
fi
fi
if [ "${APP}" = "trading" ]; then
@@ -16,7 +18,3 @@ if [ "${APP}" = "trading" ]; then
else
yarn nx build ${APP} $flags
fi
env_vars_file="/app/dist/apps/${APP}/.env"
# make sure there are no exposed .env files
rm $env_vars_file || echo "No env vars file"
+5 -4
View File
@@ -13,7 +13,7 @@ RUN apk add --update --no-cache \
COPY . ./
RUN yarn --network-timeout 100000 --pure-lockfile
# work around for different build process in trading
RUN sh ./docker-build.sh
RUN sh docker/docker-build.sh
# Server environment
# if this fails you need to docker pull nginx:1.23-alpine and pin new SHA
@@ -23,6 +23,7 @@ FROM --platform=amd64 nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a
EXPOSE 80
# Copy dist
WORKDIR /usr/share/nginx/html
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/apps/${APP} /usr/share/nginx/html
RUN apk add --no-cache go-ipfs; ipfs init && echo "$(ipfs add -rQ .)" > ipfs-hash; apk del go-ipfs
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -rf /usr/share/nginx/html/*
COPY --from=build /app/dist/apps/${APP}/* /usr/share/nginx/html
RUN apk add --no-cache go-ipfs; ipfs init && echo "$(ipfs add -rQ .)" > /ipfs-hash; apk del go-ipfs
+18 -1
View File
@@ -1,8 +1,25 @@
fragment AssetListFields on Asset {
id
name
symbol
decimals
quantum
source {
__typename
... on ERC20 {
contractAddress
lifetimeLimit
withdrawThreshold
}
}
status
}
query Assets {
assetsConnection {
edges {
node {
...AssetFields
...AssetListFields
}
}
}
+23 -5
View File
@@ -1,26 +1,44 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import { AssetFieldsFragmentDoc } from './Asset';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type AssetListFieldsFragment = { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string } };
export type AssetsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type AssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string }, infrastructureFeeAccount?: { __typename?: 'AccountBalance', balance: string } | null, globalRewardPoolAccount?: { __typename?: 'AccountBalance', balance: string } | null, takerFeeRewardAccount?: { __typename?: 'AccountBalance', balance: string } | null, makerFeeRewardAccount?: { __typename?: 'AccountBalance', balance: string } | null, lpFeeRewardAccount?: { __typename?: 'AccountBalance', balance: string } | null, marketProposerRewardAccount?: { __typename?: 'AccountBalance', balance: string } | null } } | null> | null } | null };
export type AssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string } } } | null> | null } | null };
export const AssetListFieldsFragmentDoc = gql`
fragment AssetListFields on Asset {
id
name
symbol
decimals
quantum
source {
__typename
... on ERC20 {
contractAddress
lifetimeLimit
withdrawThreshold
}
}
status
}
`;
export const AssetsDocument = gql`
query Assets {
assetsConnection {
edges {
node {
...AssetFields
...AssetListFields
}
}
}
}
${AssetFieldsFragmentDoc}`;
${AssetListFieldsFragmentDoc}`;
/**
* __useAssetsQuery__
@@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react';
import * as Schema from '@vegaprotocol/types';
import { AssetDetailsDialog } from './asset-details-dialog';
import { AssetDetail, testId } from './asset-details-table';
import { AssetsDocument } from './__generated__/Assets';
import { AssetDocument } from './__generated__/Asset';
import { generateBuiltinAsset, generateERC20Asset } from './test-helpers';
const mockedData = {
@@ -39,15 +39,17 @@ const mockedData = {
},
};
const mocks = [
{
request: {
query: AssetsDocument,
variables: {},
},
result: mockedData,
const mocks = mockedData.data.assetsConnection.edges.map((mock) => ({
request: {
query: AssetDocument,
variables: { assetId: mock.node.id },
},
];
result: {
data: {
assetsConnection: { edges: [mock] },
},
},
}));
const WrappedAssetDetailsDialog = ({ assetId }: { assetId: string }) => (
<MockedProvider mocks={mocks}>
+2 -3
View File
@@ -1,5 +1,4 @@
import { t } from '@vegaprotocol/i18n';
import { useAssetsDataProvider } from './assets-data-provider';
import {
Button,
Dialog,
@@ -10,6 +9,7 @@ import {
import { create } from 'zustand';
import { AssetDetailsTable } from './asset-details-table';
import { AssetProposalNotification } from '@vegaprotocol/proposals';
import { useAssetDataProvider } from './asset-data-provider';
export type AssetDetailsDialogStore = {
isOpen: boolean;
@@ -55,9 +55,8 @@ export const AssetDetailsDialog = ({
onChange,
asJson = false,
}: AssetDetailsDialogProps) => {
const { data } = useAssetsDataProvider();
const { data: asset } = useAssetDataProvider(assetId);
const asset = data?.find((a) => a.id === assetId);
const assetSymbol = asset?.symbol || '';
const content = asset ? (
+2 -3
View File
@@ -11,6 +11,7 @@ import {
} from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import type { Asset } from './asset-data-provider';
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from './constants';
type Rows = {
key: AssetDetail;
@@ -121,9 +122,7 @@ export const rows: Rows = [
{
key: AssetDetail.WITHDRAWAL_THRESHOLD,
label: t('Withdrawal threshold'),
tooltip: t(
'The maximum you can withdraw instantly. Theres no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them'
),
tooltip: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
value: (asset) =>
num(asset, (asset.source as Schema.ERC20).withdrawThreshold),
},
+5 -119
View File
@@ -1,8 +1,10 @@
import merge from 'lodash/merge';
import type { AssetsQuery } from './__generated__/Assets';
import type {
AssetsQuery,
AssetListFieldsFragment,
} from './__generated__/Assets';
import * as Types from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import type { AssetFieldsFragment } from './__generated__/Asset';
export const assetsQuery = (
override?: PartialDeep<AssetsQuery>
@@ -18,7 +20,7 @@ export const assetsQuery = (
return merge(defaultAssets, override);
};
const assetFields: AssetFieldsFragment[] = [
const assetFields: AssetListFieldsFragment[] = [
{
__typename: 'Asset',
id: 'asset-id',
@@ -33,30 +35,6 @@ const assetFields: AssetFieldsFragment[] = [
},
quantum: '1',
status: Types.AssetStatus.STATUS_ENABLED,
infrastructureFeeAccount: {
balance: '1',
__typename: 'AccountBalance',
},
globalRewardPoolAccount: {
balance: '2',
__typename: 'AccountBalance',
},
takerFeeRewardAccount: {
balance: '3',
__typename: 'AccountBalance',
},
makerFeeRewardAccount: {
balance: '4',
__typename: 'AccountBalance',
},
lpFeeRewardAccount: {
balance: '5',
__typename: 'AccountBalance',
},
marketProposerRewardAccount: {
balance: '6',
__typename: 'AccountBalance',
},
},
{
__typename: 'Asset',
@@ -72,30 +50,6 @@ const assetFields: AssetFieldsFragment[] = [
},
quantum: '1',
status: Types.AssetStatus.STATUS_ENABLED,
infrastructureFeeAccount: {
balance: '1',
__typename: 'AccountBalance',
},
globalRewardPoolAccount: {
balance: '2',
__typename: 'AccountBalance',
},
takerFeeRewardAccount: {
balance: '3',
__typename: 'AccountBalance',
},
makerFeeRewardAccount: {
balance: '4',
__typename: 'AccountBalance',
},
lpFeeRewardAccount: {
balance: '5',
__typename: 'AccountBalance',
},
marketProposerRewardAccount: {
balance: '6',
__typename: 'AccountBalance',
},
},
{
__typename: 'Asset',
@@ -104,20 +58,10 @@ const assetFields: AssetFieldsFragment[] = [
decimals: 5,
name: 'Asto',
source: {
maxFaucetAmountMint: '5000000000',
__typename: 'BuiltinAsset',
},
quantum: '1',
status: Types.AssetStatus.STATUS_ENABLED,
infrastructureFeeAccount: {
balance: '0',
__typename: 'AccountBalance',
},
globalRewardPoolAccount: null,
takerFeeRewardAccount: null,
makerFeeRewardAccount: null,
lpFeeRewardAccount: null,
marketProposerRewardAccount: null,
},
{
__typename: 'Asset',
@@ -126,20 +70,10 @@ const assetFields: AssetFieldsFragment[] = [
decimals: 5,
name: 'tBTC TEST',
source: {
maxFaucetAmountMint: '5000000000',
__typename: 'BuiltinAsset',
},
quantum: '1',
status: Types.AssetStatus.STATUS_ENABLED,
infrastructureFeeAccount: {
balance: '0',
__typename: 'AccountBalance',
},
globalRewardPoolAccount: null,
takerFeeRewardAccount: null,
makerFeeRewardAccount: null,
lpFeeRewardAccount: null,
marketProposerRewardAccount: null,
},
// NOTE: These assets ids and contract addresses are real assets on Sepolia, this is needed
// because we don't currently mock our seplia infura provider. If we change network these will
@@ -158,30 +92,6 @@ const assetFields: AssetFieldsFragment[] = [
__typename: 'ERC20',
},
quantum: '1',
infrastructureFeeAccount: {
balance: '1',
__typename: 'AccountBalance',
},
globalRewardPoolAccount: {
balance: '2',
__typename: 'AccountBalance',
},
takerFeeRewardAccount: {
balance: '3',
__typename: 'AccountBalance',
},
makerFeeRewardAccount: {
balance: '4',
__typename: 'AccountBalance',
},
lpFeeRewardAccount: {
balance: '5',
__typename: 'AccountBalance',
},
marketProposerRewardAccount: {
balance: '6',
__typename: 'AccountBalance',
},
},
{
__typename: 'Asset',
@@ -197,29 +107,5 @@ const assetFields: AssetFieldsFragment[] = [
__typename: 'ERC20',
},
quantum: '1',
infrastructureFeeAccount: {
balance: '1',
__typename: 'AccountBalance',
},
globalRewardPoolAccount: {
balance: '2',
__typename: 'AccountBalance',
},
takerFeeRewardAccount: {
balance: '3',
__typename: 'AccountBalance',
},
makerFeeRewardAccount: {
balance: '4',
__typename: 'AccountBalance',
},
lpFeeRewardAccount: {
balance: '5',
__typename: 'AccountBalance',
},
marketProposerRewardAccount: {
balance: '6',
__typename: 'AccountBalance',
},
},
];
+4
View File
@@ -0,0 +1,4 @@
import { t } from '@vegaprotocol/i18n';
export const WITHDRAW_THRESHOLD_TOOLTIP_TEXT = t(
"The maximum you can withdraw instantly. There's no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them"
);
+1
View File
@@ -5,3 +5,4 @@ export * from './assets-data-provider';
export * from './asset-details-dialog';
export * from './asset-details-table';
export * from './asset-option';
export * from './constants';
+1
View File
@@ -10,6 +10,7 @@ export * from './lib/cells/price-flash-cell';
export * from './lib/cells/vol-cell';
export * from './lib/cells/centered-grid-cell';
export * from './lib/cells/market-name-cell';
export * from './lib/cells/order-type-cell';
export * from './lib/filters/date-range-filter';
export * from './lib/filters/set-filter';
@@ -0,0 +1,52 @@
import type { MouseEvent } from 'react';
import { useMemo } from 'react';
import { useCallback } from 'react';
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
interface OrderTypeCellProps {
value?: Schema.OrderType;
data?: Schema.Order;
onClick?: (marketId: string, metaKey?: boolean) => void;
}
export const OrderTypeCell = ({
value,
data: order,
onClick,
}: OrderTypeCellProps) => {
const id = order ? order.market.id : '';
const label = useMemo(() => {
if (!order) {
return undefined;
}
if (!value) return '-';
if (order?.peggedOrder) {
return t('Pegged');
}
if (order?.liquidityProvision) {
return t('Liquidity provision');
}
return Schema.OrderTypeMapping[value];
}, [order, value]);
const handleOnClick = useCallback(
(ev: MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
ev.stopPropagation();
if (onClick) {
onClick(id, ev.metaKey || ev.ctrlKey);
}
},
[id, onClick]
);
if (!order) return null;
return order?.liquidityProvision ? (
<button onClick={handleOnClick} tabIndex={0} className="underline">
{label}
</button>
) : (
<span>{label}</span>
);
};
@@ -8,7 +8,7 @@ const commonProps = {
describe('DateRangeFilter', () => {
it('should be properly rendered', async () => {
const defaultRangeFilter = {
const defaultValue = {
start: '2023-02-14T13:53:01+01:00',
end: '2023-02-21T13:53:01+01:00',
};
@@ -17,7 +17,7 @@ describe('DateRangeFilter', () => {
render(
<DateRangeFilter
{...(commonProps as unknown as DateRangeFilterProps)}
defaultRangeFilter={defaultRangeFilter}
defaultValue={defaultValue}
/>
);
@@ -1,5 +1,5 @@
import type { ChangeEvent } from 'react';
import { useEffect, useMemo } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import type * as Schema from '@vegaprotocol/types';
import { forwardRef, useImperativeHandle, useState } from 'react';
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
@@ -17,9 +17,9 @@ import { formatForInput } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { InputError } from '@vegaprotocol/ui-toolkit';
const defaultFilterValue: Schema.DateRange = {};
const defaultValue: Schema.DateRange = {};
export interface DateRangeFilterProps extends IFilterParams {
defaultRangeFilter?: Schema.DateRange;
defaultValue?: Schema.DateRange;
maxSubDays?: number;
maxNextDays?: number;
maxDaysRange?: number;
@@ -27,8 +27,9 @@ export interface DateRangeFilterProps extends IFilterParams {
export const DateRangeFilter = forwardRef(
(props: DateRangeFilterProps, ref) => {
const defaultDates = props?.defaultRangeFilter || defaultFilterValue;
const defaultDates = props?.defaultValue || defaultValue;
const [value, setValue] = useState<Schema.DateRange>(defaultDates);
const valueRef = useRef<Schema.DateRange>(value);
const [error, setError] = useState<string>('');
const [minStartDate, maxStartDate, minEndDate, maxEndDate] = useMemo(() => {
const minStartDate =
@@ -93,7 +94,7 @@ export const DateRangeFilter = forwardRef(
},
isFilterActive() {
return value.start || value.end;
return valueRef.current.start || valueRef.current.end;
},
getModel() {
@@ -101,13 +102,13 @@ export const DateRangeFilter = forwardRef(
return null;
}
return { value };
return { value: valueRef.current };
},
setModel(model?: { value: Schema.DateRange } | null) {
setValue(
model?.value || props?.defaultRangeFilter || defaultFilterValue
);
valueRef.current =
model?.value || props?.defaultValue || defaultValue;
setValue(valueRef.current);
},
};
});
@@ -185,10 +186,8 @@ export const DateRangeFilter = forwardRef(
update = { ...update, end: checkForEndDate(endDate, startDate) };
if (validate(name, date, update)) {
setValue((curr) => ({
...curr,
...update,
}));
valueRef.current = { ...valueRef.current, ...update };
setValue(valueRef.current);
}
};
useEffect(() => {
@@ -241,7 +240,8 @@ export const DateRangeFilter = forwardRef(
className="ag-standard-button ag-filter-apply-panel-button"
onClick={() => {
setError('');
setValue(defaultDates);
valueRef.current = defaultDates;
setValue(valueRef.current);
}}
>
{t('Reset')}
+17 -11
View File
@@ -1,10 +1,17 @@
import type { ChangeEvent } from 'react';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import {
forwardRef,
useEffect,
useImperativeHandle,
useState,
useRef,
} from 'react';
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n';
export const SetFilter = forwardRef((props: IFilterParams, ref) => {
const [value, setValue] = useState<string[]>([]);
const valueRef = useRef(value);
// expose AG Grid Filter Lifecycle callbacks
useImperativeHandle(ref, () => {
@@ -28,29 +35,28 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
},
isFilterActive() {
return value.length !== 0;
return valueRef.current.length !== 0;
},
getModel() {
if (!this.isFilterActive()) {
return null;
}
return { value };
return { value: valueRef.current };
},
setModel(model?: { value: string[] } | null) {
setValue(!model ? [] : model.value);
valueRef.current = !model ? [] : model.value;
setValue(valueRef.current);
},
};
});
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(
event.target.checked
? [...value, event.target.value]
: value.filter((v) => v !== event.target.value)
);
valueRef.current = event.target.checked
? [...value, event.target.value]
: value.filter((v) => v !== event.target.value);
setValue(valueRef.current);
};
useEffect(() => {
@@ -77,7 +83,7 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
<button
type="button"
className="ag-standard-button ag-filter-apply-panel-button"
onClick={() => setValue([])}
onClick={() => setValue((valueRef.current = []))}
>
{t('Reset')}
</button>
@@ -15,8 +15,8 @@ export const useInitialMargin = (
marketId: OrderSubmissionBody['orderSubmission']['marketId'],
order?: OrderSubmissionBody['orderSubmission']
) => {
const { pubKey: partyId } = useVegaWallet();
const commonVariables = { marketId, partyId: partyId || '' };
const { pubKey } = useVegaWallet();
const commonVariables = { marketId, partyId: pubKey || '' };
const { data: marketData } = useDataProvider({
dataProvider: marketDataProvider,
variables: { marketId },
@@ -24,7 +24,7 @@ export const useInitialMargin = (
const { data: activeVolumeAndMargin } = useDataProvider({
dataProvider: volumeAndMarginProvider,
variables: commonVariables,
skip: !partyId,
skip: !pubKey,
});
const { data: marketInfo } = useDataProvider({
dataProvider: marketInfoProvider,
+2
View File
@@ -1,3 +1,5 @@
export * from './use-environment';
export * from './use-links';
export * from './use-node-health';
export * from './use-vega-releases';
export * from './use-vega-release';
File diff suppressed because one or more lines are too long
@@ -0,0 +1,41 @@
import { renderHook, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import {
GITHUB_VEGA_DEV_RELEASES,
GITHUB_VEGA_RELEASES,
} from './use-vega-releases';
import {
GITHUB_VEGA_RELEASES_DATA,
GITHUB_VEGA_DEV_RELEASES_DATA,
} from './mocks/github-releases';
import { useVegaRelease } from './use-vega-release';
describe('useVegaRelease', () => {
beforeEach(() => {
fetchMock.get(GITHUB_VEGA_RELEASES, GITHUB_VEGA_RELEASES_DATA);
fetchMock.get(GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_DEV_RELEASES_DATA);
});
afterEach(() => {
fetchMock.reset();
});
it('should return release information by given tag', async () => {
const { result } = renderHook(() => useVegaRelease('v0.70.1'));
await waitFor(() => {
expect(result.current).toBeTruthy();
expect(result.current?.tagName).toEqual('v0.70.1');
expect(result.current?.htmlUrl).toEqual(
'https://github.com/vegaprotocol/vega/releases/tag/v0.70.1'
);
expect(result.current?.name).toEqual('v0.70.1');
expect(result.current?.isDraft).toBeFalsy();
});
});
it('should return undefined when a release cannot be found', async () => {
const { result } = renderHook(() => useVegaRelease('v0.70.1'));
await waitFor(() => {
expect(result.current).toEqual(undefined);
});
});
});
@@ -0,0 +1,9 @@
import { useMemo } from 'react';
import { useVegaReleases } from './use-vega-releases';
export const useVegaRelease = (tag: string, includeDevReleases = false) => {
const { data } = useVegaReleases();
return useMemo(() => {
return data?.find((r) => r.tagName === tag);
}, [data, tag]);
};
@@ -0,0 +1,58 @@
import { renderHook, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import {
GITHUB_VEGA_DEV_RELEASES,
GITHUB_VEGA_RELEASES,
useVegaReleases,
} from './use-vega-releases';
import {
GITHUB_VEGA_RELEASES_DATA,
GITHUB_VEGA_DEV_RELEASES_DATA,
} from './mocks/github-releases';
describe('useVegaReleases', () => {
afterEach(() => {
fetchMock.reset();
});
it('should return an empty list when request is unsuccessful', async () => {
fetchMock.get(GITHUB_VEGA_RELEASES, 404);
fetchMock.get(GITHUB_VEGA_DEV_RELEASES, 404);
const { result } = renderHook(() => useVegaReleases());
expect(result.current.loading).toBeTruthy();
await waitFor(() => {
expect(result.current.loading).toBeFalsy();
expect(result.current.data).toEqual([]);
});
});
it('should return a list of releases', async () => {
fetchMock.get(GITHUB_VEGA_RELEASES, GITHUB_VEGA_RELEASES_DATA);
fetchMock.get(GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_DEV_RELEASES_DATA);
const { result } = renderHook(() => useVegaReleases());
expect(result.current.loading).toBeTruthy();
await waitFor(() => {
expect(result.current.loading).toBeFalsy();
expect(result.current).not.toEqual([]);
const data = result.current.data;
expect(data?.[0].tagName).toEqual('v0.70.3');
expect(data?.[0].htmlUrl).toEqual(
'https://github.com/vegaprotocol/vega/releases/tag/v0.70.3'
);
expect(data?.[1].tagName).toEqual('v0.70.2');
expect(data?.[2].tagName).toEqual('v0.70.1');
});
});
it('should return a list of releases including dev releases', async () => {
fetchMock.get(GITHUB_VEGA_RELEASES, GITHUB_VEGA_RELEASES_DATA);
fetchMock.get(GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_DEV_RELEASES_DATA);
const { result } = renderHook(() => useVegaReleases(true));
await waitFor(() => {
expect(result.current.data?.length).toBeGreaterThan(30);
expect(fetchMock.calls().length).toEqual(2);
});
});
});
@@ -0,0 +1,124 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { useCallback, useEffect, useState } from 'react';
import z from 'zod';
export const GITHUB_VEGA_RELEASES =
'https://api.github.com/repos/vegaprotocol/vega/releases';
export const GITHUB_VEGA_DEV_RELEASES =
'https://api.github.com/repos/vegaprotocol/vega-dev-releases/releases';
const GithubReleaseSchema = z.object({
url: z.string(),
html_url: z.string(),
id: z.number(),
tag_name: z.string(),
name: z.string().nullable(),
draft: z.boolean(),
created_at: z.string().datetime(),
});
const GithubReleasesSchema = z.array(GithubReleaseSchema);
type ReleaseInfo = {
id: number;
name: string;
tagName: string;
htmlUrl: string;
url: string;
isDraft: boolean;
createdAt: Date;
isDevRelease: boolean;
};
const toReleaseInfo = (
releaseData: z.infer<typeof GithubReleaseSchema>,
isDevRelease: boolean
): ReleaseInfo => ({
id: releaseData.id,
name: releaseData.name || '',
tagName: releaseData.tag_name,
htmlUrl: releaseData.html_url,
url: releaseData.url,
isDraft: releaseData.draft,
createdAt: new Date(releaseData.created_at),
isDevRelease,
});
enum ReleaseFeed {
// @ts-ignore TS18033 - allowed in 5.0
Vega = GITHUB_VEGA_RELEASES,
// @ts-ignore TS18033
VegaDev = GITHUB_VEGA_DEV_RELEASES,
}
const fetchReleases = async (feed: ReleaseFeed) => {
const response = await fetch(String(feed), {
cache: 'force-cache',
});
if (response.ok) {
const json = await response.json();
const data = GithubReleasesSchema.parse(json);
return data.map((d) => toReleaseInfo(d, feed === ReleaseFeed.VegaDev));
}
return [];
};
type State = {
loading: boolean;
data: ReleaseInfo[] | null;
error: Error | null | undefined;
};
/**
* Retrieves a list of vega releases from github.
* First element is the newest.
*/
export const useVegaReleases = (includeDevReleases = false) => {
const [state, setState] = useState<State>({
loading: true,
data: null,
error: null,
});
const fetchData = useCallback(() => {
let mounted = true;
Promise.all(
includeDevReleases
? [fetchReleases(ReleaseFeed.Vega), fetchReleases(ReleaseFeed.VegaDev)]
: [fetchReleases(ReleaseFeed.Vega)]
)
.then(([vega, vegaDev]) => {
if (mounted) {
setState({
loading: false,
data: (vegaDev ? vega.concat(vegaDev) : vega).sort(
(a, b) => b.createdAt.valueOf() - a.createdAt.valueOf()
),
error: null,
});
}
})
.catch((err) => {
if (mounted) {
setState({
loading: false,
data: null,
error: err,
});
}
});
return () => {
mounted = false;
};
}, [includeDevReleases, setState]);
useEffect(() => {
fetchData();
}, [fetchData]);
return state;
};
+2 -2
View File
@@ -38,10 +38,10 @@ export const TransferTooltipCellComponent = ({
);
};
const defaultRangeFilter = { start: formatRFC3339(subDays(Date.now(), 7)) };
const defaultValue = { start: formatRFC3339(subDays(Date.now(), 7)) };
const dateRangeFilterParams = {
maxNextDays: 0,
defaultRangeFilter,
defaultValue,
};
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
@@ -20,6 +20,7 @@ import type {
OrdersQueryVariables,
} from './__generated__/Orders';
import { OrdersDocument, OrdersUpdateDocument } from './__generated__/Orders';
import type { ApolloClient } from '@apollo/client';
export type Order = Omit<OrderFieldsFragment, 'market'> & {
market?: Market;
@@ -81,8 +82,31 @@ const getData = (
): Edge<OrderFieldsFragment>[] =>
responseData?.party?.ordersConnection?.edges || [];
const getDelta = (subscriptionData: OrdersUpdateSubscription) =>
subscriptionData.orders || [];
const getDelta = (
subscriptionData: OrdersUpdateSubscription,
variables: OrdersQueryVariables,
client: ApolloClient<object>
) => {
if (!subscriptionData.orders) {
return [];
}
subscriptionData.orders.forEach((order) => {
client.cache.modify({
id: client.cache.identify({
__typename: 'Order',
id: order.id,
}),
fields: {
price: () => order.price,
size: () => order.size,
remaining: () => order.remaining,
updatedAt: () => order.updatedAt,
status: () => order.status,
},
});
});
return subscriptionData.orders;
};
const getPageInfo = (responseData: OrdersQuery): PageInfo | null =>
responseData.party?.ordersConnection?.pageInfo || null;
@@ -150,7 +174,7 @@ export const update = (
});
};
export const ordersProvider = makeDataProvider<
const ordersProvider = makeDataProvider<
OrdersQuery,
ReturnType<typeof getData>,
OrdersUpdateSubscription,
@@ -165,11 +189,36 @@ export const ordersProvider = makeDataProvider<
pagination: {
getPageInfo,
append,
first: 100,
first: 1000,
},
additionalContext: { isEnlargedTimeout: true },
});
export const activeOrdersProvider = makeDerivedDataProvider<
ReturnType<typeof getData>,
never,
{ partyId: string; marketId?: string }
>(
[
(callback, client, variables) =>
ordersProvider(callback, client, {
partyId: variables.partyId,
filter: {
status: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
},
}),
],
(partsData, variables, prevData, parts, subscriptions) => {
if (!parts[0].isUpdate && subscriptions && subscriptions[0].load) {
subscriptions[0].load();
}
const orders = partsData[0] as ReturnType<typeof getData>;
return variables.marketId
? orders.filter((edge) => variables.marketId === edge.node.market.id)
: orders;
}
);
export const ordersWithMarketProvider = makeDerivedDataProvider<
(OrderEdge | null)[],
Order[],
@@ -193,63 +242,20 @@ export const ordersWithMarketProvider = makeDerivedDataProvider<
combineInsertionData<Order>
);
const hasActiveOrderProviderInternal = makeDataProvider<
OrdersQuery,
boolean,
OrdersUpdateSubscription,
ReturnType<typeof getDelta>,
OrdersQueryVariables
>({
query: OrdersDocument,
subscriptionQuery: OrdersUpdateDocument,
update: (
data: boolean | null,
delta: ReturnType<typeof getDelta>,
reload: () => void
) => {
const orders = delta?.filter(
(order) => !(order.peggedOrder || order.liquidityProvisionId)
);
if (!orders?.length) {
return data;
}
const hasActiveOrders = orders.some(
(order) => order.status === OrderStatus.STATUS_ACTIVE
);
if (hasActiveOrders) {
return true;
} else if (data && !hasActiveOrders) {
reload();
}
return data;
},
getData: (responseData: OrdersQuery | null) => {
const hasActiveOrder = !!responseData?.party?.ordersConnection?.edges?.some(
(order) => !(order.node.peggedOrder || order.node.liquidityProvision)
);
return hasActiveOrder;
},
getDelta,
});
export const hasActiveOrderProvider = makeDerivedDataProvider<
boolean,
never,
{ partyId: string; marketId?: string }
>(
[
(callback, client, { partyId, marketId }) =>
hasActiveOrderProviderInternal(callback, client, {
marketIds: marketId ? [marketId] : undefined,
filter: {
status: [OrderStatus.STATUS_ACTIVE],
excludeLiquidity: true,
},
pagination: {
first: 1,
},
partyId,
} as OrdersQueryVariables),
],
(parts) => parts[0]
);
>([activeOrdersProvider], (parts) => !!parts[0].length);
export const hasAmendableOrderProvider = makeDerivedDataProvider<
boolean,
never,
{ partyId: string; marketId?: string }
>([activeOrdersProvider], (parts) => {
const activeOrders = parts[0] as ReturnType<typeof getData>;
const hasAmendableOrder = activeOrders.some(
(edge) => !(edge.node.liquidityProvision || edge.node.peggedOrder)
);
return hasAmendableOrder;
});
@@ -6,10 +6,12 @@ import { OrderListManager } from './order-list-manager';
export const OrderListContainer = ({
marketId,
onMarketClick,
onOrderTypeClick,
enforceBottomPlaceholder,
}: {
marketId?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
enforceBottomPlaceholder?: boolean;
}) => {
const { pubKey, isReadOnly } = useVegaWallet();
@@ -23,6 +25,7 @@ export const OrderListContainer = ({
partyId={pubKey}
marketId={marketId}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
isReadOnly={isReadOnly}
enforceBottomPlaceholder={enforceBottomPlaceholder}
/>
@@ -8,7 +8,7 @@ import type { GridReadyEvent } from 'ag-grid-community';
import { OrderListTable } from '../order-list/order-list';
import { useOrderListData } from './use-order-list-data';
import { useHasActiveOrder } from '../../order-hooks/use-has-active-order';
import { useHasAmendableOrder } from '../../order-hooks/use-has-amendable-order';
import type { Filter, Sort } from './use-order-list-data';
import { useBottomPlaceholder } from '@vegaprotocol/react-helpers';
import { OrderStatus } from '@vegaprotocol/types';
@@ -16,6 +16,7 @@ import {
normalizeOrderAmendment,
useVegaTransactionStore,
} from '@vegaprotocol/wallet';
import isEqual from 'lodash/isEqual';
import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order, OrderEdge } from '../order-data-provider';
@@ -24,31 +25,23 @@ export interface OrderListManagerProps {
partyId: string;
marketId?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
isReadOnly: boolean;
enforceBottomPlaceholder?: boolean;
}
const CancelAllOrdersButton = ({
onClick,
marketId,
}: {
onClick: (marketId?: string) => void;
marketId?: string;
}) => {
const hasActiveOrder = useHasActiveOrder(marketId);
return hasActiveOrder ? (
<div className="dark:bg-black/75 bg-white/75 h-auto flex justify-end px-[11px] py-2 absolute bottom-0 right-3 rounded">
<Button
variant="primary"
size="sm"
onClick={() => onClick(marketId)}
data-testid="cancelAll"
>
{t('Cancel all')}
</Button>
</div>
) : null;
};
const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => (
<div className="dark:bg-black/75 bg-white/75 h-auto flex justify-end px-[11px] py-2 absolute bottom-0 right-3 rounded">
<Button
variant="primary"
size="sm"
onClick={onClick}
data-testid="cancelAll"
>
{t('Cancel all')}
</Button>
</div>
);
const initialFilter: Filter = {
status: {
@@ -60,6 +53,7 @@ export const OrderListManager = ({
partyId,
marketId,
onMarketClick,
onOrderTypeClick,
isReadOnly,
enforceBottomPlaceholder,
}: OrderListManagerProps) => {
@@ -68,9 +62,10 @@ export const OrderListManager = ({
const scrolledToTop = useRef(false);
const [sort, setSort] = useState<Sort[] | undefined>();
const [filter, setFilter] = useState<Filter | undefined>(initialFilter);
const filterRef = useRef(initialFilter);
const [editOrder, setEditOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create);
const hasActiveOrder = useHasActiveOrder(marketId);
const hasAmendableOrder = useHasAmendableOrder(marketId);
const { data, error, loading, reload } = useOrderListData({
partyId,
@@ -86,12 +81,16 @@ export const OrderListManager = ({
...bottomPlaceholderProps
} = useBottomPlaceholder<Order>({
gridRef,
disabled: !enforceBottomPlaceholder && !isReadOnly && !hasActiveOrder,
disabled: !enforceBottomPlaceholder && !isReadOnly && !hasAmendableOrder,
});
const onFilterChanged = useCallback(
(event: FilterChangedEvent) => {
const updatedFilter = event.api.getFilterModel();
if (isEqual(updatedFilter, filterRef.current)) {
return;
}
filterRef.current = updatedFilter;
if (Object.keys(updatedFilter).length) {
setFilter(updatedFilter);
} else {
@@ -142,16 +141,13 @@ export const OrderListManager = ({
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, [data]);
const cancelAll = useCallback(
(marketId?: string) => {
create({
orderCancellation: {
marketId,
},
});
},
[create]
);
const cancelAll = useCallback(() => {
create({
orderCancellation: {
marketId,
},
});
}, [create, marketId]);
const extractedData =
data && !loading
? data
@@ -171,6 +167,7 @@ export const OrderListManager = ({
cancel={cancel}
setEditOrder={setEditOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
isReadOnly={isReadOnly}
blockLoadDebounceMillis={100}
suppressLoadingOverlay
@@ -188,8 +185,8 @@ export const OrderListManager = ({
/>
</div>
</div>
{!isReadOnly && (
<CancelAllOrdersButton onClick={cancelAll} marketId={marketId} />
{!isReadOnly && hasAmendableOrder && (
<CancelAllOrdersButton onClick={cancelAll} />
)}
{editOrder && (
<OrderEditDialog
@@ -71,17 +71,27 @@ export const useOrderListData = ({
// define variable as const to get type safety, using generic with useMemo resulted in lost type safety
const allVars: OrdersQueryVariables & OrdersUpdateSubscriptionVariables = {
partyId,
filter: {
dateRange: filter?.updatedAt?.value,
status: filter?.status?.value,
timeInForce: filter?.timeInForce?.value,
types: filter?.type?.value,
},
pagination: {
first: 1000,
},
};
if (
filter?.updatedAt?.value ||
filter?.status?.value.length ||
filter?.timeInForce?.value.length ||
filter?.type?.value.length
) {
allVars.filter = {};
if (filter?.updatedAt?.value) {
allVars.filter.dateRange = filter?.updatedAt?.value;
}
if (filter?.status?.value.length) {
allVars.filter.status = filter?.status?.value;
}
if (filter?.timeInForce?.value.length) {
allVars.filter.timeInForce = filter?.timeInForce?.value;
}
if (filter?.type?.value.length) {
allVars.filter.types = filter?.type?.value;
}
}
return allVars;
}, [partyId, filter]);
@@ -16,6 +16,7 @@ import {
negativeClassNames,
positiveClassNames,
MarketNameCell,
OrderTypeCell,
} from '@vegaprotocol/datagrid';
import type {
TypedDataAgGrid,
@@ -31,12 +32,16 @@ export type OrderListTableProps = OrderListProps & {
cancel: (order: Order) => void;
setEditOrder: (order: Order) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
isReadOnly: boolean;
};
export const OrderListTable = memo(
forwardRef<AgGridReact, OrderListTableProps>(
({ cancel, setEditOrder, onMarketClick, ...props }, ref) => {
(
{ cancel, setEditOrder, onMarketClick, onOrderTypeClick, ...props },
ref
) => {
return (
<AgGrid
ref={ref}
@@ -51,7 +56,7 @@ export const OrderListTable = memo(
height: '100%',
}}
getRowId={({ data }) => data.id}
components={{ MarketNameCell }}
components={{ MarketNameCell, OrderTypeCell }}
{...props}
>
<AgGridColumn
@@ -103,17 +108,9 @@ export const OrderListTable = memo(
filterParams={{
set: Schema.OrderTypeMapping,
}}
valueFormatter={({
data: order,
value,
}: VegaValueFormatterParams<Order, 'type'>) => {
if (!order) {
return undefined;
}
if (!value) return '-';
if (order?.peggedOrder) return t('Pegged');
if (order?.liquidityProvision) return t('Liquidity provision');
return Schema.OrderTypeMapping[value];
cellRenderer="OrderTypeCell"
cellRendererParams={{
onClick: onOrderTypeClick,
}}
minWidth={80}
/>
+1 -2
View File
@@ -1,5 +1,4 @@
export * from './__generated__/OrdersSubscription';
export * from './use-has-active-order';
export * from './use-has-amendable-order';
export * from './use-order-update';
export * from './use-pending-orders-volume';
export * from './use-order-store';

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