Compare commits

...
Author SHA1 Message Date
Matthew Russell 1d89660063 chore: re-enabled nx-cloud 2023-06-23 15:59:52 -07:00
Matthew Russell 13f2e51798 chore(ui-toolkit): remove forwardRef as refs weren't being passed (#4160) 2023-06-23 15:36:21 -07:00
Bartłomiej Głownia fc047feeee fix(trading): avoid items remounting in MarketSelector List (#4162) 2023-06-23 15:35:50 -07:00
Bartłomiej Głownia 005455c870 chore(orders): use columnDefs in orders list, fix sorting by size and filled (#4170) 2023-06-23 15:33:25 -07:00
daro-maj 2c2bc391e8 test(trading): chart depth e2e tests (#4171) 2023-06-23 15:13:28 +02:00
m.ray 445a085190 fix(trading): market info number formatting (#4163) 2023-06-23 12:44:18 +01:00
Sam Keen bb1b236cdf feat(governance): show multisig penalty in tooltip (#4140) 2023-06-22 09:43:46 +01:00
Joe Tsang da66b7b20d test(governance): test market details specifications (#4153) 2023-06-22 08:32:33 +01:00
Ciaran McGhie ec12811f72 fix(ui-toolkit): visual fixes for healthbar - % and text colours (#4156) 2023-06-21 11:13:19 -07:00
Ciaran McGhie 180de8cf25 fix(ui-toolkit): healthbar fee decimals (#4152) 2023-06-21 14:51:17 +01:00
m.ray df88e77cdf fix(trading): liquidity provision table quantum formatting (#4114) 2023-06-21 11:23:07 +00:00
Mikołaj Młodzikowski 9441aee8cf feat(ci): update process tested on fairground 2023-06-21 13:05:42 +02:00
Bartłomiej Głownia 2353812834 fix(fills): fix mapping fill update (#4144) 2023-06-21 10:09:08 +01:00
Mikołaj Młodzikowski bba2b3c177 feat(ci): deploy fairground through interstitial page (#4151) 2023-06-21 11:00:27 +02:00
Joe Tsang ae57bd92f4 chore(governance): add e2e test for voting with different pub keys (#4147) 2023-06-21 09:42:36 +01:00
daro-maj 115b642140 test(trading): cover post and reduce only ACs (#4149) 2023-06-21 10:05:31 +02:00
Sam Keen ce3da97a8a fix(governance): proposal deadline validation missing (#4150) 2023-06-20 23:55:45 +01:00
Sam Keen cabd99d3ef feat(governance): new market details improvements (#4106) 2023-06-20 23:53:53 +01:00
Bartłomiej Głownia 597e07608f feat(positions): add liquidation to positions view (#4115) 2023-06-20 21:49:52 +00:00
Matthew Russell e451dc54b3 chore(trading): disable flakey network switcher test 2023-06-20 13:51:14 -07:00
Matthew Russell 9703c3b7a6 fix(trading): deposit table loading state (#4143) 2023-06-20 13:50:10 -07:00
Mikołaj Młodzikowski c22b6f3ce9 feat(ci): add missing fairground record to switch 2023-06-20 17:33:39 +02:00
Bartłomiej Głownia c740c11eaa feat(deal-ticket): improve no collateral validation (#4122) 2023-06-20 08:31:52 -07:00
Mikołaj Młodzikowski d1fabb31a3 feat(ci): fix deployments to testnet 2023-06-20 16:32:27 +02:00
Mikołaj Młodzikowski 94f045a60b fix(ci): port changes for release process fixes for console on frontend 2023-06-20 16:17:13 +02:00
Mikołaj Młodzikowski 1af375bab8 feat(ci): setup fleek deployments for testnet for console (#4138) 2023-06-20 14:49:39 +02:00
Sam Keen 5a48ba4a33 fix(governance): ensure cached finalized vote is only used when pubkey is c… (#4040) 2023-06-19 17:07:30 +00:00
Radosław Szpiech d1fd9184ce test(trading): #3508 tests for Trades ACs (#4139) 2023-06-19 17:07:27 +02:00
Joe Tsang 8f29824f9e chore(governance): update staking acs (#4123) 2023-06-16 16:45:19 +00:00
Joe Tsang fc3c73ad24 chore(governance): un-skip staking test and increase test coverage for smoke tests (#4112) 2023-06-16 14:42:38 +01:00
Bartłomiej Głownia 1027827576 feat(trading): show margin level visualisation in asset breakdown modal (#4048) 2023-06-16 12:28:59 +02:00
Sam Keen 0089920d4c fix(governance): earned by me reward percentage (#4093) 2023-06-16 10:04:03 +01:00
Matthew Russell 35896a9cf5 chore(trading): clear apollo warnings (#4120) 2023-06-16 09:50:22 +01:00
Edd 5a90ebe12e feat(explorer): transaction list filtering 2023-06-15 17:10:17 +01:00
Mikołaj Młodzikowski 6be117086a feat(ci): align releases for all apps (#4111) 2023-06-15 09:03:59 -07:00
daro-maj 9c6dd9f254 test(trading): markets selector ACs update (#4107) 2023-06-15 12:52:18 +02:00
Mikołaj Młodzikowski a675576a52 feat(ci): turn off nx cloud (#4078) 2023-06-15 11:32:04 +01:00
Matthew RussellandBartłomiej Głownia 98d248e02e feat(accounts): accounts table quantum formatting (#4076)
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
2023-06-15 11:31:43 +01:00
Joe Tsang c8d55c6293 chore(governance): fix for withdrawal tests (#4047) 2023-06-15 09:15:52 +01:00
Sam Keen 3467feb3e5 feat(governance): add 'all proposals' link on proposal details page (#4074) 2023-06-14 15:35:12 +01:00
daro-maj bdf1663709 test(trading): update e2e tests for acs - closed all and proposed markets (#4094) 2023-06-14 16:00:04 +02:00
daro-majandBartłomiej Głownia d775573543 test(trading): add e2e tests for liquidity 5002-LIQP (#4082)
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
2023-06-14 14:55:32 +02:00
Mikołaj Młodzikowski c0fdc8d570 feat(ci): add deploy & build info to ui-toolkit README 2023-06-14 13:35:50 +02:00
Mikołaj Młodzikowski a9ebb36b9b feat(ci): deploy ui-toolkit to s3 (#4089) 2023-06-14 10:37:41 +00:00
Radosław Szpiech f6c0082f2d test(trading): #3506 orderbook AC coverage (#4088) 2023-06-14 12:32:33 +02:00
m.ray 931ad75cc9 fix(trading): oracle markets matching (#4080) 2023-06-14 11:06:35 +02:00
m.ray f25a58a3c3 fix(liquidity): update liquidity table row id (#4081) 2023-06-13 14:24:29 -07:00
Mikołaj Młodzikowski 37340b4dc3 feat(ci): modify static content to trigger push 2023-06-13 13:43:54 +02:00
Mikołaj Młodzikowski 17ba3eb351 feat(ci): setup static on s3 (#4075) 2023-06-13 11:29:05 +00:00
Matthew Russell 8f966a4ba6 chore: remove copied dir 2023-06-12 12:38:55 -07:00
Sam Keen cc7f403718 feat(governance): hide proposal details for freeform proposals (#4073) 2023-06-12 12:31:03 +01:00
MaciekandMatthew Russell 9184c2374c fix(markets): back with updating by grid api (#4059)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-06-10 07:49:10 -07:00
MaciekandMatthew Russell 37247a504a fix(trading): update click area in network switcher (#4057)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-06-10 07:48:48 -07:00
Bartłomiej Głownia 2e7a6a6458 chore(trading): cleanup paginated data solution, improve performance (#4036) 2023-06-09 19:02:23 -07:00
Ben bf6c13f523 test(trading): 3502-POSI-e2e-tests (#4064) 2023-06-09 17:37:50 -07:00
Matthew Russell a6f3a08d2f chore(trading,governance,explorer): release v0.20.18-core-0.71.8 2023-06-09 15:50:54 -07:00
Matthew Russell f13e456a29 ci(trading,governance,explorer): remove affected project text manipulation 2023-06-09 10:56:42 -07:00
Sam Keen 5cab040189 fix(governance): temporarily skipped flaky staking tests (#4068) 2023-06-09 16:13:44 +01:00
186 changed files with 7324 additions and 4644 deletions
@@ -9,6 +9,11 @@ jobs:
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
@@ -27,14 +32,16 @@ jobs:
- name: resolve ipfs hashes for release
run: |
echo "Tag name: ${{ github.event.release.tag_name }}"
echo "Name: ${{ github.event.release.name }}"
echo "Description: ${{ github.event.release.body }}"
until docker pull vegaprotocol/trading:${{ github.event.release.tag_name }}; do
echo "Tag: ${{ github.event.release.tag_name }}"
commit="$(git rev-list -n 1 ${{ github.event.release.tag_name }})"
echo "Commit: $commit"
until docker pull vegaprotocol/trading:$commit; do
echo "Image not pushed yet, waiting 60 seconds"
sleep 60
done
docker run --rm vegaprotocol/trading:${{ github.event.release.tag_name }} cat /ipfs-hash > ipfs-hash
docker run --rm vegaprotocol/trading:$commit cat /ipfs-hash > ipfs-hash
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
+19 -8
View File
@@ -6,8 +6,6 @@ on:
- release/*
- develop
- main
tags:
- v*
pull_request:
types:
- opened
@@ -136,22 +134,35 @@ jobs:
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
fi
projects="$(echo $projects_e2e | sed 's|-e2e||g')"
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
# tools are only applicable to check previews or deploy from develop to mainnet
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
# tools are only applicable to check previews or deploy from develop to mainnet
echo "Deploying tools on preview"
preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug")
projects+=' "multisig-signer" '
fi
if [[ "${{ github.ref }}" =~ .*develop$ ]]; then
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
# tools are only applicable to check previews or deploy from develop to mainnet
echo "Deploying tools on s3"
projects+=' "multisig-signer" '
fi
if echo "$affected" | grep -q static; then
echo "static is affected"
echo "Deploying static on s3"
projects+=' "static" '
fi
if echo "$affected" | grep -q ui-toolkit; then
echo "ui-toolkit is affected"
echo "Deploying ui-toolkit on s3"
projects+=' "ui-toolkit" '
fi
fi
projects_e2e=${projects_e2e%?}
projects_e2e=[${projects_e2e// /,}]
projects=${projects%?}
projects=[${projects// /,}]
echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
echo PROJECTS=$projects >> $GITHUB_ENV
+96 -63
View File
@@ -31,6 +31,7 @@ jobs:
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (ghcr)
if: ${{ github.event_name == 'pull_request' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
@@ -39,7 +40,7 @@ jobs:
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
with:
# registry: registry.hub.docker.com
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -74,10 +75,16 @@ jobs:
envName="mainnet"
bucketName="tools.vega.xyz"
fi
if [[ "${{ matrix.app }}" = "static" ]]; then
envName="mainnet"
bucketName="static.vega.xyz"
fi
if [[ "${{ matrix.app }}" = "ui-toolkit" ]]; then
envName="mainnet"
bucketName="ui.vega.rocks"
fi
elif [[ "${{ github.ref }}" =~ .*main$ ]]; then
envName="mainnet"
elif [[ "${{ matrix.app}}" = "trading" ]] && [[ "${{ startsWith(github.ref, 'refs/tags/v') && 'true' || 'false' }}" = "true" ]]; then
envName="mainnet"
fi
if [[ "${envName}" = "mainnet" ]]; then
@@ -106,14 +113,15 @@ jobs:
run: |
flags=""
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
if [[ "${{ env.ENV_NAME }}" != "ops-vega" ]]; then
flags="--env=${{ env.ENV_NAME }}"
fi
flags="--env=${{ env.ENV_NAME }}"
fi
if [ "${{ matrix.app }}" = "trading" ]; then
yarn nx export trading $flags || (yarn install && yarn nx export trading $flags)
DIST_LOCATION=dist/apps/trading/exported
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
NODE_ENV=production yarn nx run ui-toolkit:build-storybook
DIST_LOCATION=dist/storybook/ui-toolkit
else
yarn nx build ${{ matrix.app }} $flags || (yarn install && yarn nx build ${{ matrix.app }} $flags)
DIST_LOCATION=dist/apps/${{ matrix.app }}
@@ -148,7 +156,9 @@ jobs:
- name: Publish dist as docker image (ghcr)
uses: docker/build-push-action@v3
if: ${{ github.event_name == 'pull_request' || (matrix.app == 'trading' && github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') ) }}
continue-on-error: true
id: ghcr-push
if: ${{ github.event_name == 'pull_request' }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
@@ -161,7 +171,9 @@ jobs:
- name: Publish dist as docker image (docker hub)
uses: docker/build-push-action@v3
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
continue-on-error: true
id: dockerhub-push
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
@@ -170,13 +182,41 @@ jobs:
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.ref_name }}
vegaprotocol/${{ matrix.app }}:mainnet
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }}
- name: Publish dist as docker image (ghcr - retry)
uses: docker/build-push-action@v3
if: ${{ steps.ghcr-push.outcome == 'failure' }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
push: true
build-args: |
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
- name: Publish dist as docker image (docker hub - retry)
uses: docker/build-push-action@v3
if: ${{ steps.dockerhub-push.outcome == 'failure' }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
push: true
build-args: |
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }}
# bucket creation in github.com/vegaprotocol/terraform//frontend
- name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master
if: ${{ github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') && ( matrix.app != 'trading' || (matrix.app == 'trading' && !endsWith(github.ref, 'main') ) ) }}
# s3 releases are not happening for trading on mainnet - it's IPFS
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) ) ) }}
with:
args: --acl private --follow-symlinks --delete
env:
@@ -194,22 +234,38 @@ jobs:
number: ${{ github.event.number }}
- name: Trigger fleek deployment
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
# release to ipfs happens only on mainnet (represented by main branch) for trading
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
run: |
# display info about app
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
if echo ${{ github.ref }} | grep -q main; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
elif echo ${{ github.ref }} | grep -q release/testnet; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id status}}"}' \
https://api.fleek.co/graphql
fi
- name: Check out ipfs-redirect
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }}
uses: actions/checkout@v3
with:
repository: 'vegaprotocol/ipfs-redirect'
@@ -217,11 +273,12 @@ jobs:
fetch-depth: '0'
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update console.vega.xyz DNS to redirect to the new console
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
- name: Update interstitial page to point to the new console
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }}
env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
run: |
# set CID
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
@@ -229,52 +286,28 @@ jobs:
new_hash=$(cat ${{ matrix.app }}-ipfs-hash)
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
ls -al ipfs-redirect
echo $new_hash > ipfs-redirect/cidv0.txt
echo $new_cid > ipfs-redirect/cidv1.txt
(
cd ipfs-redirect
# configure git
git status
cat .git/config
git config --global user.email "vega-ci-bot@vega.xyz"
git config --global user.name "vega-ci-bot"
branch_name="update-hash-${{ github.ref }}"
git checkout -b "$branch_name"
# update CID files
if echo ${{ github.ref }} | grep -q main; then
echo $new_hash > cidv0-mainnet.txt
echo $new_cid > cidv1-mainnet.txt
git add cidv0-mainnet.txt cidv1-mainnet.txt
elif echo ${{ github.ref }} | grep -q release/testnet; then
echo $new_hash > cidv0-fairground.txt
echo $new_cid > cidv1-fairground.txt
git add cidv0-fairground.txt cidv1-fairground.txt
fi
# create commit
commit_msg="Automated hash update from ${{ github.ref }}"
git add cidv0.txt cidv1.txt
git commit -m "$commit_msg"
git push -u origin "$branch_name"
pr_url="$(gh pr create --title "${commit_msg}" --body 'automated pull request to update CIDs')"
echo $pr_url
# once auto merge get's enabled on documentation repo let's do follow up
sleep 5
gh pr merge "${pr_url}" --delete-branch --squash --admin
git push -u origin "main"
)
# # Generate console URL
# new_console_url_type=ipfs
# # new_console_url_type=ipns
# new_console_url_domain=cf-ipfs.com
# # new_console_url_domain=dweb.link
# new_console_url="https://${new_cid}.${new_console_url_type}.${new_console_url_domain}/"
# echo "new_console_url=${new_console_url}"
# # Update record in DNSimple
# # docs: https://developer.dnsimple.com/v2/zones/records/#updateZoneRecord
# dnsimple_account_id=84895
# dnsimple_zone_name=console.vega.xyz
# dnsimple_record_id=44409591
# # see: https://dnsimple.com/a/84895/domains/console.vega.xyz/records/44409591/edit
# curl -H 'Authorization: Bearer ${{ secrets.DNSIMPLE_API_TOKEN }}' \
# -H 'Accept: application/json' \
# -H 'Content-Type: application/json' \
# -X PATCH \
# -d "{
# \"content\": \"${new_console_url}\"
# }" \
# https://api.dnsimple.com/v2/${dnsimple_account_id}/zones/${dnsimple_zone_name}/records/${dnsimple_record_id}
+1 -1
View File
@@ -5,7 +5,7 @@ NX_SENTRY_DSN=https://b3a56b03eda842faad731f3ea9dfd1bc@o286262.ingest.sentry.io/
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
NX_VEGA_URL=https://api.vega.community/graphql
NX_VEGA_ENV=MAINNET
NX_BLOCK_EXPLORER=https://be.vega.community/rest/
NX_BLOCK_EXPLORER=https://be.vega.community/rest
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
+1 -1
View File
@@ -70,7 +70,7 @@
"executor": "@nrwl/workspace:run-commands",
"options": {
"commands": [
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.67.3/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.71.4/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
]
}
},
@@ -1,29 +1,13 @@
import WS from 'jest-websocket-mock';
import useWebSocket from 'react-use-websocket';
import {
render,
screen,
fireEvent,
act,
waitFor,
} from '@testing-library/react';
import { TendermintWebsocketContext } from '../../contexts/websocket/tendermint-websocket-context';
import { render, screen } from '@testing-library/react';
import { BlocksRefetch } from './blocks-refetch';
const BlocksRefetchInWebsocketProvider = ({
callback,
mocketLocation,
}: {
callback: () => null;
mocketLocation: string;
}) => {
const contextShape = useWebSocket(mocketLocation);
return (
<TendermintWebsocketContext.Provider value={{ ...contextShape }}>
<BlocksRefetch refetch={callback} />
</TendermintWebsocketContext.Provider>
);
return <BlocksRefetch refetch={callback} />;
};
describe('Blocks refetch', () => {
@@ -32,111 +16,8 @@ describe('Blocks refetch', () => {
const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation);
render(
<BlocksRefetchInWebsocketProvider
callback={() => null}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
expect(screen.getByTestId('new-blocks')).toHaveTextContent('new blocks');
render(<BlocksRefetchInWebsocketProvider callback={() => null} />);
expect(screen.getByTestId('refresh')).toBeInTheDocument();
mocket.close();
});
it('should initiate callback when the button is clicked', async () => {
const mocketLocation = 'wss:localhost:3003';
const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation);
const callback = jest.fn();
render(
<BlocksRefetchInWebsocketProvider
callback={callback}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
const button = screen.getByTestId('refresh');
act(() => {
fireEvent.click(button);
});
expect(callback.mock.calls.length).toEqual(1);
mocket.close();
});
it('should show new blocks as websocket is correctly updated', async () => {
const mocketLocation = 'wss:localhost:3004';
const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation);
render(
<BlocksRefetchInWebsocketProvider
callback={() => null}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
// Ensuring we send an ID equal to the one the client subscribed with.
await waitFor(() => expect(mocket.messages.length).toEqual(1));
// @ts-ignore id on messages
const id = mocket.messages[0].id;
const newBlockMessage = {
id,
result: {
query: "tm.event = 'NewBlock'",
},
};
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
act(() => {
mocket.send(newBlockMessage);
});
expect(screen.getByTestId('new-blocks')).toHaveTextContent('1 new blocks');
act(() => {
mocket.send(newBlockMessage);
});
expect(screen.getByTestId('new-blocks')).toHaveTextContent('2 new blocks');
mocket.close();
});
it('will not show new blocks if websocket has wrong ID', async () => {
const mocketLocation = 'wss:localhost:3005';
const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation);
render(
<BlocksRefetchInWebsocketProvider
callback={() => null}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
// Ensuring we send an ID equal to the one the client subscribed with.
await waitFor(() => expect(mocket.messages.length).toEqual(1));
const newBlockMessageBadId = {
id: 'blahblahblah',
result: {
query: "tm.event = 'NewBlock'",
},
};
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
act(() => {
mocket.send(newBlockMessageBadId);
});
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
mocket.close();
});
});
@@ -1,36 +1,19 @@
import { useState, useEffect } from 'react';
import { useTendermintWebsocket } from '../../hooks/use-tendermint-websocket';
import { t } from '@vegaprotocol/i18n';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { Button, Icon } from '@vegaprotocol/ui-toolkit';
interface BlocksRefetchProps {
refetch: () => void;
}
export const BlocksRefetch = ({ refetch }: BlocksRefetchProps) => {
const [blocksToLoad, setBlocksToLoad] = useState<number>(0);
const { messages } = useTendermintWebsocket({
query: "tm.event = 'NewBlock'",
});
useEffect(() => {
if (messages.length > 0) {
setBlocksToLoad((prev) => prev + 1);
}
}, [messages]);
const refresh = () => {
refetch();
setBlocksToLoad(0);
};
return (
<div className="mb-4">
<span data-testid="new-blocks">{blocksToLoad} new blocks - </span>
<ButtonLink onClick={refresh} data-testid="refresh">
{t('refresh to see latest')}
</ButtonLink>
</div>
<Button onClick={refresh} data-testid="refresh" size="xs">
<Icon name="refresh" className="!align-baseline mr-2" size={3} />
{t('Load new')}
</Button>
);
};
@@ -77,7 +77,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
hide={window.innerWidth <= BREAKPOINT_MD}
valueGetter={({
data,
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
}: VegaValueGetterParams<MarketFieldsFragment>) => {
return data?.state ? MarketStateMapping[data?.state] : '-';
}}
/>
@@ -0,0 +1,24 @@
import { t } from '@vegaprotocol/i18n';
export interface FilterLabelProps {
filters: Set<string>;
}
/**
* Renders the list (currently limited to 1) of filters set by the
* Transaction Filter
*/
export function FilterLabel({ filters }: FilterLabelProps) {
if (!filters || filters.size !== 1) {
return <span className="uppercase">{t('Filter')}</span>;
}
return (
<div>
<span className="uppercase">{t('Filters')}:</span>&nbsp;
<code className="bg-vega-light-150 px-2 rounded-md capitalize">
{Array.from(filters)[0]}
</code>
</div>
);
}
@@ -0,0 +1,164 @@
import { t } from '@vegaprotocol/i18n';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
DropdownMenuSubContent,
Icon,
Button,
} from '@vegaprotocol/ui-toolkit';
import type { Dispatch, SetStateAction } from 'react';
import { FilterLabel } from './tx-filter-label';
// All possible transaction types. Should be generated.
export type FilterOption =
| 'Amend LiquidityProvision Order'
| 'Amend Order'
| 'Batch Market Instructions'
| 'Cancel LiquidityProvision Order'
| 'Cancel Order'
| 'Cancel Transfer Funds'
| 'Chain Event'
| 'Delegate'
| 'Ethereum Key Rotate Submission'
| 'Issue Signatures'
| 'Key Rotate Submission'
| 'Liquidity Provision Order'
| 'Node Signature'
| 'Node Vote'
| 'Proposal'
| 'Protocol Upgrade'
| 'Register new Node'
| 'State Variable Proposal'
| 'Submit Oracle Data'
| 'Submit Order'
| 'Transfer Funds'
| 'Undelegate'
| 'Validator Heartbeat'
| 'Vote on Proposal'
| 'Withdraw';
// Alphabetised list of transaction types to appear at the top level
export const PrimaryFilterOptions: FilterOption[] = [
'Amend LiquidityProvision Order',
'Amend Order',
'Batch Market Instructions',
'Cancel LiquidityProvision Order',
'Cancel Order',
'Cancel Transfer Funds',
'Delegate',
'Liquidity Provision Order',
'Proposal',
'Submit Oracle Data',
'Submit Order',
'Transfer Funds',
'Undelegate',
'Vote on Proposal',
'Withdraw',
];
// Alphabetised list of transaction types to nest under a 'More...' submenu
export const SecondaryFilterOptions: FilterOption[] = [
'Chain Event',
'Ethereum Key Rotate Submission',
'Issue Signatures',
'Key Rotate Submission',
'Node Signature',
'Node Vote',
'Protocol Upgrade',
'Register new Node',
'State Variable Proposal',
'Validator Heartbeat',
];
export const AllFilterOptions: FilterOption[] = [
...PrimaryFilterOptions,
...SecondaryFilterOptions,
];
export interface TxFilterProps {
filters: Set<FilterOption>;
setFilters: Dispatch<SetStateAction<Set<FilterOption>>>;
}
/**
* Renders a structured dropdown menu of all of the available transaction
* types. It allows a user to select one transaction type to view. Later
* it will support multiple selection, but until the API supports that it is
* one or all.
* @param filters null or Set of tranaction types
* @param setFilters A function to update the filters prop
* @returns
*/
export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
return (
<DropdownMenu
modal={false}
trigger={
<DropdownMenuTrigger className="ml-2">
<Button size="xs">
<FilterLabel filters={filters} />
</Button>
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{filters.size > 1 ? null : (
<>
<DropdownMenuCheckboxItem
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
>
{t('Clear filters')} <Icon name="cross" />
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
</>
)}
{PrimaryFilterOptions.map((f) => (
<DropdownMenuCheckboxItem
key={f}
checked={filters.has(f)}
onCheckedChange={() => {
// NOTE: These act like radio buttons until the API supports multiple filters
setFilters(new Set([f]));
}}
id={`radio-${f}`}
>
{f}
<DropdownMenuItemIndicator>
<Icon name="tick-circle" />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
))}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
{t('More Types')}
<Icon name="chevron-right" />
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{SecondaryFilterOptions.map((f) => (
<DropdownMenuCheckboxItem
key={f}
checked={filters.has(f)}
onCheckedChange={(checked) => {
// NOTE: These act like radio buttons until the API supports multiple filters
setFilters(new Set([f]));
}}
id={`radio-${f}`}
>
{f}
<DropdownMenuItemIndicator>
<Icon name="tick-circle" className="inline" />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useRef } from 'react';
import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { t } from '@vegaprotocol/i18n';
@@ -69,6 +69,17 @@ export const TxsInfiniteList = ({
}: TxsInfiniteListProps) => {
const { screenSize } = useScreenDimensions();
const isStacked = ['xs', 'sm'].includes(screenSize);
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
const hasMountedRef = useRef(false);
useEffect(() => {
if (hasMountedRef.current) {
if (infiniteLoaderRef.current) {
infiniteLoaderRef.current.resetloadMoreItemsCache(true);
}
}
hasMountedRef.current = true;
}, [loadMoreTxs]);
if (!txs) {
if (!areTxsLoading) {
@@ -110,6 +121,7 @@ export const TxsInfiniteList = ({
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
ref={infiniteLoaderRef}
>
{({ onItemsRendered, ref }) => (
<List
+9 -1
View File
@@ -33,7 +33,7 @@ export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
// Hacky fix for param as array
let urlAsString = url.toString();
if (filters) {
urlAsString += '&' + filters;
urlAsString += '&' + filters.replace(' ', '%20');
}
return urlAsString;
@@ -65,6 +65,14 @@ export const useTxsData = ({ limit, filters }: IUseTxsData) => {
}
}, [setTxsState, data]);
useEffect(() => {
setTxsState((prev) => ({
txsData: [],
hasMoreTxs: true,
lastCursor: '',
}));
}, [filters]);
const loadTxs = useCallback(() => {
return refetch({
limit: limit,
@@ -5,17 +5,43 @@ import { TxsInfiniteList } from '../../../components/txs';
import { useTxsData } from '../../../hooks/use-txs-data';
import { useDocumentTitle } from '../../../hooks/use-document-title';
const BE_TXS_PER_REQUEST = 20;
import { useState } from 'react';
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
const BE_TXS_PER_REQUEST = 15;
export const TxsList = () => {
useDocumentTitle(['Transactions']);
const { hasMoreTxs, loadTxs, error, txsData, refreshTxs, loading } =
useTxsData({ limit: BE_TXS_PER_REQUEST });
return (
<section className="md:p-2 lg:p-4 xl:p-6">
<section className="md:p-2 lg:p-4 xl:p-6 relative">
<RouteTitle>{t('Transactions')}</RouteTitle>
<BlocksRefetch refetch={refreshTxs} />
<TxsListFiltered />
</section>
);
};
export const TxsListFiltered = () => {
const [filters, setFilters] = useState(new Set(AllFilterOptions));
const f =
filters && filters.size === 1
? `filters[cmd.type]=${Array.from(filters)[0]}`
: '';
const { hasMoreTxs, loadTxs, error, txsData, refreshTxs, loading } =
useTxsData({
limit: BE_TXS_PER_REQUEST,
filters: f,
});
return (
<>
<menu className="mb-2">
<BlocksRefetch refetch={refreshTxs} />
<TxsFilter filters={filters} setFilters={setFilters} />
</menu>
<TxsInfiniteList
hasMoreTxs={hasMoreTxs}
areTxsLoading={loading}
@@ -24,6 +50,6 @@ export const TxsList = () => {
error={error}
className="mb-28"
/>
</section>
</>
);
};
+428 -421
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -49,7 +49,7 @@ module.exports = defineConfig({
vegaTokenContractAddress: '0xF41bD86d462D36b997C0bbb4D97a0a3382f205B7',
vegaTokenAddress: '0x67175Da1D5e966e40D11c4B2519392B2058373de',
txTimeout: { timeout: 70000 },
epochTimeout: { timeout: 6000 },
epochTimeout: { timeout: 10000 },
blockConfirmations: 3,
grepTags: '@regression @smoke @slow',
grepFilterSpecs: true,
@@ -14,9 +14,16 @@ import {
submitUniqueRawProposal,
voteForProposal,
} from '../../../../governance-e2e/src/support/governance.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../../../governance-e2e/src/support/staking.functions';
import {
ensureSpecifiedUnstakedTokensAreAssociated,
stakingPageAssociateTokens,
stakingPageDisassociateAllTokens,
} from '../../../../governance-e2e/src/support/staking.functions';
import { ethereumWalletConnect } from '../../../../governance-e2e/src/support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../../../governance-e2e/src/support/wallet-teardown.functions';
import {
switchVegaWalletPubKey,
vegaWalletSetSpecifiedApprovalAmount,
} from '../../support/wallet-functions';
import type { testFreeformProposal } from '../../support/common-interfaces';
import { formatDateWithLocalTimezone } from '@vegaprotocol/utils';
@@ -44,7 +51,7 @@ describe(
before('connect wallets and set approval limit', function () {
cy.visit('/');
ethereumWalletConnect();
cy.associateTokensToVegaWallet('1');
// cy.associateTokensToVegaWallet('1');
});
beforeEach('visit proposals tab', function () {
@@ -297,5 +304,33 @@ describe(
.and('be.visible');
});
});
it('Able to vote for proposal twice by switching public key', function () {
ensureSpecifiedUnstakedTokensAreAssociated('1');
createRawProposal();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getProposalFromTitle(rawProposal.rationale.title).within(() =>
cy.get(viewProposalButton).click()
);
voteForProposal('for');
cy.contains('You voted: For').should('be.visible');
ethereumWalletConnect();
switchVegaWalletPubKey();
stakingPageAssociateTokens('2');
navigateTo(navigation.proposals);
getProposalFromTitle(rawProposal.rationale.title).within(() =>
cy.get(viewProposalButton).click()
);
cy.getByTestId('you-voted').should('not.exist');
voteForProposal('against');
cy.contains('You voted: Against').should('be.visible');
switchVegaWalletPubKey();
cy.get(proposalVoteProgressForTokens).should('contain.text', '1.00');
// Checking vote status for different public keys is displayed correctly
cy.contains('You voted: For').should('be.visible');
});
switchVegaWalletPubKey();
stakingPageDisassociateAllTokens();
});
}
);
@@ -16,7 +16,7 @@ import {
} from '../../support/proposal.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-functions';
const proposalListItem = '[data-testid="proposals-list-item"]';
const closedProposals = '[data-testid="closed-proposals"]';
@@ -89,8 +89,14 @@ context(
});
cy.get(proposalStatus).should('have.text', 'Open');
voteForProposal('for');
cy.get(proposalStatus, proposalTimeout).should('have.text', 'Passed');
cy.get(proposalStatus, proposalTimeout).should('have.text', 'Enacted');
cy.get(proposalStatus, proposalTimeout)
.should('have.text', 'Passed')
.then(() => {
cy.get(proposalStatus, proposalTimeout).should(
'have.text',
'Enacted'
);
});
cy.get(votesTable).within(() => {
cy.contains('Vote passed.').should('be.visible');
cy.contains('Voting has ended.').should('be.visible');
@@ -32,7 +32,7 @@ import {
import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
} from '../../support/wallet-functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import type { testFreeformProposal } from '../../support/common-interfaces';
@@ -130,7 +130,7 @@ context(
createRawProposal();
});
it.skip('Creating a proposal - proposal rejected - when closing time sooner than system default', function () {
it('Creating a proposal - proposal rejected - when closing time sooner than system default', function () {
goToMakeNewProposal(governanceProposalType.FREEFORM);
enterUniqueFreeFormProposalBody('0.1', generateFreeFormProposalTitle());
cy.get('input:invalid')
@@ -138,7 +138,7 @@ context(
.should('equal', 'Value must be greater than or equal to 1.');
});
it.skip('Creating a proposal - proposal rejected - when closing time later than system default', function () {
it('Creating a proposal - proposal rejected - when closing time later than system default', function () {
goToMakeNewProposal(governanceProposalType.FREEFORM);
enterUniqueFreeFormProposalBody(
'100000',
@@ -8,6 +8,7 @@ import {
} from '../../support/common.functions';
import {
getDownloadedProposalJsonPath,
getProposalFromTitle,
submitUniqueRawProposal,
} from '../../support/governance.functions';
import {
@@ -23,10 +24,11 @@ import {
} from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
switchVegaWalletPubKey,
vegaWalletFaucetAssetsWithoutCheck,
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
} from '../../support/wallet-functions';
const proposalListItem = '[data-testid="proposals-list-item"]';
const openProposals = '[data-testid="open-proposals"]';
@@ -54,7 +56,8 @@ const feedbackError = '[data-testid="Error"]';
const viewProposalBtn = 'view-proposal-btn';
const liquidityVoteStatus = 'liquidity-votes-status';
const tokenVoteStatus = 'token-votes-status';
const proposalTermsSection = 'proposal';
const proposalJsonToggle = 'proposal-json-toggle';
const proposalJsonSection = 'proposal-json';
const vegaWalletPublicKey = Cypress.env('vegaWalletPublicKey');
const fUSDCId =
'816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc';
@@ -185,7 +188,7 @@ context(
cy.get(maxVoteDeadline).click();
cy.get(enactmentDeadlineError).should(
'have.text',
'Proposal will fail if enactment is earlier than the voting deadline'
'The proposal will fail if enactment is earlier than the voting deadline'
);
cy.get(proposalDownloadBtn)
.should('be.visible')
@@ -205,29 +208,51 @@ context(
});
// 3003-PMAN-001
it('Able to submit valid new market proposal', function () {
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.get(newProposalTitle).type('Test new market proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
const newMarketPayload = JSON.stringify(newMarketProposal);
cy.get(newProposalTerms).type(newMarketPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
cy.get(proposalDownloadBtn)
.should('be.visible')
.click()
.then(() => {
cy.wrap(
getDownloadedProposalJsonPath('vega-new-market-proposal-')
).then((filePath) => {
goToMakeNewProposal(governanceProposalType.RAW);
submitUniqueRawProposal({ proposalBody: filePath }); // 3003-PMAN-003
it(
'Able to submit valid new market proposal',
{ tags: '@smoke' },
function () {
const proposalTitle = 'Test new market proposal';
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.get(newProposalTitle).type('Test new market proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
const newMarketPayload = JSON.stringify(newMarketProposal);
cy.get(newProposalTerms).type(newMarketPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
});
cy.get(proposalDownloadBtn)
.should('be.visible')
.click()
.then(() => {
cy.wrap(
getDownloadedProposalJsonPath('vega-new-market-proposal-')
).then((filePath) => {
goToMakeNewProposal(governanceProposalType.RAW);
submitUniqueRawProposal({ proposalBody: filePath }); // 3003-PMAN-003
});
});
navigateTo(navigation.proposals);
getProposalFromTitle(proposalTitle).within(() =>
cy.getByTestId('view-proposal-btn').click()
);
cy.getByTestId('proposal-market-data').within(() => {
cy.getByTestId('proposal-market-data-toggle').click();
cy.contains('Key details').click();
getMarketProposalDetailsFromTable('Name').should(
'have.text',
'Token test market'
);
cy.contains('Settlement asset').click();
// Settlement asset symbol
cy.getByTestId('3_value').should('have.text', 'fBTC');
cy.contains('Oracle').click();
cy.getByTestId('oracle-spec-links').should('have.attr', 'href');
});
}
);
it('Unable to submit new market proposal with missing/invalid fields', function () {
const errorMsg =
@@ -265,8 +290,7 @@ context(
// Will fail if run after 'Able to submit update market proposal and vote for proposal'
// 3002-PROP-022
it('Unable to submit update market proposal without equity-like share in the market', function () {
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click(); // switch to second wallet pub key
switchVegaWalletPubKey();
stakingPageAssociateTokens('1');
goToMakeNewProposal(governanceProposalType.UPDATE_MARKET);
cy.get(newProposalTitle).type('Test update market proposal - rejected');
@@ -296,8 +320,7 @@ context(
closeDialog();
ethereumWalletConnect();
stakingPageDisassociateAllTokens();
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
switchVegaWalletPubKey();
});
// 3002-PROP-020
@@ -451,8 +474,8 @@ context(
.within(() => {
cy.getByTestId(viewProposalBtn).click();
});
cy.getByTestId('proposal-terms-toggle').click();
cy.getByTestId(proposalTermsSection).within(() => {
cy.getByTestId(proposalJsonToggle).click();
cy.getByTestId(proposalJsonSection).within(() => {
cy.contains('USDT Coin').should('be.visible');
cy.contains('USDT').should('be.visible');
});
@@ -498,13 +521,11 @@ context(
.invoke('text')
.should('not.be.empty');
// 3001-VOTE-030 3001-VOTE-031
cy.getByTestId('proposal-terms-toggle').click();
cy.getByTestId('proposal-terms').within(() => {
getProposalInformationFromTable('assetId').should('have.text', assetId);
getProposalInformationFromTable('lifetimeLimit').should(
'have.text',
'10'
);
cy.getByTestId(proposalJsonToggle).click();
cy.getByTestId(proposalJsonSection).within(() => {
cy.contains(assetId).should('be.visible');
cy.contains('lifetimeLimit').should('be.visible');
cy.contains('10').should('be.visible');
});
});
@@ -602,5 +623,13 @@ context(
});
});
}
function getMarketProposalDetailsFromTable(heading: string) {
return cy
.getByTestId('key-value-table-row')
.contains(heading)
.parent()
.siblings();
}
}
);
@@ -21,7 +21,7 @@ import {
} from '../../support/governance.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-functions';
const proposalListItem = 'proposals-list-item';
const openProposals = '[data-testid="open-proposals"]';
@@ -14,7 +14,7 @@ import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
depositAsset,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
} from '../../support/wallet-functions';
const vegaAssetAddress = '0x67175Da1D5e966e40D11c4B2519392B2058373de';
const vegaWalletUnstakedBalance =
@@ -25,7 +25,7 @@ import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
} from '../../support/wallet-functions';
const stakeValidatorListTotalStake = 'total-stake';
const stakeValidatorListTotalShare = 'total-stake-share';
const stakeValidatorListStakePercentage = 'stake-percentage';
@@ -54,7 +54,7 @@ context(
'Staking Tab - with eth and vega wallets connected',
{ tags: '@slow' },
function () {
// 2001-STKE-002, 2001-STKE-032
// 1002-STKE-002, 1002-STKE-032
before('visit staking tab and connect vega wallet', function () {
cy.visit('/');
ethereumWalletConnect();
@@ -77,25 +77,41 @@ context(
}
);
// 1002-STKE-035 1002-STKE-036
it('Unable to stake against a validator with less than minimum and more than associated amount', function () {
ensureSpecifiedUnstakedTokensAreAssociated('3');
verifyUnstakedBalance(3.0);
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletAssociatedBalance('3.0');
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
cy.getByTestId(stakeAddStakeRadioButton, epochTimeout).click({
force: true,
});
cy.getByTestId(stakeTokenAmountInputBox).type('0.001');
cy.getByTestId(stakeTokenSubmitButton).should('be.disabled');
cy.getByTestId(stakeTokenAmountInputBox).clear().type('4');
cy.getByTestId(stakeTokenSubmitButton).should('be.disabled');
});
it('Able to stake against a validator - using vega from wallet', function () {
ensureSpecifiedUnstakedTokensAreAssociated('3');
verifyUnstakedBalance(3.0);
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletAssociatedBalance('3.0');
cy.get('button').contains('Select a validator to nominate').click();
// 2001-STKE-031
// 1002-STKE-031
clickOnValidatorFromList(0);
// 2001-STKE-033, 2001-STKE-034, 2001-STKE-037
// 1002-STKE-033, 1002-STKE-034, 1002-STKE-037
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(1.0);
// 2001-STKE-039
// 1002-STKE-039
verifyStakedBalance(2.0);
verifyNextEpochValue(2.0); // 2001-STKE-016 2001-STKE-038
verifyThisEpochValue(2.0); // 2001-STKE-013
verifyNextEpochValue(2.0); // 1002-STKE-016 1002-STKE-038
verifyThisEpochValue(2.0); // 1002-STKE-013
closeStakingDialog();
navigateTo(navigation.validators);
// 2002-SINC-007
// 2002-SINC-007 1002-STKE-015 1002-STKE-017 1002-STKE-052
validateValidatorListTotalStakeAndShare('0', '3,002.00', '50.02%');
});
@@ -218,7 +234,7 @@ context(
});
});
// 2001-STKE-041
// 1002-STKE-041 1002-STKE-053
it(
'Able to remove part of a stake against a validator',
{ tags: '@smoke' },
@@ -231,11 +247,11 @@ context(
verifyUnstakedBalance(1.0);
closeStakingDialog();
navigateTo(navigation.validators);
// 2001-STKE-040
// 1002-STKE-040
clickOnValidatorFromList(0);
// 2001-STKE-044, 2001-STKE-048
// 1002-STKE-044, 1002-STKE-048
stakingValidatorPageRemoveStake('1');
// 2001-STKE-049
// 1002-STKE-049
verifyNextEpochValue(2.0);
verifyUnstakedBalance(2.0);
verifyStakedBalance(2.0);
@@ -255,7 +271,7 @@ context(
}
);
// 2001-STKE-045
// 1002-STKE-045
it('Able to remove a full stake against a validator', function () {
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
@@ -303,6 +319,7 @@ context(
.and('be.visible');
});
// 1002-STKE-046 1002-STKE-047
it('Unable to remove a stake greater than staked amount next epoch for a validator', function () {
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
@@ -381,6 +398,8 @@ context(
it('Disassociating some tokens - prioritizes unstaked tokens', function () {
vegaWalletSetSpecifiedApprovalAmount('1000');
cy.reload();
ethereumWalletConnect();
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -403,7 +422,7 @@ context(
});
it('Associating wallet tokens - when some already staked - auto stakes tokens to staked validator', function () {
// 2001-STKE-004
// 1002-STKE-004
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -417,7 +436,7 @@ context(
});
it('Associating vesting contract tokens - when some already staked - auto stakes tokens to staked validator', function () {
// 2001-STKE-004
// 1002-STKE-004
stakingPageAssociateTokens('3', { type: 'contract' });
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -431,7 +450,7 @@ context(
});
it('Associating vesting contract tokens - when wallet tokens already staked - auto stakes tokens to staked validator', function () {
// 2001-STKE-004
// 1002-STKE-004
stakingPageAssociateTokens('3', { type: 'wallet' });
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -445,7 +464,7 @@ context(
});
it('Associating tokens - with multiple validators already staked - auto stakes to staked validators - abiding by existing stake ratio', function () {
// 2001-STKE-004
// 1002-STKE-004
stakingPageAssociateTokens('6');
verifyUnstakedBalance(6.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -498,7 +517,7 @@ context(
}
function verifyThisEpochValue(amount: number) {
cy.getByTestId('stake-this-epoch', epochTimeout) // 2001-STKE-013
cy.getByTestId('stake-this-epoch', epochTimeout) // 1002-STKE-013
.contains(amount, epochTimeout)
.should('be.visible');
}
@@ -14,11 +14,12 @@ import {
} from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
switchVegaWalletPubKey,
vegaWalletAssociate,
vegaWalletDisassociate,
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
} from '../../support/wallet-functions';
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
@@ -64,31 +65,38 @@ context(
}
);
it('Able to associate tokens - from wallet', function () {
//1004-ASSO-003
//1004-ASSO-005
//1004-ASSO-009
//1004-ASSO-030
//1004-ASSO-012
//1004-ASSO-013
//1004-ASSO-014
//1004-ASSO-015
//1004-ASSO-030
//0005-ETXN-006
//0005-ETXN-003
//0005-ETXN-005
stakingPageAssociateTokens('2', { skipConfirmation: true });
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
// 0005-ETXN-002
verifyEthWalletAssociatedBalance('2.0');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
});
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0);
});
it(
'Able to associate tokens - from wallet',
{ tags: '@smoke' },
function () {
//1004-ASSO-003
//1004-ASSO-005
//1004-ASSO-009
//1004-ASSO-030
//1004-ASSO-012
//1004-ASSO-013
//1004-ASSO-014
//1004-ASSO-015
//1004-ASSO-030
//0005-ETXN-006
//0005-ETXN-003
//0005-ETXN-005
stakingPageAssociateTokens('2', { skipConfirmation: true });
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
// 0005-ETXN-002
verifyEthWalletAssociatedBalance('2.0');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
2.0
);
});
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0);
}
);
it('Able to disassociate all associated tokens - manually', function () {
// 1004-ASSO-025
@@ -294,8 +302,7 @@ context(
Cypress.env('vegaWalletPublicKey')
);
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
switchVegaWalletPubKey();
cy.get(connectedVegaKey).should(
'have.text',
Cypress.env('vegaWalletPublicKey2')
@@ -5,7 +5,7 @@ import {
waitForSpinner,
} from '../../support/common.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { depositAsset } from '../../support/wallet-teardown.functions';
import { depositAsset } from '../../support/wallet-functions';
const withdraw = 'withdraw';
const withdrawalForm = 'withdraw-form';
@@ -55,6 +55,10 @@ context(
navigateTo(navigation.withdraw);
cy.connectVegaWallet();
ethereumWalletConnect();
cy.getByTestId('currency-title', txTimeout).should(
'contain.text',
usdtName
);
});
it('Able to open withdrawal form with vega wallet connected', function () {
@@ -69,7 +73,10 @@ context(
it('Unable to submit withdrawal with invalid fields', function () {
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(submitWithdrawalButton).click();
cy.getByTestId(formValidationError).should('have.length', 1);
@@ -89,68 +96,75 @@ context(
});
});
it('Able to withdraw asset: -eth wallet connected -withdraw funds button', function () {
// fill in withdrawal form
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(withdrawalThreshold).should(
'have.text',
'100,000.00000T'
);
cy.getByTestId(delayTime).should('have.text', 'None');
cy.getByTestId(amountInput).click().type('120');
cy.getByTestId(submitWithdrawalButton).click();
});
// assert withdrawal request
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'Funds unlocked')
.within(() => {
cy.getByTestId('external-link').should('exist');
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 120.00 tUSDC'
it(
'Able to withdraw asset: -eth wallet connected -withdraw funds button',
{ tags: '@smoke' },
function () {
// fill in withdrawal form
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(withdrawalThreshold).should(
'have.text',
'100,000.00000T'
);
cy.getByTestId(toastCompleteWithdrawal).click();
cy.getByTestId(toastClose).click();
cy.getByTestId(delayTime).should('have.text', 'None');
cy.getByTestId(amountInput).click().type('120');
cy.getByTestId(submitWithdrawalButton).click();
});
// withdrawal complete
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'The withdrawal has been approved.')
.within(() => {
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 120.00 tUSDC'
);
});
cy.getByTestId(toast)
.last(txTimeout)
.should('contain.text', 'Transaction confirmed')
.within(() => {
cy.getByTestId('external-link').should('exist');
});
// withdrawal history for complete withdrawal displayed
cy.get(tableWithdrawnStatus)
.eq(1, txTimeout)
.should('have.text', 'Completed')
.parent()
.within(() => {
cy.get(tableAssetSymbol).should('have.text', usdcSymbol);
cy.get(tableAmount).should('have.text', '120.00');
cy.get(tableReceiverAddress)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/address/');
cy.get(tableWithdrawnTimeStamp).should('not.be.empty');
cy.get(tableTxHash)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/tx/');
});
});
// assert withdrawal request
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'Funds unlocked')
.within(() => {
cy.getByTestId('external-link').should('exist');
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 120.00 tUSDC'
);
cy.getByTestId(toastCompleteWithdrawal).click();
cy.getByTestId(toastClose).click();
});
// withdrawal complete
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'The withdrawal has been approved.')
.within(() => {
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 120.00 tUSDC'
);
});
cy.getByTestId(toast)
.last(txTimeout)
.should('contain.text', 'Transaction confirmed')
.within(() => {
cy.getByTestId('external-link').should('exist');
});
// withdrawal history for complete withdrawal displayed
cy.get(tableWithdrawnStatus)
.eq(1, txTimeout)
.should('have.text', 'Completed')
.parent()
.within(() => {
cy.get(tableAssetSymbol).should('have.text', usdcSymbol);
cy.get(tableAmount).should('have.text', '120.00');
cy.get(tableReceiverAddress)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/address/');
cy.get(tableWithdrawnTimeStamp).should('not.be.empty');
cy.get(tableTxHash)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/tx/');
});
}
);
it('Able to withdraw asset: -eth wallet not connected', function () {
const ethWalletAddress = Cypress.env('ethWalletPublicKey');
@@ -159,7 +173,10 @@ context(
// fill in withdrawal form
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(ethAddressInput).should('be.empty');
cy.getByTestId(amountInput).click().type('110');
cy.getByTestId(submitWithdrawalButton).click();
@@ -219,7 +236,10 @@ context(
it('Should be able to see withdrawal details from toast', function () {
cy.getByTestId(withdraw).click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(withdrawalThreshold).should(
'have.text',
@@ -274,7 +294,10 @@ context(
cy.connectPublicKey(vegaWalletPubKey);
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.get('select').select(usdtSelectValue, { force: true });
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(amountInput).click().type('100');
cy.pause();
@@ -11,7 +11,7 @@ import {
goToMakeNewProposal,
governanceProposalType,
} from '../../support/governance.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-functions';
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey2');
const vegaPubkeyTruncated = Cypress.env('vegaWalletPublicKey2Short');
@@ -5,7 +5,10 @@ import {
verifyPageHeader,
verifyTabHighlighted,
} from '../../support/common.functions';
import { clickOnValidatorFromList } from '../../support/staking.functions';
import {
clickOnValidatorFromList,
waitForBeginningOfEpoch,
} from '../../support/staking.functions';
const guideLink = '[data-testid="staking-guide-link"]';
const validatorTitle = '[data-testid="validator-node-title"]';
@@ -27,7 +30,6 @@ const normalisedVotingPowerToolTip =
'[data-testid="normalised-voting-power-tooltip"]';
const performancePenaltyToolTip = '[data-testid="performance-penalty-tooltip"]';
const overstakedPenaltyToolTip = '[data-testid="overstaked-penalty-tooltip"]';
const totalPenaltyToolTip = '[data-testid="total-penalty-tooltip"]';
const epochCountDown = '[data-testid="epoch-countdown"]';
const stakeNumberRegex = /^\d{1,3}(,\d{3})*(\.\d+)?$/;
@@ -37,206 +39,210 @@ context('Validators Page - verify elements on page', function () {
});
describe('with wallets disconnected', { tags: '@smoke' }, function () {
describe('description section', function () {
it('Should have validators tab highlighted', function () {
verifyTabHighlighted(navigation.validators);
});
it('Should have validators ON VEGA header visible', function () {
verifyPageHeader('Validators');
});
it('Should have Staking Guide link visible', function () {
// 2001-STKE-003
cy.get(guideLink)
.should('be.visible')
.and('have.text', 'Read more about staking on Vega')
.and(
'have.attr',
'href',
'https://docs.vega.xyz/mainnet/concepts/vega-chain/#staking-on-vega'
);
});
it('Should have validators tab highlighted', function () {
verifyTabHighlighted(navigation.validators);
});
describe(
'Should be able to see validator list from the staking page',
{ tags: '@regression' },
function () {
// 2001-STKE-050
it('Should be able to see validator names', function () {
cy.get('[col-id="validator"] > div > span')
.should('have.length.at.least', 1)
.each(($name) => {
cy.wrap($name).should('not.be.empty');
});
});
it('Should be able to see validator stake', function () {
cy.getByTestId('total-stake')
.should('have.length.at.least', 1)
.each(($stake) => {
cy.wrap($stake).should('not.be.empty');
});
});
it('Should have validators ON VEGA header visible', function () {
verifyPageHeader('Validators');
});
it('Should be able to see validator stake tooltip', function () {
cy.getByTestId('total-stake').first().realHover();
it('Should have Staking Guide link visible', function () {
// 1002-STKE-003
cy.get(guideLink)
.should('be.visible')
.and('have.text', 'Read more about staking on Vega')
.and(
'have.attr',
'href',
'https://docs.vega.xyz/mainnet/concepts/vega-chain/#staking-on-vega'
);
});
cy.get(stakedByOperatorToolTip)
.invoke('text')
.should('contain', 'Staked by operator: 3,000.00');
cy.get(stakedByDelegatesToolTip)
.invoke('text')
.should('contain', 'Staked by delegates: 0.00');
cy.get(totalStakedToolTip)
.invoke('text')
.should('contain', 'Total stake: 3,000.00');
});
it('Should be able to see validator normalised voting power', function () {
cy.getByTestId('normalised-voting-power')
.should('have.length.at.least', 1)
.each(($vPower) => {
cy.wrap($vPower).should('not.be.empty');
});
});
it('Should be able to see validator normalised voting power tooltip', function () {
cy.getByTestId('normalised-voting-power').first().realHover();
cy.get(unnormalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Unnormalised voting power: 20.00%');
cy.get(normalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Normalised voting power: 50.00%');
});
// 2002-SINC-018
it('Should be able to see validator total penalties', function () {
cy.getByTestId('total-penalty')
.should('have.length.at.least', 1)
.each(($penalties) => {
cy.wrap($penalties).should('contain.text', '0%');
});
});
it('Should be able to see validator penalties tooltip', function () {
cy.getByTestId('total-penalty').realHover();
cy.get(performancePenaltyToolTip)
.invoke('text')
.should('contain', 'Performance penalty: 0.00%');
cy.get(overstakedPenaltyToolTip)
.invoke('text')
.should('contain', 'Overstaked penalty: 60.00%'); // value not asserted due to #2886
cy.get(totalPenaltyToolTip)
.invoke('text')
.should('contain', 'Total penalties: 60.00%');
});
it('Should be able to see validator pending stake', function () {
cy.getByTestId('total-pending-stake')
.should('have.length.at.least', 1)
.each(($pendingStake) => {
cy.wrap($pendingStake).should('contain.text', '0.00');
});
});
}
);
// 2001-STKE-050
describe(
'Should be able to see static information about a validator',
{ tags: '@smoke' },
function () {
before('connect wallets and click on validator', function () {
cy.connectVegaWallet();
clickOnValidatorFromList(0);
});
// 2001-STKE-006
it('Should be able to see validator name', function () {
cy.get(validatorTitle).should('not.be.empty');
});
// 2001-STKE-007
it('Should be able to see validator id', function () {
cy.get(validatorId).should('not.be.empty');
});
// 2001-STKE-008
it('Should be able to see validator public key', function () {
cy.get(validatorPubKey).should('not.be.empty');
});
// 2001-STKE-010
it('Should be able to see Ethereum address', function () {
cy.get(ethAddressLink)
.should('not.be.empty')
.and('have.attr', 'href');
});
// TODO validators missing url for more information about them 2001-STKE-09
it('Should be able to see validator status', function () {
cy.get(validatorStatus).should('have.text', 'Consensus');
});
// 2001-STKE-012
it('Should be able to see total stake', function () {
cy.get(totalStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see pending stake', function () {
cy.get(pendingStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see staked by operator', function () {
cy.get(stakedByOperator)
.invoke('text')
.should('match', stakeNumberRegex);
});
it('Should be able to see staked by delegates', function () {
cy.get(stakedByDelegates)
.invoke('text')
.should('match', stakeNumberRegex);
});
// 2001-STKE-051
it('Should be able to see stake share in percentage', function () {
cy.get(stakeShare)
.invoke('text')
.then(($stakePercentage) => {
// The pattern must start at a word boundary (\b).
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
// The pattern can be one of the following:
// A percentage value of zero (0%), or
// A non-zero percentage value that can be:
// A single digit (\d) between 0 and 9, or
// A two-digit number between 0 and 99 (\d{1,2}), or
// The number 100.
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
// The pattern must end with a percentage sign (%).
cy.wrap($stakePercentage).should(
'match',
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
);
});
});
// 2001-STKE-011 2002-SINC-001 2002-SINC-002
it('Should be able to see epoch information', function () {
const epochTitle = 'h3';
const nextEpochInfo = 'p';
cy.get(epochCountDown).within(() => {
cy.get(epochTitle).should('not.be.empty');
cy.get(nextEpochInfo).should('contain.text', 'Next epoch');
});
});
}
);
// 1002-STKE-032
it('Should have button to connect vega wallet in validator page', function () {
clickOnValidatorFromList(0);
cy.getByTestId('connect-to-vega-wallet-btn').should('be.visible');
cy.visit('/validators');
});
});
// 1002-STKE-020 1002-STKE-021 1002-STKE-022 1002-STKE-023 1002-STKE-024
describe(
'Should be able to see validator list from the staking page',
{ tags: '@regression' },
function () {
// 1002-STKE-050
it('Should be able to see validator names', function () {
cy.get('[col-id="validator"] > div > span')
.should('have.length.at.least', 1)
.each(($name) => {
cy.wrap($name).should('not.be.empty');
});
});
it('Should be able to see validator stake', function () {
cy.getByTestId('total-stake')
.should('have.length.at.least', 1)
.each(($stake) => {
cy.wrap($stake).should('not.be.empty');
});
});
it('Should be able to see validator stake tooltip', function () {
waitForBeginningOfEpoch();
cy.getByTestId('total-stake').first().realHover();
cy.get(stakedByOperatorToolTip)
.invoke('text')
.should('contain', 'Staked by operator: 3,000.00');
cy.get(stakedByDelegatesToolTip)
.invoke('text')
.should('contain', 'Staked by delegates: 0.00');
cy.get(totalStakedToolTip)
.invoke('text')
.should('contain', 'Total stake: 3,000.00');
});
it('Should be able to see validator normalised voting power', function () {
cy.getByTestId('normalised-voting-power')
.should('have.length.at.least', 1)
.each(($vPower) => {
cy.wrap($vPower).should('not.be.empty');
});
});
it('Should be able to see validator normalised voting power tooltip', function () {
waitForBeginningOfEpoch();
cy.getByTestId('normalised-voting-power').first().realHover();
cy.get(unnormalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Unnormalised voting power: 20.00%');
cy.get(normalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Normalised voting power: 50.00%');
});
// 2002-SINC-018
it('Should be able to see validator total penalties', function () {
cy.getByTestId('total-penalty')
.should('have.length.at.least', 1)
.each(($penalties) => {
cy.wrap($penalties).should('contain.text', '0%');
});
});
it('Should be able to see validator penalties tooltip', function () {
waitForBeginningOfEpoch();
cy.getByTestId('total-penalty').realHover();
cy.get(performancePenaltyToolTip)
.invoke('text')
.should('contain', 'Performance penalty: 0.00%');
cy.get(overstakedPenaltyToolTip)
.invoke('text')
.should('contain', 'Overstaked penalty: 60.00%'); // value not asserted due to #2886
});
it('Should be able to see validator pending stake', function () {
cy.getByTestId('total-pending-stake')
.should('have.length.at.least', 1)
.each(($pendingStake) => {
cy.wrap($pendingStake).should('contain.text', '0.00');
});
});
}
);
// 1002-STKE-050
describe(
'Should be able to see static information about a validator',
{ tags: '@smoke' },
function () {
before('connect wallets and click on validator', function () {
cy.connectVegaWallet();
clickOnValidatorFromList(0);
});
// 1002-STKE-006
it('Should be able to see validator name', function () {
cy.get(validatorTitle).should('not.be.empty');
});
// 1002-STKE-007
it('Should be able to see validator id', function () {
cy.get(validatorId).should('not.be.empty');
});
// 1002-STKE-008
it('Should be able to see validator public key', function () {
cy.get(validatorPubKey).should('not.be.empty');
});
// 1002-STKE-010
it('Should be able to see Ethereum address', function () {
cy.get(ethAddressLink).should('not.be.empty').and('have.attr', 'href');
});
// TODO validators missing url for more information about them 1002-STKE-09
it('Should be able to see validator status', function () {
cy.get(validatorStatus).should('have.text', 'Consensus');
});
// 1002-STKE-012
it('Should be able to see total stake', function () {
cy.get(totalStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see pending stake', function () {
cy.get(pendingStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see staked by operator', function () {
cy.get(stakedByOperator)
.invoke('text')
.should('match', stakeNumberRegex);
});
it('Should be able to see staked by delegates', function () {
cy.get(stakedByDelegates)
.invoke('text')
.should('match', stakeNumberRegex);
});
// 1002-STKE-051
it('Should be able to see stake share in percentage', function () {
cy.get(stakeShare)
.invoke('text')
.then(($stakePercentage) => {
// The pattern must start at a word boundary (\b).
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
// The pattern can be one of the following:
// A percentage value of zero (0%), or
// A non-zero percentage value that can be:
// A single digit (\d) between 0 and 9, or
// A two-digit number between 0 and 99 (\d{1,2}), or
// The number 100.
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
// The pattern must end with a percentage sign (%).
cy.wrap($stakePercentage).should(
'match',
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
);
});
});
// 1002-STKE-011 2002-SINC-001 2002-SINC-002
it('Should be able to see epoch information', function () {
const epochTitle = 'h3';
const nextEpochInfo = 'p';
cy.get(epochCountDown).within(() => {
cy.get(epochTitle).should('not.be.empty');
cy.get(nextEpochInfo).should('contain.text', 'Next epoch');
});
});
}
);
});
@@ -1,7 +1,9 @@
import { truncateByChars } from '@vegaprotocol/utils';
import { waitForSpinner } from '../../support/common.functions';
import { vegaWalletTeardown } from '../../support/wallet-teardown.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
import {
vegaWalletFaucetAssetsWithoutCheck,
vegaWalletTeardown,
} from '../../support/wallet-functions';
const walletContainer = 'aside [data-testid="vega-wallet"]';
const walletHeader = '[data-testid="wallet-header"] h1';
@@ -280,13 +282,6 @@ context(
// 2002-SINC-016
describe('Vega wallet with assets', function () {
const assets = [
{
id: '816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc',
name: 'USDC (fake)',
symbol: 'fUSDC',
amount: '1000000',
expectedAmount: '10.00',
},
{
id: '8566db7257222b5b7ef2886394ad28b938b28680a54a169bbc795027b89d6665',
name: 'DAI (fake)',
@@ -308,6 +303,13 @@ context(
amount: '800000',
expectedAmount: '8.00',
},
{
id: '816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc',
name: 'USDC (fake)',
symbol: 'fUSDC',
amount: '1000000',
expectedAmount: '10.00',
},
];
before('faucet assets to connected vega wallet', function () {
@@ -337,6 +339,7 @@ context(
.parent()
.siblings()
.invoke('text')
.should('have.length.at.least', 4)
.then(parseFloat)
.should('be.gte', parseFloat(expectedAmount));
+1 -2
View File
@@ -5,8 +5,7 @@ import './common.functions.ts';
import './staking.functions.ts';
import './governance.functions.ts';
import './wallet-eth.functions.ts';
import './wallet-teardown.functions.ts';
import './wallet-vega.functions.ts';
import './wallet-functions.ts';
import './proposal.functions.ts';
import 'cypress-mochawesome-reporter/register';
import registerCypressGrep from '@cypress/grep';
@@ -1,5 +1,5 @@
import { closeDialog } from './common.functions';
import { vegaWalletTeardown } from './wallet-teardown.functions';
import { vegaWalletTeardown } from './wallet-functions';
const tokenAmountInputBox = '[data-testid="token-amount-input"]';
const tokenSubmitButton = '[data-testid="token-input-submit-button"]';
@@ -173,3 +173,27 @@ export async function vegaWalletDisassociate(amount: string) {
amount = amount + '0'.repeat(18);
stakingBridgeContract.remove_stake(amount, vegaWalletPubKey);
}
export function vegaWalletFaucetAssetsWithoutCheck(
asset: string,
amount: string,
vegaWalletPublicKey: string
) {
cy.highlight(`Topping up vega wallet with ${asset}, amount: ${amount}`);
cy.exec(
`curl -X POST -d '{"amount": "${amount}", "asset": "${asset}", "party": "${vegaWalletPublicKey}"}' http://localhost:1790/api/v1/mint`
)
.its('stdout')
.then((response) => {
assert.include(
response,
`"success":true`,
'Ensuring curl command was successfully undertaken'
);
});
}
export function switchVegaWalletPubKey() {
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
}
@@ -1,18 +0,0 @@
export function vegaWalletFaucetAssetsWithoutCheck(
asset: string,
amount: string,
vegaWalletPublicKey: string
) {
cy.highlight(`Topping up vega wallet with ${asset}, amount: ${amount}`);
cy.exec(
`curl -X POST -d '{"amount": "${amount}", "asset": "${asset}", "party": "${vegaWalletPublicKey}"}' http://localhost:1790/api/v1/mint`
)
.its('stdout')
.then((response) => {
assert.include(
response,
`"success":true`,
'Ensuring curl command was successfully undertaken'
);
});
}
+11 -2
View File
@@ -596,6 +596,8 @@
"noPercentage": "No percentage",
"proposalJson": "Full proposal JSON",
"proposalDetails": "Proposal details",
"marketSpecification": "Market specification",
"viewMarketJson": "View market JSON",
"proposalDescription": "Description",
"currentlySetTo": "Currently expected to ",
"currently": "currently",
@@ -729,7 +731,11 @@
"ThisWillSetValidationDeadlineTo": "This will set the validation deadline to",
"Hours": "hours",
"ThisWillAdd2MinutesToAllowTimeToConfirmInWallet": "Note: 2 minutes of extra time are added when you choose the minimum value. This gives you time to confirm the proposal in your wallet.",
"ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "Proposal will fail if enactment is earlier than the voting deadline",
"ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "The proposal will fail if enactment is earlier than the voting deadline",
"ProposalWillFailIfEnactmentIsBelowTheMinimumDeadline": "The proposal will fail if enactment deadline is below the minimum",
"ProposalWillFailIfVotingIsBelowTheMinimumDeadline": "The proposal will fail if voting deadline is below the minimum",
"ProposalWillFailIfEnactmentIsAboveTheMaximumDeadline": "The proposal will fail if enactment deadline is above the maximum",
"ProposalWillFailIfVotingIsAboveTheMaximumDeadline": "The proposal will fail if voting deadline is above the maximum",
"SelectAMarketToChange": "Select a market to change",
"MarketName": "Market name",
"MarketCode": "Market code",
@@ -780,6 +786,7 @@
"performancePenalty": "Performance penalty",
"overstaked": "Overstaked",
"overstakedPenalty": "Overstaked penalty",
"multisigPenalty": "Multisig penalty",
"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.",
@@ -824,5 +831,7 @@
"disclaimer5": "No developer of the Vega Governance App accepts any responsibility for, or liability to users in connection with their use of the Vega Governance App.",
"multisigContractLink": "Ethereum Multisig Contract",
"multisigContractIncorrect": "is incorrectly configured. Validator and delegator rewards will be penalised until this is resolved.",
"learnMore": "Learn more"
"learnMore": "Learn more",
"AllValidators": "All validators",
"AllProposals": "All proposals"
}
@@ -0,0 +1,6 @@
import classnames from 'classnames';
export const collapsibleToggleStyles = (toggleState: boolean) =>
classnames('mb-4 transition-transform ease-in-out duration-300', {
'rotate-180': toggleState,
});
@@ -1,9 +1,9 @@
import ReactMarkdown from 'react-markdown';
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
export const ProposalDescription = ({
description,
@@ -12,9 +12,6 @@ export const ProposalDescription = ({
}) => {
const { t } = useTranslation();
const [showDescription, setShowDescription] = useState(false);
const showDescriptionIconClasses = classnames('mb-4', {
'rotate-180': showDescription,
});
return (
<section data-testid="proposal-description">
@@ -24,7 +21,7 @@ export const ProposalDescription = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalDescription')} />
<div className={showDescriptionIconClasses}>
<div className={collapsibleToggleStyles(showDescription)}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -1,8 +1,8 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@@ -13,9 +13,6 @@ export const ProposalJson = ({
}) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return (
<section data-testid="proposal-json">
@@ -25,7 +22,7 @@ export const ProposalJson = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalJson')} />
<div className={showDetailsIconClasses}>
<div className={collapsibleToggleStyles(showDetails)}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -0,0 +1 @@
export * from './proposal-market-data';
@@ -0,0 +1,222 @@
import isEqual from 'lodash/isEqual';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
InstrumentInfoPanel,
KeyDetailsInfoPanel,
LiquidityMonitoringParametersInfoPanel,
LiquidityPriceRangeInfoPanel,
MetadataInfoPanel,
OracleInfoPanel,
PriceMonitoringBoundsInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
} from '@vegaprotocol/markets';
import {
Accordion,
AccordionItem,
Button,
CopyWithTooltip,
Dialog,
Icon,
SyntaxHighlighter,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import { create } from 'zustand';
type MarketDataDialogState = {
isOpen: boolean;
open: () => void;
close: () => void;
};
export const useMarketDataDialogStore = create<MarketDataDialogState>(
(set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
})
);
export const ProposalMarketData = ({
marketData,
}: {
marketData: MarketInfoWithData;
}) => {
const { t } = useTranslation();
const { isOpen, open, close } = useMarketDataDialogStore();
const [showDetails, setShowDetails] = useState(false);
if (!marketData) {
return null;
}
const settlementData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
const terminationData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
return signers.map(({ signer }) => {
return (
(signer.__typename === 'ETHAddress' && signer.address) ||
(signer.__typename === 'PubKey' && signer.key)
);
});
}
return [];
};
return (
<section className="relative" data-testid="proposal-market-data">
<button
onClick={() => setShowDetails(!showDetails)}
data-testid="proposal-market-data-toggle"
>
<div className="flex items-center gap-3">
<SubHeading title={t('marketSpecification')} />
<div className={collapsibleToggleStyles(showDetails)}>
<Icon name="chevron-down" size={8} />
</div>
</div>
</button>
{showDetails && (
<>
<div className="float-right">
<Button onClick={open} data-testid="view-market-json">
{t('viewMarketJson')}
</Button>
</div>
<div className="mb-10">
<Accordion>
<AccordionItem
itemId="key-details"
title={t('Key details')}
content={<KeyDetailsInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="instrument"
title={t('Instrument')}
content={<InstrumentInfoPanel market={marketData} />}
/>
{isEqual(
getSigners(settlementData),
getSigners(terminationData)
) ? (
<AccordionItem
itemId="oracles"
title={t('Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
/>
}
/>
) : (
<>
<AccordionItem
itemId="settlement-oracle"
title={t('Settlement Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
/>
}
/>
<AccordionItem
itemId="termination-oracle"
title={t('Termination Oracle')}
content={
<OracleInfoPanel market={marketData} type="termination" />
}
/>
</>
)}
<AccordionItem
itemId="settlement-asset"
title={t('Settlement asset')}
content={<SettlementAssetInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="metadata"
title={t('Metadata')}
content={<MetadataInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-model"
title={t('Risk model')}
content={<RiskModelInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={<RiskParametersInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-factors"
title={t('Risk factors')}
content={<RiskFactorsInfoPanel market={marketData} />}
/>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
).map((_, triggerIndex) => (
<AccordionItem
itemId={`trigger-${triggerIndex}`}
title={t(`Price monitoring bounds ${triggerIndex + 1}`)}
content={
<PriceMonitoringBoundsInfoPanel
market={marketData}
triggerIndex={triggerIndex}
/>
}
/>
))}
<AccordionItem
itemId="liqudity-monitoring-parameters"
title={t('Liquidity monitoring parameters')}
content={
<LiquidityMonitoringParametersInfoPanel market={marketData} />
}
/>
<AccordionItem
itemId="liquidity-price-range"
title={t('Liquidity price range')}
content={<LiquidityPriceRangeInfoPanel market={marketData} />}
/>
</Accordion>
</div>
</>
)}
<Dialog
title={marketData.tradableInstrument.instrument.code}
open={isOpen}
onChange={(isOpen) => (isOpen ? open() : close())}
size="medium"
dataTestId="market-json-dialog"
>
<CopyWithTooltip text={JSON.stringify(marketData)}>
<button className="bg-vega-dark-100 rounded-sm py-2 px-3 mb-4 text-white">
<span>
<Icon name="duplicate" />
</span>
<span className="ml-2">Copy</span>
</button>
</CopyWithTooltip>
<SyntaxHighlighter data={marketData} />
</Dialog>
</section>
);
};
@@ -1,4 +1,3 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -13,6 +12,7 @@ import { SubHeading } from '../../../../components/heading';
import { useVoteInformation } from '../../hooks';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { ProposalType } from '../proposal/proposal';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@@ -57,10 +57,6 @@ export const ProposalVotesTable = ({
? t('byTokenVote')
: t('byLiquidityVote');
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return (
<>
<button
@@ -69,7 +65,7 @@ export const ProposalVotesTable = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('voteBreakdown')} />
<div className={showDetailsIconClasses}>
<div className={collapsibleToggleStyles(showDetails)}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -1,3 +1,4 @@
import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { Proposal } from './proposal';
@@ -28,9 +29,6 @@ jest.mock('../proposal-change-table', () => ({
jest.mock('../proposal-json', () => ({
ProposalJson: () => <div data-testid="proposal-json"></div>,
}));
jest.mock('../proposal-terms/proposal-terms', () => ({
ProposalTerms: () => <div data-testid="proposal-terms"></div>,
}));
jest.mock('../proposal-votes-table', () => ({
ProposalVotesTable: () => <div data-testid="proposal-votes-table"></div>,
}));
@@ -41,23 +39,38 @@ jest.mock('../list-asset', () => ({
ListAsset: () => <div data-testid="proposal-list-asset"></div>,
}));
const renderComponent = (proposal: ProposalQuery['proposal']) => {
render(
<MemoryRouter>
<Proposal
restData={{}}
proposal={proposal as ProposalQuery['proposal']}
/>
</MemoryRouter>
);
};
it('Renders with data-testid', async () => {
const proposal = generateProposal();
render(
<Proposal restData={{}} proposal={proposal as ProposalQuery['proposal']} />
);
renderComponent(proposal);
expect(await screen.findByTestId('proposal')).toBeInTheDocument();
});
it('Renders with a link back to "all proposals"', async () => {
const proposal = generateProposal();
renderComponent(proposal);
expect(await screen.findByTestId('all-proposals-link')).toBeInTheDocument();
});
it('renders each section', async () => {
const proposal = generateProposal();
render(
<Proposal restData={{}} proposal={proposal as ProposalQuery['proposal']} />
);
renderComponent(proposal);
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
expect(screen.getByTestId('proposal-terms')).toBeInTheDocument();
expect(screen.getByTestId('proposal-votes-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-vote-details')).toBeInTheDocument();
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();
@@ -80,8 +93,7 @@ it('renders whitelist section if proposal is new asset and source is erc20', asy
},
},
});
render(
<Proposal restData={{}} proposal={proposal as ProposalQuery['proposal']} />
);
renderComponent(proposal);
expect(screen.getByTestId('proposal-list-asset')).toBeInTheDocument();
});
@@ -1,18 +1,22 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { AsyncRenderer, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { AsyncRenderer, Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalDescription } from '../proposal-description';
import { ProposalChangeTable } from '../proposal-change-table';
import { ProposalJson } from '../proposal-json';
import { ProposalTerms } from '../proposal-terms';
import { ProposalVotesTable } from '../proposal-votes-table';
import { VoteDetails } from '../vote-details';
import { ListAsset } from '../list-asset';
import Routes from '../../../routes';
import { ProposalMarketData } from '../proposal-market-data';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
export enum ProposalType {
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
@@ -24,11 +28,17 @@ export enum ProposalType {
}
export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
newMarketData?: MarketInfoWithData | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: any;
}
export const Proposal = ({ proposal, restData }: ProposalProps) => {
export const Proposal = ({
proposal,
restData,
newMarketData,
}: ProposalProps) => {
const { t } = useTranslation();
const { params, loading, error } = useNetworkParams([
NetworkParams.governance_proposal_market_minVoterBalance,
NetworkParams.governance_proposal_updateMarket_minVoterBalance,
@@ -81,52 +91,69 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
return (
<AsyncRenderer data={params} loading={loading} error={error}>
<section data-testid="proposal">
<div
className="flex items-center gap-1"
data-testid="all-proposals-link"
>
<Icon name={'chevron-left'} />
<Link className="underline" to={Routes.PROPOSALS}>
{t('AllProposals')}
</Link>
</div>
<ProposalHeader proposal={proposal} isListItem={false} />
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
<div id="details">
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={
proposal.terms.change.source.withdrawThreshold
}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
{proposal.terms.change.__typename !== 'NewMarket' &&
proposal.terms.change.__typename !== 'UpdateMarket' && (
{newMarketData && (
<div className="mb-4">
<ProposalTerms data={proposal.terms} />
<ProposalMarketData marketData={newMarketData} />
</div>
)}
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
</div>
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
<div id="voting">
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</div>
</div>
</section>
</AsyncRenderer>
@@ -224,7 +224,47 @@ describe('Proposal form vote, validation and enactment deadline', () => {
expect(
screen.getByTestId('enactment-before-voting-deadline')
).toHaveTextContent(
'Proposal will fail if enactment is earlier than the voting deadline'
'The proposal will fail if enactment is earlier than the voting deadline'
);
});
it('displays error text if the vote deadline is set earlier than the minimum allowed', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
fireEvent.change(voteDeadlineInput, { target: { value: 0.01 } });
expect(screen.getByTestId('voting-less-than-min')).toHaveTextContent(
'The proposal will fail if voting deadline is below the minimum'
);
});
it('displays error text if the vote deadline is set later than the maximum allowed', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
fireEvent.change(voteDeadlineInput, { target: { value: 100000 } });
expect(screen.getByTestId('voting-greater-than-max')).toHaveTextContent(
'The proposal will fail if voting deadline is above the maximum'
);
});
it('displays error text if the enactment deadline is set earlier than the minimum allowed', () => {
renderComponent();
const enactmentDeadlineInput = screen.getByTestId(
'proposal-enactment-deadline'
);
fireEvent.change(enactmentDeadlineInput, { target: { value: 0.01 } });
expect(screen.getByTestId('enactment-less-than-min')).toHaveTextContent(
'The proposal will fail if enactment deadline is below the minimum'
);
});
it('displays error text if the enactment deadline is set later than the maximum allowed', () => {
renderComponent();
const enactmentDeadlineInput = screen.getByTestId(
'proposal-enactment-deadline'
);
fireEvent.change(enactmentDeadlineInput, { target: { value: 100000 } });
expect(screen.getByTestId('enactment-greater-than-max')).toHaveTextContent(
'The proposal will fail if enactment deadline is above the maximum'
);
});
});
@@ -220,21 +220,41 @@ const EnactmentForm = ({
<span data-testid="enactment-date" className="pl-2">
{getDateTimeFormat().format(deadlineDates.enactment)}
</span>
{deadlines.enactment === minEnactmentHours && (
<span
data-testid="enactment-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.enactment && deadlines.enactment < deadlines.vote && (
<span
data-testid="enactment-before-voting-deadline"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline')}
</span>
{deadlines.enactment && (
<>
{deadlines.enactment === minEnactmentHours && (
<span
data-testid="enactment-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.enactment < deadlines.vote && (
<span
data-testid="enactment-before-voting-deadline"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline')}
</span>
)}
{deadlines.enactment < minEnactmentHours && (
<span
data-testid="enactment-less-than-min"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsBelowTheMinimumDeadline')}
</span>
)}
{deadlines.enactment > maxEnactmentHours && (
<span
data-testid="enactment-greater-than-max"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsAboveTheMaximumDeadline')}
</span>
)}
</>
)}
</p>
)}
@@ -500,13 +520,33 @@ export function ProposalFormVoteAndEnactmentDeadline({
<span data-testid="voting-date" className="pl-2">
{getDateTimeFormat().format(deadlineDates.vote)}
</span>
{deadlines.vote === minVoteHours && (
<span
data-testid="voting-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
{deadlines.vote && (
<>
{deadlines.vote === minVoteHours && (
<span
data-testid="voting-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.vote < minVoteHours && (
<span
data-testid="voting-less-than-min"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfVotingIsBelowTheMinimumDeadline')}
</span>
)}
{deadlines.vote > maxVoteHours && (
<span
data-testid="voting-greater-than-max"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfVotingIsAboveTheMaximumDeadline')}
</span>
)}
</>
)}
</p>
)}
@@ -1,11 +1,10 @@
import { captureMessage } from '@sentry/minimal';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { VoteValue } from '@vegaprotocol/types';
import { useEffect, useState } from 'react';
import { useUserVoteQuery } from './__generated__/Vote';
import type { FinalizedVote } from '@vegaprotocol/proposals';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { FinalizedVote } from '@vegaprotocol/proposals';
export enum VoteState {
NotCast = 'NotCast',
@@ -45,21 +44,23 @@ export function useUserVote(
);
useEffect(() => {
if (finalizedVote?.vote.value) {
if (finalizedVote?.vote.value && finalizedVote.pubKey === pubKey) {
setUserVote(finalizedVote);
} else if (data?.party?.votesConnection?.edges) {
// This sets the vote (if any) when the user first loads the page
setUserVote(
removePaginationWrapper(data?.party?.votesConnection?.edges).find(
({ proposalId: pId }) => proposalId === pId
)
);
} else if (data?.party?.votesConnection?.edges && pubKey) {
const vote = removePaginationWrapper(
data?.party?.votesConnection?.edges
).find(({ proposalId: pId }) => proposalId === pId);
if (vote) {
setUserVote({ ...vote, pubKey });
}
}
}, [
finalizedVote?.vote.value,
data?.party?.votesConnection?.edges,
finalizedVote,
proposalId,
pubKey,
]);
// If user vote changes update the vote state
@@ -94,6 +95,8 @@ export function useUserVote(
return {
voteState,
userVote,
voteDatetime: userVote ? new Date(userVote.vote.datetime) : null,
voteDatetime: userVote?.vote?.datetime
? new Date(userVote.vote.datetime)
: null,
};
}
@@ -7,6 +7,8 @@ import { ProposalNotFound } from '../components/proposal-not-found';
import { useProposalQuery } from './__generated__/Proposal';
import { useFetch } from '@vegaprotocol/react-helpers';
import { ENV } from '../../../config';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketInfoWithDataProvider } from '@vegaprotocol/markets';
export const ProposalContainer = () => {
const params = useParams<{ proposalId: string }>();
@@ -20,15 +22,36 @@ export const ProposalContainer = () => {
skip: !params.proposalId,
});
const {
data: newMarketData,
loading: newMarketLoading,
error: newMarketError,
} = useDataProvider({
dataProvider: marketInfoWithDataProvider,
skipUpdates: true,
variables: {
marketId: data?.proposal?.id || '',
skip: !data?.proposal?.id,
},
});
useEffect(() => {
const interval = setInterval(refetch, 1000);
const interval = setInterval(refetch, 2000);
return () => clearInterval(interval);
}, [refetch]);
return (
<AsyncRenderer loading={loading} error={error} data={data}>
<AsyncRenderer
loading={loading || newMarketLoading}
error={error || newMarketError}
data={newMarketData ? { newMarketData, data } : data}
>
{data?.proposal ? (
<Proposal proposal={data.proposal} restData={restData} />
<Proposal
proposal={data.proposal}
restData={restData}
newMarketData={newMarketData}
/>
) : (
<ProposalNotFound />
)}
@@ -48,15 +48,22 @@ export const EpochIndividualRewards = ({
return removePaginationWrapper(data.party.rewardsConnection.edges);
}, [data]);
const epochRewardSummaries = useMemo(() => {
if (!data?.epochRewardSummaries) return [];
return removePaginationWrapper(data.epochRewardSummaries.edges);
}, [data]);
const epochIndividualRewardSummaries = useMemo(() => {
if (!data?.party) return [];
return generateEpochIndividualRewardsList({
rewards,
epochId,
epochRewardSummaries,
page,
size: EPOCHS_PAGE_SIZE,
});
}, [data?.party, epochId, page, rewards]);
}, [data?.party, epochId, epochRewardSummaries, page, rewards]);
const refetchData = useCallback(
async (toPage?: number) => {
@@ -65,7 +65,11 @@ describe('generateEpochIndividualRewardsList', () => {
it('should return an empty array if no rewards are provided', () => {
expect(
generateEpochIndividualRewardsList({ rewards: [], epochId: 1 })
generateEpochIndividualRewardsList({
rewards: [],
epochId: 1,
epochRewardSummaries: [],
})
).toEqual([
{
epoch: 1,
@@ -78,6 +82,7 @@ describe('generateEpochIndividualRewardsList', () => {
const result = generateEpochIndividualRewardsList({
rewards: [rewardWrongType],
epochId: 1,
epochRewardSummaries: [],
});
expect(result).toEqual([
@@ -92,6 +97,15 @@ describe('generateEpochIndividualRewardsList', () => {
const result = generateEpochIndividualRewardsList({
rewards: [reward1],
epochId: 1,
epochRewardSummaries: [
{
__typename: 'EpochRewardSummary',
epoch: 1,
assetId: 'usd',
amount: '100000',
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
},
],
});
expect(result[0]).toEqual({
@@ -134,7 +148,11 @@ describe('generateEpochIndividualRewardsList', () => {
it('should return an array sorted by epoch descending', () => {
const rewards = [reward1, reward2, reward3, reward4];
const result1 = generateEpochIndividualRewardsList({ rewards, epochId: 2 });
const result1 = generateEpochIndividualRewardsList({
rewards,
epochId: 2,
epochRewardSummaries: [],
});
expect(result1[0].epoch).toEqual(2);
expect(result1[1].epoch).toEqual(1);
@@ -143,6 +161,7 @@ describe('generateEpochIndividualRewardsList', () => {
const result2 = generateEpochIndividualRewardsList({
rewards: reorderedRewards,
epochId: 2,
epochRewardSummaries: [],
});
expect(result2[0].epoch).toEqual(2);
@@ -151,7 +170,11 @@ describe('generateEpochIndividualRewardsList', () => {
it('correctly calculates the total value of rewards for an asset', () => {
const rewards = [reward1, reward4];
const result = generateEpochIndividualRewardsList({ rewards, epochId: 1 });
const result = generateEpochIndividualRewardsList({
rewards,
epochId: 1,
epochRewardSummaries: [],
});
expect(result[0].rewards[0].totalAmount).toEqual('200');
});
@@ -159,7 +182,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, epochId: 2 });
const result = generateEpochIndividualRewardsList({
rewards,
epochId: 2,
epochRewardSummaries: [],
});
expect(result).toEqual([
{
@@ -273,6 +300,7 @@ describe('generateEpochIndividualRewardsList', () => {
const resultPageOne = generateEpochIndividualRewardsList({
rewards,
epochId: 3,
epochRewardSummaries: [],
page: 1,
size: 2,
});
@@ -386,6 +414,7 @@ describe('generateEpochIndividualRewardsList', () => {
const resultPageTwo = generateEpochIndividualRewardsList({
rewards,
epochId: 3,
epochRewardSummaries: [],
page: 2,
size: 2,
});
@@ -429,4 +458,69 @@ describe('generateEpochIndividualRewardsList', () => {
},
]);
});
it('correctly calculates the percentage of two or more rewards by referencing the total rewards amount', () => {
const result = generateEpochIndividualRewardsList({
rewards: [
// reward1 is 100 usd, which is 10% of the total rewards amount
reward1,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '200',
percentageOfTotal: '0.2',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
party: { id: 'blah' },
epoch: { id: '1' },
},
],
epochId: 1,
epochRewardSummaries: [
{
__typename: 'EpochRewardSummary',
epoch: 1,
assetId: 'usd',
amount: '1000',
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
},
],
});
expect(result[0]).toEqual({
epoch: 1,
rewards: [
{
asset: 'USD',
decimals: 6,
totalAmount: '300',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '300',
percentageOfTotal: '30',
},
[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',
},
},
},
],
});
});
});
@@ -1,6 +1,9 @@
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 {
EpochRewardSummaryFieldsFragment,
RewardFieldsFragment,
} from '../home/__generated__/Rewards';
import type { AccountType } from '@vegaprotocol/types';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
@@ -32,11 +35,13 @@ const emptyRowAccountTypes = accountTypes.map((type) => [
export const generateEpochIndividualRewardsList = ({
rewards,
epochId,
epochRewardSummaries,
page = 1,
size = 10,
}: {
rewards: RewardFieldsFragment[];
epochId: number;
epochRewardSummaries: EpochRewardSummaryFieldsFragment[];
page?: number;
size?: number;
}) => {
@@ -54,6 +59,7 @@ export const generateEpochIndividualRewardsList = ({
const epochIndividualRewards = rewards.reduce((acc, reward) => {
const epochId = reward.epoch.id;
const assetName = reward.asset.name;
const assetId = reward.asset.id;
const assetDecimals = reward.asset.decimals;
const rewardType = reward.rewardType;
const amount = reward.amount;
@@ -70,6 +76,14 @@ export const generateEpochIndividualRewardsList = ({
const epoch = acc.get(epochId);
// matchingTotalReward is the total awarded for all users for the reward type in the epoch of the asset
const matchingTotalRewardAmount = epochRewardSummaries.find(
(summary) =>
summary.epoch === Number(epochId) &&
summary.assetId === assetId &&
summary.rewardType === rewardType
)?.amount;
let asset = epoch?.rewards.find((r) => r.asset === assetName);
if (!asset) {
@@ -86,22 +100,24 @@ export const generateEpochIndividualRewardsList = ({
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
} else {
const previousAmount = asset.rewardTypes[rewardType]?.amount;
const previousPercentageOfTotal =
asset.rewardTypes[rewardType]?.percentageOfTotal;
const newAmount = previousAmount
? new BigNumber(previousAmount).plus(amount).toString()
: amount;
asset.rewardTypes[rewardType] = {
amount: previousAmount
? new BigNumber(previousAmount).plus(amount).toString()
: amount,
percentageOfTotal: previousPercentageOfTotal
? new BigNumber(previousPercentageOfTotal)
.plus(percentageOfTotal)
amount: newAmount,
percentageOfTotal: matchingTotalRewardAmount
? new BigNumber(newAmount)
.dividedBy(matchingTotalRewardAmount)
.multipliedBy(100)
.toString()
: percentageOfTotal,
: // this should never be reached, if there's an individual reward there should
// always be a reward total from the api too, but set it as a fallback just in case
percentageOfTotal,
};
}
// totalAmount is the sum of all rewardTypes amounts
// totalAmount is the sum of all individual rewardTypes amounts
asset.totalAmount = Object.values(asset.rewardTypes).reduce(
(sum, rewardType) => {
return new BigNumber(sum).plus(rewardType.amount).toString();
@@ -22,6 +22,13 @@ fragment DelegationFields on Delegation {
epoch
}
fragment EpochRewardSummaryFields on EpochRewardSummary {
epoch
assetId
amount
rewardType
}
query Rewards(
$partyId: ID!
$fromEpoch: Int
@@ -50,13 +57,16 @@ query Rewards(
}
}
}
}
fragment EpochRewardSummaryFields on EpochRewardSummary {
epoch
assetId
amount
rewardType
epochRewardSummaries(
filter: { fromEpoch: $fromEpoch, toEpoch: $toEpoch }
pagination: $rewardsPagination
) {
edges {
node {
...EpochRewardSummaryFields
}
}
}
}
query EpochAssetsRewards(
@@ -7,6 +7,8 @@ export type RewardFieldsFragment = { __typename?: 'Reward', rewardType: Types.Ac
export type DelegationFieldsFragment = { __typename?: 'Delegation', amount: string, epoch: number };
export type EpochRewardSummaryFieldsFragment = { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType };
export type RewardsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
fromEpoch?: Types.InputMaybe<Types.Scalars['Int']>;
@@ -16,9 +18,7 @@ export type RewardsQueryVariables = Types.Exact<{
}>;
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, decimals: number }, 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 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, decimals: number }, 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, epochRewardSummaries?: { __typename?: 'EpochRewardSummaryConnection', edges?: Array<{ __typename?: 'EpochRewardSummaryEdge', node: { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType } } | null> | null } | null };
export type EpochAssetsRewardsQueryVariables = Types.Exact<{
epochRewardSummariesFilter?: Types.InputMaybe<Types.RewardSummaryFilter>;
@@ -102,9 +102,20 @@ export const RewardsDocument = gql`
}
}
}
epochRewardSummaries(
filter: {fromEpoch: $fromEpoch, toEpoch: $toEpoch}
pagination: $rewardsPagination
) {
edges {
node {
...EpochRewardSummaryFields
}
}
}
}
${RewardFieldsFragmentDoc}
${DelegationFieldsFragmentDoc}`;
${DelegationFieldsFragmentDoc}
${EpochRewardSummaryFieldsFragmentDoc}`;
/**
* __useRewardsQuery__
@@ -36,6 +36,7 @@ import {
toBigNum,
} from '@vegaprotocol/utils';
import { VALIDATOR_LOGO_MAP } from './logo-map';
import { getMultisigStatusInfo } from '../../../../lib/get-multisig-status-info';
interface CanonisedConsensusNodeProps {
id: string;
@@ -137,6 +138,10 @@ export const ConsensusValidatorsTable = ({
[totalStake]
);
const multisigStatus = previousEpochData
? getMultisigStatusInfo(previousEpochData)
: undefined;
const allNodesInPreviousEpoch = removePaginationWrapper(
previousEpochData?.epoch.validatorsConnection?.edges
);
@@ -223,6 +228,8 @@ export const ConsensusValidatorsTable = ({
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
? formatNumberPercentage(new BigNumber(userStakeShare), 2)
: undefined,
[ValidatorFields.MULTISIG_ERROR]:
multisigStatus?.showMultisigStatusError,
};
}
);
@@ -332,6 +339,7 @@ export const ConsensusValidatorsTable = ({
data,
decimals,
hideTopThird,
multisigStatus?.showMultisigStatusError,
previousEpochData,
thirdOfTotalStake,
validatorsView,
@@ -39,6 +39,7 @@ export enum ValidatorFields {
STAKED_BY_USER = 'stakedByUser',
PENDING_USER_STAKE = 'pendingUserStake',
USER_STAKE_SHARE = 'userStakeShare',
MULTISIG_ERROR = 'multisigError',
}
export const addUserDataToValidator = (
@@ -326,6 +327,7 @@ interface TotalPenaltiesRendererProps {
overstakedAmount: string;
overstakingPenalty: string;
totalPenalties: string;
multisigError?: boolean;
};
}
@@ -344,10 +346,11 @@ export const TotalPenaltiesRenderer = ({
<div data-testid="overstaked-penalty-tooltip">
{t('overstakedPenalty')}: {data.overstakingPenalty}
</div>
<div data-testid="total-penalty-tooltip">
{t('totalPenalties')}:{' '}
<span className="font-bold">{data.totalPenalties}</span>
</div>
{data.multisigError && (
<div data-testid="multisig-error-tooltip">
{t('multisigPenalty')}: 100%
</div>
)}
</>
}
>
@@ -107,7 +107,7 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
<div className="flex items-center gap-1">
<Icon name={'chevron-left'} />
<Link className="underline" to={Routes.VALIDATORS}>
{t('All validators')}
{t('AllValidators')}
</Link>
</div>
<Heading
@@ -7,31 +7,30 @@ import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import {
getFeeLevels,
sumLiquidityCommitted,
marketLiquidityDataProvider,
lpAggregatedDataProvider,
} from '@vegaprotocol/liquidity';
import type { MarketLpQuery } from '@vegaprotocol/liquidity';
import { marketWithDataProvider } from '@vegaprotocol/markets';
import type { MarketWithData } from '@vegaprotocol/markets';
import { Market } from './market';
import { Header } from './header';
import { LPProvidersGrid } from './providers';
const formatMarket = (data: MarketLpQuery) => {
const formatMarket = (market: MarketWithData) => {
return {
name: data?.market?.tradableInstrument.instrument.name,
name: market?.tradableInstrument.instrument.name,
symbol:
data?.market?.tradableInstrument.instrument.product.settlementAsset
.symbol,
market?.tradableInstrument.instrument.product.settlementAsset.symbol,
settlementAsset:
data?.market?.tradableInstrument.instrument.product.settlementAsset,
targetStake: data?.market?.data?.targetStake,
tradingMode: data?.market?.data?.marketTradingMode,
trigger: data?.market?.data?.trigger,
market?.tradableInstrument.instrument.product.settlementAsset,
targetStake: market?.data?.targetStake,
tradingMode: market?.data?.marketTradingMode,
trigger: market?.data?.trigger,
};
};
export const lpDataProvider = makeDerivedDataProvider(
[marketLiquidityDataProvider, lpAggregatedDataProvider],
[marketWithDataProvider, lpAggregatedDataProvider],
([market, lpAggregatedData]) => ({
market: { ...formatMarket(market) },
liquidityProviders: lpAggregatedData || [],
+3 -1
View File
@@ -1,3 +1,5 @@
# Static
A static CDN for Vega assets
A static CDN for Vega assets: `static.vega.xyz`
prepare assets by running: `yarn nx build static`
@@ -1,5 +1,5 @@
// #region consts
const asset = 'asset';
const assetColId = '[col-id="asset.symbol"]';
const assetDetailsDialog = 'dialog-content';
const assetRow = 'key-value-table-row';
const contractAddress = '7_value';
@@ -108,7 +108,7 @@ beforeEach(() => {
const visitPortfolioAndClickAsset = (assetName: string) => {
cy.visit('/#/portfolio');
cy.getByTestId(asset).contains(assetName).click();
cy.get(assetColId).contains(assetName).click();
};
const testTooltip = (index: number, testId: string, tooltip: string) => {
@@ -0,0 +1,15 @@
describe('charts', { tags: '@smoke' }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId('Depth').click();
});
it('can see market depth chart', () => {
// 6006-DEPC-001
cy.getByTestId('tab-depth').should('be.visible');
cy.get('.depth-chart-module_canvas__260De').should('be.visible');
});
});
@@ -450,3 +450,17 @@ describe('Closed markets', { tags: '@smoke' }, () => {
.should('have.text', 'View on Explorer');
});
});
describe('no closed markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Closed markets"]').click();
});
it('can see no markets message', () => {
// 6001-MARK-034
cy.getByTestId('tab-closed-markets').should('contain.text', 'No markets');
});
});
@@ -1,10 +1,8 @@
const dialogContent = 'dialog-content';
const nodeHealth = 'node-health';
const statusIncidentsLink = 'footer [data-testid=external-link]';
describe('home', { tags: '@regression' }, () => {
before(() => {
cy.clearAllLocalStorage();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
@@ -76,23 +74,14 @@ describe('home', { tags: '@regression' }, () => {
cy.visit('/');
});
// 0006-NETW-002
// 0006-NETW-003
// 0006-NETW-011
it('switch to fairground network and check status & incidents link', () => {
// 0006-NETW-002
// 0006-NETW-003
cy.getByTestId('navigation')
.find('[data-testid="network-switcher"]')
.should('have.text', 'Custom')
.click();
cy.getByTestId('network-item').contains('Fairground testnet').click();
cy.get('[aria-haspopup="menu"]').should('contain.text', 'Fairground');
cy.url().should('include', 'fairground.wtf');
cy.contains('Continue').click();
cy.get(statusIncidentsLink)
.children('span')
.should('have.text', 'Mainnet status & incidents');
cy.get(statusIncidentsLink)
.should('have.attr', 'href')
.and('contain', 'https://blog.vega.xyz/tagged/vega-incident-reports');
cy.getByTestId('network-item').contains('Fairground testnet');
});
});
});
@@ -1,7 +1,10 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { MarketsQuery } from '@vegaprotocol/markets';
import * as Schema from '@vegaprotocol/types';
const rowSelector =
'[data-testid="tab-all-markets"] .ag-center-cols-container .ag-row';
const colInstrumentCode = '[col-id="tradableInstrument.instrument.code"]';
describe('markets all table', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -60,7 +63,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
// 6001-MARK-035
cy.get(rowSelector)
.first()
.find('[col-id="tradableInstrument.instrument.code"]')
.find(colInstrumentCode)
.should('have.text', 'SOLUSD');
// 6001-MARK-036
@@ -155,6 +158,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
});
it('able to open and sort full market list - market page', () => {
// 6001-MARK-064
const ExpectedSortedMarkets = [
'AAPL.MF21',
'BTCUSD.MF21',
@@ -167,8 +171,38 @@ describe('markets all table', { tags: '@smoke' }, () => {
cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name
for (let i = 0; i < ExpectedSortedMarkets.length; i++) {
cy.get(`[row-index=${i}]`)
.find('[col-id="tradableInstrument.instrument.code"]')
.find(colInstrumentCode)
.should('have.text', ExpectedSortedMarkets[i]);
}
});
it('can drag and drop columns', () => {
// 6001-MARK-065
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get(colInstrumentCode)
.realMouseDown()
.realMouseMove(700, 15)
.realMouseUp();
cy.get(colInstrumentCode).should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
});
describe('no all markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
const markets: MarketsQuery = {};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Markets', markets);
});
cy.mockSubscription();
cy.visit('/#/markets/all');
});
it('can see no markets message', () => {
// 6001-MARK-048
cy.getByTestId('tab-all-markets').should('contain.text', 'No markets');
});
});
@@ -182,7 +182,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
.contains('Liquidity monitoring parameters')
.click();
validateMarketDataRow(0, 'Triggering Ratio', '0');
validateMarketDataRow(0, 'Triggering Ratio', '0.7');
validateMarketDataRow(1, 'Time Window', '3,600');
validateMarketDataRow(2, 'Scaling Factor', '10');
});
@@ -0,0 +1,328 @@
import { checkSorting } from '@vegaprotocol/cypress';
import * as Schema from '@vegaprotocol/types';
const liquidityTab = 'Liquidity';
const rowSelector =
'[data-testid="tab-liquidity"] .ag-center-cols-container .ag-row';
const rowSelectorLiquidityActive =
'[data-testid="tab-active"] .ag-center-cols-container .ag-row';
const rowSelectorLiquidityInactive =
'[data-testid="tab-inactive"] .ag-center-cols-container .ag-row';
const marketSummaryBlock = 'header-summary';
const itemValue = 'item-value';
const itemHeader = 'item-header';
const colCommitmentAmount = '[col-id="commitmentAmount"]';
const colAverageEntryValuation = '[col-id="averageEntryValuation"]';
const colEquityLikeShare = '[col-id="equityLikeShare"]';
const colFee = '[col-id="fee"]';
const colCommitmentAmount_1 = '[col-id="commitmentAmount_1"]';
const colBalance = '[col-id="balance"]';
const colStatus = '[col-id="status"]';
const colCreatedAt = '[col-id="createdAt"] button';
const colUpdatedAt = '[col-id="updatedAt"] button';
const headers = [
'Party',
'Commitment (tDAI)',
'Share',
'Proposed fee',
'Market valuation at entry',
'Obligation',
'Supplied',
'Status',
'Created',
'Updated',
];
describe('liquidity table - trading', { tags: '@smoke' }, () => {
before(() => {
cy.mockSubscription();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.visit('/#/markets/market-0');
cy.wait('@MarketData');
cy.getByTestId(liquidityTab).click();
cy.wait('@LiquidityProvisions');
});
it('can see table headers', () => {
// 5002-LIQP-001
cy.getByTestId('tab-liquidity').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
});
});
it('renders liquidity table correctly', () => {
// 5002-LIQP-002
cy.get(rowSelector)
.first()
.find('[col-id="party.id"]')
.should(
'have.text',
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
);
cy.get(rowSelector)
.first()
.find(colCommitmentAmount)
.should('have.text', '4,000.00');
cy.get(rowSelector)
.first()
.find(colEquityLikeShare)
.should('have.text', '100.00%');
cy.get(rowSelector).first().find(colFee).should('have.text', '0.09%');
// 5002-LIQP-013
cy.get(rowSelector)
.first()
.find(colAverageEntryValuation)
.should('have.text', '685,852.93692');
cy.get(rowSelector)
.first()
.find(colCommitmentAmount_1)
.should('have.text', '4,000.00');
cy.get(rowSelector)
.first()
.find(colBalance)
.scrollIntoView()
.should('have.text', '4,000.00');
cy.get(rowSelector).first().find(colStatus).should('have.text', 'Active');
cy.get(rowSelector).first().find(colCreatedAt).should('not.be.empty');
cy.get(rowSelector).first().find(colUpdatedAt).should('not.be.empty');
});
it.skip('liquidity status column should be sorted properly', () => {
// 5002-LIQP-003
const liquidityColDefault = ['Active', 'Pending'];
const liquidityColAsc = ['Active', 'Pending'];
const liquidityColDesc = ['Pending', 'Active'];
checkSorting(
'status',
liquidityColDefault,
liquidityColAsc,
liquidityColDesc
);
});
});
describe('liquidity table view', { tags: '@smoke' }, () => {
before(() => {
cy.mockSubscription();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.visit('/#/liquidity/market-0');
cy.wait('@LiquidityProvisions');
});
it('can see header title', () => {
// 5002-LIQP-004
// 5002-LIQP-005
cy.getByTestId('header-title')
.should('contain.text', 'BTCUSD.MF21 liquidity provision')
.and('contain.text', 'Go to trading');
});
it('can see target stake', () => {
// 5002-LIQP-006
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('target-stake').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Target stake');
cy.getByTestId(itemValue).should('have.text', '10.00 tDAI').realHover();
});
});
cy.getByTestId('tooltip-content').should(
'contain.text',
`The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.`
);
});
it('can see supplied stake', () => {
// 5002-LIQP-007
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('supplied-stake').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Supplied stake');
cy.getByTestId(itemValue).should('have.text', '0.01 tDAI').realHover();
});
});
cy.getByTestId('tooltip-content').should(
'contain.text',
'The current amount of liquidity supplied for this market.'
);
});
it('can see liquidity supplied', () => {
// 5002-LIQP-008
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('liquidity-supplied').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied');
cy.getByTestId('indicator').should('be.visible');
cy.getByTestId(itemValue).should('have.text', '0.10%').realHover();
});
});
});
it('can see market id', () => {
// 5002-LIQP-009
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('liquidity-market-id').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Market ID');
cy.getByTestId(itemValue).should('have.text', 'market-0');
});
});
});
it('can see market id', () => {
// 5002-LIQP-010
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('liquidity-learn-more').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Learn more');
cy.getByTestId(itemValue).should('have.text', 'Providing liquidity');
cy.getByTestId('external-link')
.should('have.attr', 'href')
.and(
'include',
'https://docs.vega.xyz/testnet/concepts/liquidity/provision'
);
});
});
});
describe('liquidity table view', { tags: '@smoke' }, () => {
it('can see table headers', () => {
cy.getByTestId('tab-active').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
});
});
it('renders liquidity active table correctly', () => {
// 5002-LIQP-011
cy.get(rowSelectorLiquidityActive)
.first()
.find('[col-id="party.id"]')
.should(
'have.text',
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
);
cy.get(rowSelectorLiquidityActive)
.first()
.find(colCommitmentAmount)
.should('have.text', '4,000.00');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colEquityLikeShare)
.should('have.text', '100.00%');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colFee)
.should('have.text', '0.09%');
// 5002-LIQP-013
cy.get(rowSelectorLiquidityActive)
.first()
.find(colAverageEntryValuation)
.should('have.text', '685,852.93692');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colCommitmentAmount_1)
.should('have.text', '4,000.00');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colBalance)
.should('have.text', '4,000.00');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colStatus)
.should('have.text', 'Active');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colCreatedAt)
.should('not.be.empty');
cy.get(rowSelectorLiquidityActive)
.first()
.find(colUpdatedAt)
.should('not.be.empty');
});
it('renders liquidity inactive table correctly', () => {
// 5002-LIQP-012
cy.getByTestId('Inactive').click();
cy.get(rowSelectorLiquidityInactive)
.first()
.find('[col-id="party.id"]')
.should(
'have.text',
'cc464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f'
);
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colCommitmentAmount)
.should('have.text', '4,000.00');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colEquityLikeShare)
.should('have.text', '100.00%');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colFee)
.should('have.text', '0.40%');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colAverageEntryValuation)
.should('have.text', '685,852.93692');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colCommitmentAmount_1)
.should('have.text', '4,000.00');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colBalance)
.should('have.text', '2,000.00');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colStatus)
.should('have.text', 'Pending');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colCreatedAt)
.should('not.be.empty');
cy.get(rowSelectorLiquidityInactive)
.first()
.find(colUpdatedAt)
.should('not.be.empty');
});
});
});
@@ -25,6 +25,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
cy.wait('@MarketsCandles');
});
// 6001-MARK-066
it('can toggle the sidebar', () => {
cy.getByTestId('market-selector').should('be.visible');
cy.getByTestId('sidebar-toggle').click();
@@ -67,6 +68,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
.each((item, i) => {
const market = data[i];
// 6001-MARK-021
// 6001-MARK-022
expect(item.find('h3').text()).equals(market.code);
expect(
item.find('[data-testid="market-selector-data-row"]').eq(0).text()
@@ -84,8 +86,19 @@ describe('markets selector', { tags: '@smoke' }, () => {
});
});
// 6001-MARK-27
it('can see all markets link', () => {
// 6001-MARK-026
cy.getByTestId('market-selector').within(() => {
cy.getByTestId('all-markets-link')
.should('be.visible')
.and('have.text', 'All markets')
.and('have.attr', 'href')
.and('contain', '#/markets/all');
});
});
it('can use the filter options', () => {
// 6001-MARK-027
// product type
cy.getByTestId('product-Spot').click();
cy.getByTestId(list).contains('Spot markets coming soon.');
@@ -94,7 +107,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
cy.getByTestId('product-Future').click();
cy.getByTestId(list).find('a').should('have.length', 4);
// 6001-MARK-29
// 6001-MARK-029
cy.getByTestId(searchInput).clear().type('btc');
cy.getByTestId(list).find('a').should('have.length', 2);
cy.getByTestId(list).find('a').eq(1).contains('BTCUSD.MF21');
@@ -103,4 +116,29 @@ describe('markets selector', { tags: '@smoke' }, () => {
cy.getByTestId(searchInput).clear();
cy.getByTestId(list).find('a').should('have.length', 4);
});
it('can sort by by top gaining and top losing market', () => {
// 6001-MARK-030
// 6001-MARK-031
// 6001-MARK-032
// 6001-MARK-033
cy.getByTestId(' sort-trigger').click();
cy.getByTestId('sort-item-Gained')
.contains('Top gaining')
.should('be.visible');
cy.getByTestId('sort-item-Lost')
.contains('Top losing')
.should('be.visible');
cy.getByTestId('sort-item-New')
.contains('New markets')
.should('be.visible');
});
it('can filter by settlement asset', () => {
// 6001-MARK-028
cy.getByTestId('asset-trigger').click();
cy.getByTestId('asset-id-asset-3').contains('tBTC').click();
cy.getByTestId(list).find('a').should('have.length', 1);
cy.getByTestId(list).find('a').eq(0).contains('ETHBTC.QM21');
});
});
@@ -1,16 +1,16 @@
import { checkSorting } from '@vegaprotocol/cypress';
import { aliasGQLQuery, checkSorting } from '@vegaprotocol/cypress';
import type { ProposalsListQuery } from '@vegaprotocol/proposals';
const rowSelector =
'[data-testid="tab-proposed-markets"] .ag-center-cols-container .ag-row';
const colMarketId = '[col-id="market"]';
describe('markets proposed table', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.clearLocalStorage().then(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
});
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
});
it('can see table headers', () => {
@@ -35,10 +35,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
it('renders markets correctly', () => {
// 6001-MARK-049
cy.get(rowSelector)
.first()
.find('[col-id="market"]')
.should('have.text', 'ETHUSD');
cy.get(rowSelector).first().find(colMarketId).should('have.text', 'ETHUSD');
// 6001-MARK-050
cy.get(rowSelector)
@@ -119,6 +116,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
);
});
it('proposed markets tab should be sorted properly', () => {
// 6001-MARK-062
cy.get('[data-testid="Proposed markets"]').click({ force: true });
const marketColDefault = [
'ETHUSD',
@@ -196,4 +194,31 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
];
checkSorting('state', stateColDefault, stateColAsc, stateColDesc);
});
it('can drag and drop columns', () => {
// 6001-MARK-063
cy.get(colMarketId).realMouseDown().realMouseMove(700, 15).realMouseUp();
cy.get(colMarketId).should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
});
describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
const proposal: ProposalsListQuery = {};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ProposalsList', proposal);
});
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
});
it('can see no markets message', () => {
// 6001-MARK-061
cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets');
});
});
@@ -0,0 +1,102 @@
const orderbookTab = 'Orderbook';
const orderbookTable = 'tab-orderbook';
const askPrice = 'price-9894585';
const bidPrice = 'price-9889001';
const askVolume = 'ask-vol-9894585';
const bidVolume = 'bid-vol-9889001';
const askCumulative = 'cumulative-vol-9894585';
const bidCumulative = 'cumulative-vol-9889001';
const midPrice = 'middle-mark-price-4612690000';
const priceResolution = 'resolution';
const dealTicketPrice = 'order-price';
const resPrice = 'price-990';
describe('order book', { tags: '@smoke' }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
beforeEach(() => {
cy.mockTradingPage();
});
it('show order book', () => {
// 6003-ORDB-001
// 6003-ORDB-002
cy.getByTestId(orderbookTab).click();
cy.getByTestId(orderbookTable).should('be.visible');
cy.getByTestId(orderbookTable).should('not.be.empty');
});
it('show orders prices', () => {
// 6003-ORDB-003
cy.getByTestId(askPrice).should('have.text', '98.94585');
cy.getByTestId(bidPrice).should('have.text', '98.89001');
});
it('show prices volumes', () => {
// 6003-ORDB-004
cy.getByTestId(askVolume).should('have.text', '1');
cy.getByTestId(bidVolume).should('have.text', '1');
});
it('show prices cumulative volumes', () => {
// 6003-ORDB-005
cy.getByTestId(askCumulative).should('have.text', '39');
cy.getByTestId(bidCumulative).should('have.text', '7');
});
it('show mid price', () => {
// 6003-ORDB-006
cy.getByTestId(midPrice).should('have.text', '46,126.90');
});
it('sort prices descending', () => {
// 6003-ORDB-007
const prices: number[] = [];
cy.getByTestId(orderbookTable).within(() => {
cy.get('[data-testid*=price]')
.each(($el) => {
prices.push(Number($el.text()));
})
.then(() => {
expect(prices).to.deep.equal(prices.sort((a, b) => b - a));
});
});
});
it('copy price to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(askPrice).click();
cy.getByTestId(dealTicketPrice).should('have.value', '98.94585');
});
it('change price resolution', () => {
// 6003-ORDB-008
const resolutions = [
'0.00000',
'0.0000',
'0.000',
'0.00',
'0.0',
'0',
'10',
'100',
'1,000',
'10,000',
];
cy.getByTestId(priceResolution)
.find('option')
.each(($el, index) => {
expect($el.text()).to.equal(resolutions[index]);
});
cy.getByTestId(priceResolution).select('0.0');
cy.getByTestId(resPrice).should('have.text', '99.0');
cy.getByTestId(askPrice).should('not.exist');
cy.getByTestId(bidPrice).should('not.exist');
});
});
@@ -20,6 +20,7 @@ describe('accounts', { tags: '@smoke' }, () => {
// 7001-COLL-006
// 7001-COLL-007
// 1003-TRAN-001
// 7001-COLL-012
const tradingAccountRowId = '[row-id="t-0"]';
cy.getByTestId('Collateral').click();
@@ -34,28 +35,23 @@ describe('accounts', { tags: '@smoke' }, () => {
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="used"]')
.should('have.text', '1.010.00%');
.should('have.text', '1.01' + '1.00%');
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="available"]')
.should('have.text', '100,000.00');
.should('have.text', '100.00');
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="total"]')
.should('have.text', '100,001.01');
.should('have.text', '101.01');
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="accounts-actions"]')
.should('have.text', '');
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="total"]')
.should('have.text', '100,001.01');
cy.getByTestId('tab-accounts')
.get('[col-id="accounts-actions"]')
.find('[data-testid="dropdown-menu"]')
@@ -101,7 +97,7 @@ describe('accounts', { tags: '@smoke' }, () => {
'Liquidity provision fee reward account balance',
'Market proposer reward account balance',
];
cy.getByTestId('asset').contains('tEURO').click();
cy.get('[col-id="asset.symbol"]').contains('tEURO').click();
cy.get('[data-testid$="_label"]').should('have.length', 16);
cy.get('[data-testid$="_label"]').each((element, index) => {
cy.wrap(element).should('have.text', titles[index]);
@@ -112,8 +108,8 @@ describe('accounts', { tags: '@smoke' }, () => {
it('should open usage breakdown dialog when clicked on used', () => {
// 7001-COLL-009
cy.getByTestId('breakdown').contains('1.01').click();
const headers = ['Market', 'Account type', 'Balance'];
cy.get('[col-id="used"]').contains('1.01').click();
const headers = ['Market', 'Account type', 'Balance', 'Margin health'];
cy.getByTestId('usage-breakdown').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
@@ -132,12 +128,14 @@ describe('accounts', { tags: '@smoke' }, () => {
cy.wrap(btn).click();
});
}
cy.contains('Loading...').should('not.exist');
});
// 7001-COLL-010
it('sorting by asset', () => {
cy.getByTestId('Collateral').click();
const marketsSortedDefault = ['tBTC', 'tEURO', 'tDAI', 'tBTC'];
const marketsSortedAsc = ['tBTC', 'tBTC', 'tDAI', 'tEURO'];
const marketsSortedDesc = ['tEURO', 'tDAI', 'tBTC', 'tBTC'];
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
checkSorting(
'asset.symbol',
marketsSortedDefault,
@@ -160,12 +158,8 @@ describe('accounts', { tags: '@smoke' }, () => {
'1,000.00002',
'1,000.01',
];
const marketsSortedDesc = [
'1,000.01',
'1,000.00002',
'1,000.00001',
'1,000.00',
];
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
checkSorting(
'total',
marketsSortedDefault,
@@ -176,24 +170,22 @@ describe('accounts', { tags: '@smoke' }, () => {
it('sorting by used', () => {
cy.getByTestId('Collateral').click();
// concat actual value with percentage value
// as cypress will pick up the entire cell contes
// textContent
const marketsSortedDefault = [
'0.000.00%',
'0.010.00%',
'0.000.00%',
'0.000.00%',
'0.00' + '0.00%',
'0.01' + '0.00%',
'0.00' + '0.00%',
'0.00' + '0.00%',
];
const marketsSortedAsc = [
'0.000.00%',
'0.000.00%',
'0.000.00%',
'0.010.00%',
];
const marketsSortedDesc = [
'0.010.00%',
'0.000.00%',
'0.000.00%',
'0.000.00%',
'0.00' + '0.00%',
'0.00' + '0.00%',
'0.00' + '0.00%',
'0.01' + '0.00%',
];
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
checkSorting(
'used',
marketsSortedDefault,
@@ -202,29 +194,24 @@ describe('accounts', { tags: '@smoke' }, () => {
);
});
it('sorting by total', () => {
it('sorting by available', () => {
cy.getByTestId('Collateral').click();
const marketsSortedDefault = [
'1,000.00002',
'1,000.01',
'1,000.00',
'1,000.00',
'1,000.00001',
];
const marketsSortedAsc = [
'1,000.00',
'1,000.00001',
'1,000.00002',
'1,000.01',
];
const marketsSortedDesc = [
'1,000.01',
'1,000.00002',
'1,000.00001',
'1,000.00',
'1,000.00001',
'1,000.00002',
];
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
checkSorting(
'total',
'available',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
@@ -81,7 +81,7 @@ describe(
cy.visit('/#/markets/market-0');
});
it('must display that market is not accepting orders', function () {
cy.getByTestId('dealticket-error-message-summary').should(
cy.getByTestId('deal-ticket-error-message-summary').should(
'have.text',
`This market is ${marketState
.split('_')
@@ -45,7 +45,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('dealticket-error-message-expiry').should(
cy.getByTestId('deal-ticket-error-message-expiry').should(
'have.text',
'The expiry date that you have entered appears to be in the past'
);
@@ -57,7 +57,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId('dealticket-error-message-price-limit').should(
cy.getByTestId('deal-ticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
);
@@ -79,7 +79,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(orderSizeField).clear().type('1.234');
// 7002-SORD-060
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('dealticket-error-message-size-market').should(
cy.getByTestId('deal-ticket-error-message-size-market').should(
'have.text',
'Size must be whole numbers for this market'
);
@@ -88,7 +88,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must warn if order size is set to 0', function () {
cy.getByTestId(orderSizeField).clear().type('0');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('dealticket-error-message-size-market').should(
cy.getByTestId('deal-ticket-error-message-size-market').should(
'have.text',
'Size cannot be lower than 1'
);
@@ -96,15 +96,29 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must have total margin available', () => {
// 7001-COLL-011
cy.getByTestId('tab-ticket')
.find('.text-xs')
.eq(5)
.within(() => {
cy.get('[data-state="closed"]').should(
'have.text',
'Total margin available100,000.01 tDAI'
);
});
cy.getByTestId('deal-ticket-fee-total-margin-available').within(() => {
cy.get('[data-state="closed"]').should(
'have.text',
'Total margin available100.01 tDAI'
);
});
});
it('must have current margin allocation', () => {
cy.getByTestId('deal-ticket-fee-current-margin-allocation').within(() => {
cy.get('[data-state="closed"]:first').should(
'have.text',
'Current margin allocation'
);
});
});
it('should open usage breakdown dialog when clicked on current margin allocation', () => {
cy.getByTestId('deal-ticket-fee-current-margin-allocation').within(() => {
cy.get('button').click();
});
cy.getByTestId('usage-breakdown').should('exist');
cy.getByTestId('dialog-close').click();
});
});
});
@@ -1,6 +1,10 @@
import * as Schema from '@vegaprotocol/types';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { accountsQuery, amendGeneralAccountBalance } from '@vegaprotocol/mock';
import {
accountsQuery,
amendGeneralAccountBalance,
amendMarginAccountBalance,
} from '@vegaprotocol/mock';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
@@ -12,8 +16,9 @@ describe(
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
const accounts = accountsQuery();
amendGeneralAccountBalance(accounts, 'market-0', '0');
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '0');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
@@ -23,10 +28,15 @@ describe(
});
it('should show an error if your balance is zero', () => {
const accounts = accountsQuery();
amendMarginAccountBalance(accounts, 'market-0', '0');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
// 7002-SORD-060
cy.getByTestId('place-order').should('be.enabled');
// 7002-SORD-003
cy.getByTestId('dealticket-error-message-zero-balance').should(
cy.getByTestId('deal-ticket-error-message-zero-balance').should(
'have.text',
'You need ' +
'tDAI' +
@@ -40,8 +50,9 @@ describe(
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
const accounts = accountsQuery();
amendGeneralAccountBalance(accounts, 'market-0', '1');
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '1');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
@@ -54,11 +65,11 @@ describe(
// 7002-SORD-003
// warning should show immediately
cy.getByTestId('dealticket-warning-margin').should(
cy.getByTestId('deal-ticket-warning-margin').should(
'contain.text',
'You may not have enough margin available to open this position'
);
cy.getByTestId('dealticket-warning-margin').should(
cy.getByTestId('deal-ticket-warning-margin').should(
'contain.text',
'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.'
);
@@ -37,7 +37,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
// 7002-SORD-060
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('dealticket-error-message-type').should(
cy.getByTestId('deal-ticket-error-message-type').should(
'have.text',
'This market is in auction until it reaches sufficient liquidity. Only limit orders are permitted when market is in auction'
);
@@ -48,7 +48,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('dealticket-warning-auction').should(
cy.getByTestId('deal-ticket-warning-auction').should(
'have.text',
'Any orders placed now will not trade until the auction ends'
);
@@ -60,7 +60,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
TIFlist.filter((item) => item.code === 'FOK')[0].value
);
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('dealticket-error-message-tif').should(
cy.getByTestId('deal-ticket-error-message-tif').should(
'have.text',
'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
);
@@ -5,6 +5,10 @@ import {
toggleMarket,
} from '../support/deal-ticket';
const tooltipContent = 'tooltip-content';
const reduceOnly = 'reduce-only';
const postOnly = 'post-only';
describe('time in force validation', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
@@ -122,13 +126,18 @@ describe('time in force validation', { tags: '@smoke' }, () => {
// 7002-SORD-026
it(`post and reduce order market for ${tif.code}`, function () {
// 7003-SORD-054
// 7003-SORD-055
// 7003-SORD-056
// 7003-SORD-057
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId('post-only').should('be.disabled');
cy.getByTestId('reduce-only').should('be.enabled');
cy.getByTestId(postOnly).should('be.disabled');
cy.getByTestId(reduceOnly).should('be.enabled');
});
});
});
@@ -144,14 +153,33 @@ describe('time in force validation', { tags: '@smoke' }, () => {
validTIFLimit.forEach((tif) => {
it(`post and reduce order for limit ${tif.code}`, function () {
// 7003-SORD-054
// 7003-SORD-055
// 7003-SORD-056
// 7003-SORD-057
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId('post-only').should('be.enabled');
cy.getByTestId('reduce-only').should('be.disabled');
cy.getByTestId(postOnly).should('be.enabled');
cy.getByTestId(reduceOnly).should('be.disabled');
});
});
it(`can see explanation of what post only and reduce only is/does`, function () {
// 7003-SORD-058
cy.get('[for="post-only"]').should('have.text', 'Post only').realHover();
cy.getByTestId(tooltipContent).should(
'contain.text',
`"Post only" will ensure the order is not filled immediately but is placed on the order book as a passive order. When the order is processed it is either stopped (if it would not be filled immediately), or placed in the order book as a passive order until the price taker matches with it.`
);
cy.get('[for="reduce-only"]')
.should('have.text', 'Reduce only')
.realHover();
cy.getByTestId(tooltipContent).should(
'contain.text',
`"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".`
);
});
});
});
@@ -3,6 +3,18 @@ import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { marketsDataQuery } from '@vegaprotocol/mock';
import { positionsQuery } from '@vegaprotocol/mock';
// #region consts
const closePosition = 'close-position';
const dialogCloseX = 'dialog-close';
const dialogContent = 'dialog-content';
const dropDownMenu = 'dropdown-menu';
const marketActionsContent = 'market-actions-content';
const positions = 'Positions';
const tabPositions = 'tab-positions';
const toastContent = 'toast-content';
const tooltipContent = 'tooltip-content';
// #endregion
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
@@ -11,8 +23,9 @@ beforeEach(() => {
describe('positions', { tags: '@smoke', testIsolation: true }, () => {
it('renders positions on trading page', () => {
cy.visit('/#/markets/market-0');
cy.getByTestId('Positions').click();
visitAndClickPositions();
// 7004-POSI-001
// 7004-POSI-002
validatePositionsDisplayed();
});
@@ -35,174 +48,306 @@ describe('positions', { tags: '@smoke', testIsolation: true }, () => {
}
aliasGQLQuery(req, 'Positions', positions);
});
cy.visit('/#/portfolio');
cy.getByTestId('Positions').click();
visitAndClickPositions();
// 7004-POSI-001
// 7004-POSI-002
validatePositionsDisplayed(true);
});
describe('renders position among some graphql errors', () => {
it('rows should be displayed despite errors', () => {
const errors = [
{
message:
'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['marketsConnection', 'edges'],
extensions: {
code: 13,
type: 'Internal',
},
it('Close my position', () => {
visitAndClickPositions();
cy.getByTestId(closePosition).first().click();
// 7004-POSI-010
cy.getByTestId(toastContent).should(
'contain.text',
'Awaiting confirmation'
);
});
});
describe('positions', { tags: '@regression', testIsolation: true }, () => {
it('rows should be displayed despite errors', () => {
const errors = [
{
message:
'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['marketsConnection', 'edges'],
extensions: {
code: 13,
type: 'Internal',
},
];
const marketData = marketsDataQuery();
const edges = marketData.marketsConnection?.edges.map((market) => {
const replace =
market.node.data?.market.id === 'market-2' ? null : market.node.data;
return { ...market, node: { ...market.node, data: replace } };
});
const overrides = {
...marketData,
marketsConnection: { ...marketData.marketsConnection, edges },
};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
cy.visit('/#/markets/market-0');
const emptyCells = [
'notional',
'markPrice',
'currentLeverage',
'averageEntryPrice',
];
cy.getByTestId('tab-positions')
.first()
.within(() => {
cy.get(
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
)
.eq(1)
.within(() => {
emptyCells.forEach((cell) => {
cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
});
},
];
const marketData = marketsDataQuery();
const edges = marketData.marketsConnection?.edges.map((market) => {
const replace =
market.node.data?.market.id === 'market-2' ? null : market.node.data;
return { ...market, node: { ...market.node, data: replace } };
});
const overrides = {
...marketData,
marketsConnection: { ...marketData.marketsConnection, edges },
};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
cy.visit('/#/markets/market-0');
const emptyCells = [
'notional',
'markPrice',
'currentLeverage',
'averageEntryPrice',
];
cy.getByTestId(tabPositions)
.first()
.within(() => {
cy.get(
'[row-id="02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65-market-2"]'
)
.eq(1)
.within(() => {
emptyCells.forEach((cell) => {
cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
});
});
});
it('error message should be displayed', () => {
const errors = [
{
message:
'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['assets', 'edges'],
extensions: {
code: 13,
type: 'Internal',
},
},
];
const overrides = {
marketsConnection: { edges: [] },
};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
});
cy.visit('/#/markets/market-0');
cy.get('[data-testid="tab-positions"]').contains('no market data');
});
it('error message should be displayed', () => {
const errors = [
{
message:
'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
path: ['assets', 'edges'],
extensions: {
code: 13,
type: 'Internal',
},
},
];
const overrides = {
marketsConnection: { edges: [] },
};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'MarketsData', overrides, errors);
});
cy.visit('/#/markets/market-0');
cy.getByTestId(tabPositions).contains('no market data');
});
it('sorting by Market', () => {
visitAndClickPositions();
const marketsSortedDefault = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'ETHBTC Quarterly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedAsc = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'ETHBTC Quarterly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedDesc = [
'SUSPENDED MARKET',
'ETHBTC Quarterly (30 Jun 2022)',
'Apple Monthly (30 Jun 2022)',
'ACTIVE MARKET',
];
cy.getByTestId(positions).click();
// 7004-POSI-003
checkSorting(
'marketName',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
);
});
it('Resize column', () => {
let elementWidth: number;
visitAndClickPositions();
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="marketName"]`)
.find('.ag-header-cell-resize')
.realMouseDown()
.realMouseMove(250, 0)
.realMouseUp();
});
// 7004-POSI-006
cy.get(`[col-id="marketName"]`)
.invoke('width')
.should('be.greaterThan', 250);
cy.get(`[col-id="marketName"]`)
.invoke('width')
.then((width) => {
elementWidth = width as number;
})
.then(() => {
let localStorageCopy: Record<string, string>;
cy.window().then((win) => {
localStorageCopy = { ...win.localStorage };
});
cy.reload();
cy.window().then((win) => {
Object.keys(localStorageCopy).forEach((key) => {
win.localStorage.setItem(key, localStorageCopy[key]);
});
});
// 7004-POSI-012
cy.get('[col-id="marketName"]')
.invoke('width')
.should('equal', elementWidth);
});
});
it('Scroll horizontally', () => {
visitAndClickPositions();
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="marketName"]`)
.find('.ag-header-cell-resize')
.realMouseDown()
.realMouseMove(400, 0)
.realMouseUp();
});
cy.get('[col-id="marketName"]').should('be.visible');
cy.get('.ag-body-horizontal-scroll-viewport').realMouseWheel({
deltaX: 500,
});
// 7004-POSI-004
cy.get('[col-id="updatedAt"]').should('be.visible');
});
it('Drag and drop columns', () => {
visitAndClickPositions();
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get('[col-id="marketName"]')
.realMouseDown()
.realMouseMove(700, 15)
.realMouseUp();
// 7004-POSI-005
cy.get('[col-id="marketName"]').should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
describe('sorting by ag-grid columns should work well', () => {
it('sorting by Market', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = [
'ACTIVE MARKET',
'Apple Monthly (30 Jun 2022)',
'SUSPENDED MARKET',
];
const marketsSortedAsc = ['ACTIVE MARKET', 'Apple Monthly (30 Jun 2022)'];
const marketsSortedDesc = [
'SUSPENDED MARKET',
'Apple Monthly (30 Jun 2022)',
'ACTIVE MARKET',
];
cy.getByTestId('Positions').click();
checkSorting(
'marketName',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
it('I can see warnings', () => {
visitAndClickPositions();
cy.get('[col-id="openVolume"]').within(() => {
cy.get('[aria-label="warning-sign icon"]')
.should('be.visible')
.realHover();
});
// 7004-POSI-011
cy.getByTestId(tooltipContent).should('be.visible');
});
it('Positive and Negative color change', () => {
cy.visit('/#/markets/market-0');
cy.getByTestId(positions).click();
// 7004-POSI-007
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="realisedPNL"]',
'text-vega-green',
'text-vega-pink'
);
});
it('sorting by notional', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = [
'276,761.40348',
'46,126.90058',
'1,688.20',
];
const marketsSortedAsc = ['1,688.20', '46,126.90058', '276,761.40348'];
const marketsSortedDesc = ['276,761.40348', '46,126.90058', '1,688.20'];
cy.getByTestId('Positions').click();
checkSorting(
'notional',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="unrealisedPNL"]',
'text-vega-green',
'text-vega-pink'
);
});
it('sorting by unrealisedPNL', () => {
cy.visit('/#/markets/market-0');
const marketsSortedDefault = ['8.95', '-0.22519', '8.95'];
const marketsSortedAsc = ['-0.22519', '8.95', '8.95'];
const marketsSortedDesc = ['8.95', '8.95', '-0.22519'];
cy.getByTestId('Positions').click();
checkSorting(
'unrealisedPNL',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
cy.get('.ag-center-cols-container').within(() => {
assertPNLColor(
'[col-id="openVolume"]',
'text-vega-green',
'text-vega-pink'
);
});
});
function validatePositionsDisplayed(multiKey = false) {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions').within(() => {
it('View settlement asset', () => {
visitAndClickPositions();
cy.get('[col-id="asset"]').within(() => {
cy.get('button[type="button"]').first().click();
});
// 7004-POSI-008
cy.getByTestId(dialogContent).should('be.visible');
cy.getByTestId(dialogCloseX).click();
cy.getByTestId(dropDownMenu).first().click();
cy.getByTestId(marketActionsContent).click();
// 7004-POSI-009
cy.getByTestId(dialogContent).should('be.visible');
});
});
function validatePositionsDisplayed(multiKey = false) {
cy.getByTestId('tab-positions').should('be.visible');
cy.getByTestId('tab-positions')
.get('.ag-center-cols-container .ag-row')
.first()
.within(() => {
cy.get('[col-id="marketName"]')
.should('be.visible')
.each(($marketSymbol) => {
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
});
.invoke('text')
.should('not.be.empty');
cy.get('.ag-center-cols-container [col-id="openVolume"]').each(
($openVolume) => {
cy.wrap($openVolume).invoke('text').should('not.be.empty');
}
);
cy.get('[col-id="openVolume"]').should('not.be.empty');
// includes average entry price, mark price, realised PNL & leverage
cy.getByTestId('flash-cell').each(($prices) => {
cy.wrap($prices).invoke('text').should('not.be.empty');
});
cy.getByTestId('flash-cell').should('not.be.empty');
if (!multiKey) {
cy.get('[col-id="currentLeverage"]').should('contain.text', '2.846.1');
cy.get('[col-id="currentLeverage"]').should('contain.text', '2,767.3');
cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '0.01');
}
cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
});
cy.get('[col-id="unrealisedPNL"]').should('not.be.empty');
cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position
cy.get('[col-id="realisedPNL"]').should('contain.text', '2.30'); // Total Realised PNL
cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95'); // Total Unrealised PNL
cy.get('.ag-header-row [col-id="notional"]')
.should('contain.text', 'Notional')
.realHover();
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
});
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
}
});
cy.get('.ag-header-row [col-id="notional"]')
.should('contain.text', 'Notional')
.realHover();
cy.get('.ag-popup').should('contain.text', 'Mark price x open volume');
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
}
function assertPNLColor(
pnlSelector: string,
positiveClass: string,
negativeClass: string
) {
cy.get(pnlSelector).each(($el) => {
const value = parseFloat($el.text());
if (value > 0) {
cy.wrap($el).invoke('attr', 'class').should('contain', positiveClass);
} else if (value < 0) {
cy.wrap($el).invoke('attr', 'class').should('contain', negativeClass);
} else if (value == 0) {
cy.wrap($el)
.invoke('attr', 'class')
.should('not.contain', negativeClass, positiveClass);
} else {
throw new Error('Unexpected value');
}
});
}
function visitAndClickPositions() {
cy.visit('/#/markets/market-0');
cy.getByTestId(positions).click();
}
@@ -1,32 +1,79 @@
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
});
const colHeader = '.ag-header-cell-text';
const colIdPrice = '[col-id=price]';
const colIdSize = '[col-id=size]';
const colIdCreatedAt = '[col-id=createdAt]';
const tradesTab = 'Trades';
const tradesTable = 'tab-trades';
describe('trades', { tags: '@smoke' }, () => {
const colIdPrice = 'price';
const colIdSize = 'size';
const colIdCreatedAt = 'createdAt';
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
});
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.getByTestId(tradesTab).click();
});
it('renders trades', () => {
cy.getByTestId('Trades').click();
cy.getByTestId('tab-trades').should('be.visible');
it('show trades', () => {
// 6005-THIS-001
// 6005-THIS-002
cy.getByTestId(tradesTab).should('be.visible');
cy.getByTestId(tradesTable).should('be.visible');
cy.getByTestId(tradesTable).should('not.be.empty');
});
cy.get(`[col-id=${colIdPrice}]`).each(($tradePrice) => {
it('show trades prices', () => {
// 6005-THIS-003
cy.get(`${colIdPrice} ${colHeader}`).first().should('have.text', 'Price');
cy.get(colIdPrice).each(($tradePrice) => {
cy.wrap($tradePrice).invoke('text').should('not.be.empty');
});
cy.get(`[col-id=${colIdSize}]`).each(($tradeSize) => {
});
it('show trades sizes', () => {
// 6005-THIS-004
cy.get(`${colIdSize} ${colHeader}`).first().should('have.text', 'Size');
cy.get(colIdSize).each(($tradeSize) => {
cy.wrap($tradeSize).invoke('text').should('not.be.empty');
});
});
it('show trades date and time', () => {
// 6005-THIS-005
cy.get(`${colIdCreatedAt} ${colHeader}`).should('have.text', 'Created at');
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
cy.get(`[col-id=${colIdCreatedAt}]`).each(($tradeDateTime, index) => {
cy.get(colIdCreatedAt).each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
cy.wrap($tradeDateTime).invoke('text').should('match', dateTimeRegex);
}
});
});
it('trades are sorted descending by datetime', () => {
// 6005-THIS-006
const dateTimes: Date[] = [];
cy.get(colIdCreatedAt)
.each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
dateTimes.push(new Date($tradeDateTime.text()));
}
})
.then(() => {
expect(dateTimes).to.deep.equal(
dateTimes.sort((a, b) => b.getTime() - a.getTime())
);
});
});
it('copy price to deal ticket form', () => {
// 6005-THIS-007
cy.get(colIdPrice).last().click();
cy.getByTestId('order-price').should('have.value', '171.16898');
});
});
+8
View File
@@ -31,6 +31,8 @@ import {
protocolUpgradeProposalsQuery,
blockStatisticsQuery,
networkParamQuery,
liquidityProvisionsQuery,
liquidityProviderFeeShareQuery,
} from '@vegaprotocol/mock';
import type { PartialDeep } from 'type-fest';
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/markets';
@@ -158,6 +160,12 @@ const mockTradingPage = (
);
aliasGQLQuery(req, 'Trades', tradesQuery());
aliasGQLQuery(req, 'Chart', chartQuery());
aliasGQLQuery(req, 'LiquidityProvisions', liquidityProvisionsQuery());
aliasGQLQuery(
req,
'LiquidityProviderFeeShare',
liquidityProviderFeeShareQuery
);
aliasGQLQuery(req, 'Candles', candlesQuery());
aliasGQLQuery(req, 'Withdrawals', withdrawalsQuery());
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
+1 -1
View File
@@ -14,4 +14,4 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.20.16-core-0.71.5
NX_APP_VERSION=v0.20.18-core-0.71.8
@@ -83,6 +83,8 @@ export const LiquidityContainer = ({
const assetDecimalPlaces =
market?.tradableInstrument.instrument.product.settlementAsset.decimals || 0;
const quantum =
market?.tradableInstrument.instrument.product.settlementAsset.quantum || 0;
const symbol =
market?.tradableInstrument.instrument.product.settlementAsset.symbol;
@@ -98,6 +100,7 @@ export const LiquidityContainer = ({
rowData={data}
symbol={symbol}
assetDecimalPlaces={assetDecimalPlaces}
quantum={quantum}
stakeToCcyVolume={stakeToCcyVolume}
overlayNoRowsTemplate={error ? error.message : t('No data')}
/>
@@ -150,6 +153,7 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
<HeaderStat
heading={t('Target stake')}
description={tooltipMapping['targetStake']}
testId="target-stake"
>
<div>
{targetStake
@@ -163,6 +167,7 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
<HeaderStat
heading={t('Supplied stake')}
description={tooltipMapping['suppliedStake']}
testId="supplied-stake"
>
<div>
{suppliedStake
@@ -178,10 +183,10 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
{formatNumberPercentage(percentage, 2)}
</HeaderStat>
<HeaderStat heading={t('Market ID')}>
<HeaderStat heading={t('Market ID')} testId="liquidity-market-id">
<div className="break-word">{marketId}</div>
</HeaderStat>
<HeaderStat heading={t('Learn more')}>
<HeaderStat heading={t('Learn more')} testId="liquidity-learn-more">
{DocsLinks ? (
<ExternalLink href={DocsLinks.LIQUIDITY}>
{t('Providing liquidity')}
@@ -8,7 +8,7 @@ import {
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { CSSProperties } from 'react';
import { useState } from 'react';
import { useCallback, useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
@@ -151,7 +151,11 @@ export const MarketSelector = ({
</div>
<div className="px-4 py-2">
<span className="inline-block border-b border-black dark:border-white">
<Link to={'/markets/all'} className="flex items-center gap-x-2">
<Link
to={'/markets/all'}
data-testid="all-markets-link"
className="flex items-center gap-x-2"
>
{t('All markets')}
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
</Link>
@@ -180,7 +184,6 @@ const MarketList = ({
if (error) {
return <div>{error.message}</div>;
}
return (
<AutoSizer>
{({ width, height }) => (
@@ -200,6 +203,29 @@ const MarketList = ({
);
};
interface ListItemData {
data: MarketMaybeWithDataAndCandles[];
onSelect?: (marketId: string) => void;
currentMarketId?: string;
}
const ListItem = ({
index,
style,
data,
}: {
index: number;
style: CSSProperties;
data: ListItemData;
}) => (
<MarketSelectorItem
market={data.data[index]}
currentMarketId={data.currentMarketId}
style={style}
onSelect={data.onSelect}
/>
);
const List = ({
data,
loading,
@@ -208,28 +234,20 @@ const List = ({
onSelect,
noItems,
currentMarketId,
}: {
data: MarketMaybeWithDataAndCandles[];
}: ListItemData & {
loading: boolean;
width: number;
height: number;
noItems: string;
onSelect?: (marketId: string) => void;
currentMarketId?: string;
}) => {
const row = ({ index, style }: { index: number; style: CSSProperties }) => {
const market = data[index];
return (
<MarketSelectorItem
market={market}
currentMarketId={currentMarketId}
style={style}
onSelect={onSelect}
/>
);
};
const itemKey = useCallback(
(index: number, data: ListItemData) => data.data[index].id,
[]
);
const itemData = useMemo(
() => ({ data, onSelect, currentMarketId }),
[data, onSelect, currentMarketId]
);
if (!data || loading) {
return (
<div style={{ width, height }}>
@@ -255,11 +273,13 @@ const List = ({
<FixedSizeList
className="virtualized-list"
itemCount={data.length}
itemData={itemData}
itemSize={130}
itemKey={itemKey}
width={width}
height={height}
>
{row}
{ListItem}
</FixedSizeList>
);
};
@@ -49,7 +49,7 @@ const MarketBottomPanel = memo(
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'bottom' });
const { screenSize } = useScreenDimensions();
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
return 'xxxl' === screenSize ? (
<ResizableGrid
@@ -143,7 +143,7 @@ const MarketBottomPanel = memo(
<VegaWalletContainer>
<TradingViews.collateral.component
pinnedAsset={pinnedAsset}
noBottomPlaceholder
onMarketClick={onMarketClick}
hideButtons
storeKey="marketCollateral"
/>
@@ -224,6 +224,7 @@ const MarketBottomPanel = memo(
<VegaWalletContainer>
<TradingViews.collateral.component
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
hideButtons
storeKey="marketCollateral"
/>
@@ -249,6 +250,7 @@ const MainGrid = memo(
const [sizesMiddle, handleOnMiddleLayoutChange] = usePaneLayout({
id: 'middle-1',
});
const onMarketClick = useMarketClickHandler(true);
return (
<ResizableGrid vertical onChange={handleOnLayoutChange}>
@@ -267,6 +269,7 @@ const MainGrid = memo(
<Tab id="ticket" name={t('Ticket')}>
<TradingViews.ticket.component
marketId={marketId}
onMarketClick={onMarketClick}
onClickCollateral={() => navigate('/portfolio')}
/>
</Tab>
@@ -39,7 +39,7 @@ export const TradePanels = ({
}: TradePanelsProps) => {
const [drawerOpen, setDrawerOpen] = useState(false);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
const [view, setView] = useState<TradingView>('candles');
const renderView = () => {
@@ -22,7 +22,7 @@ export const DepositsContainer = () => {
<div className="h-full">
<div className="h-full relative">
<DepositsTable
rowData={data || []}
rowData={data}
ref={gridRef}
{...bottomPlaceholderProps}
overlayNoRowsTemplate={error ? error.message : t('No deposits')}
@@ -34,7 +34,7 @@ export const Portfolio = () => {
}, [updateTitle]);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
const wrapperClasses = 'h-full max-h-full flex flex-col';
return (
@@ -92,7 +92,10 @@ export const Portfolio = () => {
<Tabs storageKey="console-portfolio-bottom">
<Tab id="collateral" name={t('Collateral')}>
<VegaWalletContainer>
<AccountsContainer storeKey="portfolioCollateral" />
<AccountsContainer
storeKey="portfolioCollateral"
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="deposits" name={t('Deposits')}>
@@ -12,13 +12,13 @@ import { useDepositDialog } from '@vegaprotocol/deposits';
export const AccountsContainer = ({
pinnedAsset,
hideButtons,
noBottomPlaceholder,
storeKey,
onMarketClick,
}: {
pinnedAsset?: PinnedAsset;
hideButtons?: boolean;
noBottomPlaceholder?: boolean;
storeKey?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}) => {
const { pubKey, isReadOnly } = useVegaWallet();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
@@ -48,9 +48,9 @@ export const AccountsContainer = ({
onClickAsset={onClickAsset}
onClickWithdraw={openWithdrawalDialog}
onClickDeposit={openDepositDialog}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
pinnedAsset={pinnedAsset}
noBottomPlaceholder={noBottomPlaceholder}
storeKey={storeKey}
/>
{!isReadOnly && !hideButtons && (
@@ -49,6 +49,9 @@ export const AppLoader = ({ children }: { children: ReactNode }) => {
const cacheConfig: InMemoryCacheConfig = {
typePolicies: {
Statistics: {
merge: true,
},
Account: {
keyFields: false,
fields: {
@@ -80,12 +83,6 @@ const cacheConfig: InMemoryCacheConfig = {
ERC20: {
keyFields: ['contractAddress'],
},
PositionUpdate: {
keyFields: false,
},
AccountUpdate: {
keyFields: false,
},
Party: {
keyFields: false,
},
@@ -95,8 +92,16 @@ const cacheConfig: InMemoryCacheConfig = {
Fees: {
keyFields: false,
},
// Don't cache order update as this subscription result gets merged into the main order cache
// We don't need to write these to the cache at all
// The folling types are cached by the data provider and not by apollo
PositionUpdate: {
keyFields: false,
},
TradeUpdate: {
keyFields: false,
},
AccountUpdate: {
keyFields: false,
},
OrderUpdate: {
keyFields: false,
},
@@ -125,14 +125,19 @@ export const MarketLiquiditySupplied = ({
</KeyValueTableRow>
</KeyValueTable>
<br />
<Link href={`/#/liquidity/${marketId}`} data-testid="view-liquidity-link">
{t('View liquidity provision table')}
</Link>
{DocsLinks && (
<ExternalLink href={DocsLinks.LIQUIDITY} className="mt-2">
{t('Learn about providing liquidity')}
</ExternalLink>
)}
<div className="flex flex-col gap-2">
<Link
href={`/#/liquidity/${marketId}`}
data-testid="view-liquidity-link"
>
{t('View liquidity provision table')}
</Link>
{DocsLinks && (
<ExternalLink href={DocsLinks.LIQUIDITY} className="mt-2">
{t('Learn about providing liquidity')}
</ExternalLink>
)}
</div>
{showMessage && (
<p className="mt-4">
{t(
@@ -20,20 +20,8 @@ export const useMarketClickHandler = (replace = false) => {
);
};
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]
);
export const useMarketLiquidityClickHandler = () => {
return useCallback((selectedId: string, metaKey?: boolean) => {
window.open(`/#/liquidity/${selectedId}`, metaKey ? '_blank' : '_self');
}, []);
};
+38
View File
@@ -0,0 +1,38 @@
fragment MarginFields on MarginLevels {
maintenanceLevel
searchLevel
initialLevel
collateralReleaseLevel
asset {
id
}
market {
id
}
}
query Margins($partyId: ID!) {
party(id: $partyId) {
id
marginsConnection {
edges {
node {
...MarginFields
}
}
}
}
}
subscription MarginsSubscription($partyId: ID!) {
margins(partyId: $partyId) {
marketId
asset
partyId
maintenanceLevel
searchLevel
initialLevel
collateralReleaseLevel
timestamp
}
}
+114
View File
@@ -0,0 +1,114 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
export type MarginsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type MarginsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, marginsConnection?: { __typename?: 'MarginConnection', edges?: Array<{ __typename?: 'MarginEdge', node: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
export type MarginsSubscriptionSubscriptionVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type MarginsSubscriptionSubscription = { __typename?: 'Subscription', margins: { __typename?: 'MarginLevelsUpdate', marketId: string, asset: string, partyId: string, maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, timestamp: any } };
export const MarginFieldsFragmentDoc = gql`
fragment MarginFields on MarginLevels {
maintenanceLevel
searchLevel
initialLevel
collateralReleaseLevel
asset {
id
}
market {
id
}
}
`;
export const MarginsDocument = gql`
query Margins($partyId: ID!) {
party(id: $partyId) {
id
marginsConnection {
edges {
node {
...MarginFields
}
}
}
}
}
${MarginFieldsFragmentDoc}`;
/**
* __useMarginsQuery__
*
* To run a query within a React component, call `useMarginsQuery` and pass it any options that fit your needs.
* When your component renders, `useMarginsQuery` 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 } = useMarginsQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function useMarginsQuery(baseOptions: Apollo.QueryHookOptions<MarginsQuery, MarginsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<MarginsQuery, MarginsQueryVariables>(MarginsDocument, options);
}
export function useMarginsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MarginsQuery, MarginsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<MarginsQuery, MarginsQueryVariables>(MarginsDocument, options);
}
export type MarginsQueryHookResult = ReturnType<typeof useMarginsQuery>;
export type MarginsLazyQueryHookResult = ReturnType<typeof useMarginsLazyQuery>;
export type MarginsQueryResult = Apollo.QueryResult<MarginsQuery, MarginsQueryVariables>;
export const MarginsSubscriptionDocument = gql`
subscription MarginsSubscription($partyId: ID!) {
margins(partyId: $partyId) {
marketId
asset
partyId
maintenanceLevel
searchLevel
initialLevel
collateralReleaseLevel
timestamp
}
}
`;
/**
* __useMarginsSubscriptionSubscription__
*
* To run a query within a React component, call `useMarginsSubscriptionSubscription` and pass it any options that fit your needs.
* When your component renders, `useMarginsSubscriptionSubscription` 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 subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useMarginsSubscriptionSubscription({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function useMarginsSubscriptionSubscription(baseOptions: Apollo.SubscriptionHookOptions<MarginsSubscriptionSubscription, MarginsSubscriptionSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<MarginsSubscriptionSubscription, MarginsSubscriptionSubscriptionVariables>(MarginsSubscriptionDocument, options);
}
export type MarginsSubscriptionSubscriptionHookResult = ReturnType<typeof useMarginsSubscriptionSubscription>;
export type MarginsSubscriptionSubscriptionResult = Apollo.SubscriptionResult<MarginsSubscriptionSubscription>;
@@ -94,6 +94,7 @@ export const accountsOnlyDataProvider = makeDataProvider<
update,
getData,
getDelta,
fetchPolicy: 'no-cache',
});
export interface AccountFields extends Account {
+70 -54
View File
@@ -1,31 +1,39 @@
import { useRef, memo, useCallback, useState } from 'react';
import { useRef, memo, useState, useCallback } from 'react';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { AgGridReact } from 'ag-grid-react';
import type { AccountFields } from './accounts-data-provider';
import {
aggregatedAccountsDataProvider,
aggregatedAccountDataProvider,
} from './accounts-data-provider';
import type { PinnedAsset } from './accounts-table';
import { AccountTable } from './accounts-table';
import isEqual from 'lodash/isEqual';
import { Dialog } from '@vegaprotocol/ui-toolkit';
import BreakdownTable from './breakdown-table';
const AccountBreakdown = ({
assetId,
partyId,
onMarketClick,
}: {
assetId: string;
partyId: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}) => {
const gridRef = useRef<AgGridReact>(null);
const { data } = useDataProvider({
dataProvider: aggregatedAccountDataProvider,
variables: { partyId, assetId },
update: ({ data }) => {
if (gridRef.current?.api && data?.breakdown) {
gridRef.current?.api.setRowData(data?.breakdown);
return true;
}
return false;
},
});
return (
<div
className="h-[35vh] w-full m-auto flex flex-col"
@@ -42,19 +50,58 @@ const AccountBreakdown = ({
])}
</p>
)}
<BreakdownTable data={data?.breakdown || null} domLayout="autoHeight" />
<BreakdownTable
ref={gridRef}
data={data?.breakdown || null}
domLayout="autoHeight"
onMarketClick={onMarketClick}
/>
</div>
);
};
export const AccountBreakdownDialog = memo(
({
assetId,
partyId,
onClose,
onMarketClick,
}: {
assetId?: string;
partyId: string;
onClose: () => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}) => {
return (
<Dialog
size="medium"
open={Boolean(assetId)}
onChange={(isOpen) => {
if (!isOpen) {
onClose();
}
}}
>
{assetId && (
<AccountBreakdown
assetId={assetId}
partyId={partyId}
onMarketClick={onMarketClick}
/>
)}
</Dialog>
);
}
);
interface AccountManagerProps {
partyId: string;
onClickAsset: (assetId: string) => void;
onClickWithdraw?: (assetId?: string) => void;
onClickDeposit?: (assetId?: string) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
isReadOnly: boolean;
pinnedAsset?: PinnedAsset;
noBottomPlaceholder?: boolean;
storeKey?: string;
}
@@ -65,50 +112,27 @@ export const AccountManager = ({
partyId,
isReadOnly,
pinnedAsset,
noBottomPlaceholder,
storeKey,
onMarketClick,
}: AccountManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [breakdownAssetId, setBreakdownAssetId] = useState<string>();
const update = useCallback(
({ data }: { data: AccountFields[] | null }) => {
if (!data || !gridRef.current?.api) {
return false;
}
const pinnedAssetRowData =
pinnedAsset && data.find((d) => d.asset.id === pinnedAsset.id);
if (pinnedAssetRowData) {
const pinnedTopRow = gridRef.current.api.getPinnedTopRow(0);
if (
pinnedTopRow?.data?.balance === '0' &&
pinnedAssetRowData.balance !== '0'
) {
return false;
}
if (!isEqual(pinnedTopRow?.data, pinnedAssetRowData)) {
gridRef.current.api.setPinnedTopRowData([pinnedAssetRowData]);
}
}
gridRef.current.api.setRowData(
pinnedAssetRowData
? data?.filter((d) => d !== pinnedAssetRowData)
: data
);
return true;
},
[gridRef, pinnedAsset]
);
const { data, error } = useDataProvider({
dataProvider: aggregatedAccountsDataProvider,
variables: { partyId },
update,
});
const bottomPlaceholderProps = useBottomPlaceholder({
gridRef,
disabled: noBottomPlaceholder,
});
const onMarketClickInternal = useCallback(
(...args: Parameters<NonNullable<typeof onMarketClick>>) => {
setBreakdownAssetId(undefined);
if (onMarketClick) {
onMarketClick(...args);
}
},
[onMarketClick]
);
return (
<div className="relative h-full">
<AccountTable
@@ -121,22 +145,14 @@ export const AccountManager = ({
isReadOnly={isReadOnly}
pinnedAsset={pinnedAsset}
storeKey={storeKey}
{...bottomPlaceholderProps}
overlayNoRowsTemplate={error ? error.message : t('No accounts')}
/>
<Dialog
size="medium"
open={Boolean(breakdownAssetId)}
onChange={(isOpen) => {
if (!isOpen) {
setBreakdownAssetId(undefined);
}
}}
>
{breakdownAssetId && (
<AccountBreakdown assetId={breakdownAssetId} partyId={partyId} />
)}
</Dialog>
<AccountBreakdownDialog
assetId={breakdownAssetId}
partyId={partyId}
onClose={useCallback(() => setBreakdownAssetId(undefined), [])}
onMarketClick={onMarketClick ? onMarketClickInternal : undefined}
/>
</div>
);
};
+138 -138
View File
@@ -1,6 +1,7 @@
import { forwardRef, useMemo, useCallback } from 'react';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
isNumeric,
toBigNum,
} from '@vegaprotocol/utils';
@@ -10,21 +11,15 @@ import type {
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { COL_DEFS } from '@vegaprotocol/datagrid';
import {
ButtonLink,
Button,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Button, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { AgGridColumn } from 'ag-grid-react';
import type {
IDatasource,
IGetRowsParams,
RowNode,
RowHeightParams,
ColDef,
} from 'ag-grid-community';
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
import type { AccountFields } from './accounts-data-provider';
@@ -35,29 +30,16 @@ import classNames from 'classnames';
import { AccountsActionsDropdown } from './accounts-actions-dropdown';
const colorClass = (percentageUsed: number, neutral = false) => {
return classNames({
return classNames('text-right', {
'text-neutral-500 dark:text-neutral-400': percentageUsed < 75 && !neutral,
'text-vega-orange': percentageUsed >= 75 && percentageUsed < 90,
'text-vega-pink': percentageUsed >= 90,
});
};
export const percentageValue = (part?: string, total?: string) =>
new BigNumber(part || 0)
.dividedBy(total || 1)
.multipliedBy(100)
.toNumber();
const formatWithAssetDecimals = (
data: AccountFields | undefined,
value: string | undefined
) => {
return (
data &&
data.asset &&
isNumeric(value) &&
addDecimalsFormatNumber(value, data.asset.decimals)
);
export const percentageValue = (part: string, total: string) => {
total = !total || total === '0' ? '1' : total;
return new BigNumber(part).dividedBy(total).multipliedBy(100).toNumber();
};
export const accountValuesComparator = (
@@ -81,15 +63,10 @@ export interface GetRowsParams extends Omit<IGetRowsParams, 'successCallback'> {
successCallback(rowsThisBlock: AccountFields[], lastRow?: number): void;
}
export interface Datasource extends IDatasource {
getRows(params: GetRowsParams): void;
}
export type PinnedAsset = Pick<Asset, 'symbol' | 'name' | 'id' | 'decimals'>;
export interface AccountTableProps extends AgGridReactProps {
rowData?: AccountFields[] | null;
datasource?: Datasource;
onClickAsset: (assetId: string) => void;
onClickWithdraw?: (assetId: string) => void;
onClickDeposit?: (assetId: string) => void;
@@ -148,83 +125,55 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
const showDepositButton = pinnedAsset?.balance === '0';
return (
<AgGrid
{...props}
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No accounts')}
getRowId={({
data,
}: {
data: AccountFields & { isLastPlaceholder?: boolean; id?: string };
}) => (data.isLastPlaceholder && data.id ? data.id : data.asset.id)}
ref={ref}
tooltipShowDelay={500}
rowData={rowData?.filter(
(data) => data.asset.id !== props.pinnedAsset?.id
)}
defaultColDef={{
resizable: true,
tooltipComponent: TooltipCellComponent,
sortable: true,
comparator: accountValuesComparator,
}}
getRowHeight={getPinnedAssetRowHeight}
pinnedTopRowData={pinnedAsset ? [pinnedAsset] : undefined}
>
<AgGridColumn
headerName={t('Asset')}
field="asset.symbol"
headerTooltip={t(
const colDefs = useMemo(() => {
const defs: ColDef[] = [
{
headerName: t('Asset'),
field: 'asset.symbol',
headerTooltip: t(
'Asset is the collateral that is deposited into the Vega protocol.'
)}
cellRenderer={({
value,
data,
}: VegaICellRendererParams<AccountFields, 'asset.symbol'>) => {
return (
<ButtonLink
data-testid="asset"
onClick={() => {
if (data) {
onClickAsset(data.asset.id);
}
}}
>
{value}
</ButtonLink>
);
}}
/>
<AgGridColumn
headerName={t('Used')}
type="rightAligned"
field="used"
headerTooltip={t(
),
cellClass: 'underline',
onCellClicked: ({ data }) => {
if (data) {
onClickAsset(data.asset.id);
}
},
},
{
headerName: t('Used'),
type: 'rightAligned',
field: 'used',
headerTooltip: t(
'Currently allocated to a market as margin or bond. Check the breakdown for details.'
)}
cellRenderer={({
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
onCellClicked: ({ data }) => {
if (!data || !onClickBreakdown) return;
onClickBreakdown(data.asset.id);
},
cellRenderer: ({
data,
value,
}: VegaICellRendererParams<AccountFields, 'used'>) => {
if (!data) return null;
if (!value || !data) return '-';
const percentageUsed = percentageValue(value, data.total);
const valueFormatted = formatWithAssetDecimals(data, value);
const valueFormatted = addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
return data.breakdown ? (
<>
<ButtonLink
data-testid="breakdown"
onClick={() => {
onClickBreakdown && onClickBreakdown(data.asset.id);
}}
>
<span>{valueFormatted}</span>
</ButtonLink>
<span className="underline">{valueFormatted}</span>
<span
className={classNames(
colorClass(percentageUsed),
'ml-2 inline-block w-14'
'ml-1 inline-block w-14'
)}
>
{percentageUsed.toFixed(2)}%
@@ -232,59 +181,77 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
</>
) : (
<>
<span>{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-neutral-500 dark:text-neutral-400">
0.00%
<span className="underline">{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-vega-light-200 dark:text-vega-dark-200">
{t('0.00%')}'
</span>
</>
);
}}
/>
<AgGridColumn
headerName={t('Available')}
field="available"
type="rightAligned"
headerTooltip={t(
},
},
{
headerName: t('Available'),
field: 'available',
type: 'rightAligned',
headerTooltip: t(
'Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.'
)}
cellRenderer={({
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
cellClass: ({ data }) => {
const percentageUsed = percentageValue(data?.used, data?.total);
return colorClass(percentageUsed, true);
},
valueFormatter: ({
value,
data,
}: VegaICellRendererParams<AccountFields, 'available'>) => {
const percentageUsed = percentageValue(data?.used, data?.total);
return (
<span className={colorClass(percentageUsed, true)}>
{formatWithAssetDecimals(data, value)}
</span>
}: VegaValueFormatterParams<AccountFields, 'available'>) => {
if (!value || !data) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
}}
/>
<AgGridColumn
headerName={t('Total')}
type="rightAligned"
field="total"
headerTooltip={t(
},
},
{
headerName: t('Total'),
type: 'rightAligned',
field: 'total',
headerTooltip: t(
'The total amount of each asset on this key. Includes used and available collateral.'
)}
valueFormatter={({
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<AccountFields, 'total'>) =>
formatWithAssetDecimals(data, data?.total)
}
/>
<AgGridColumn
colId="accounts-actions"
field="asset.id"
{...COL_DEFS.actions}
minWidth={showDepositButton ? 130 : COL_DEFS.actions.minWidth}
maxWidth={showDepositButton ? 130 : COL_DEFS.actions.maxWidth}
cellRenderer={({
value,
}: VegaValueFormatterParams<AccountFields, 'total'>) => {
if (!data || !value) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
colId: 'accounts-actions',
field: 'asset.id',
...COL_DEFS.actions,
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
cellRenderer: ({
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.balance === '0') {
if (node.rowPinned && node.data?.total === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
@@ -319,9 +286,42 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
}}
/>
);
}}
/>
</AgGrid>
},
},
];
return defs;
}, [
onClickAsset,
onClickBreakdown,
onClickDeposit,
onClickWithdraw,
props.isReadOnly,
showDepositButton,
]);
const data = rowData?.filter(
(data) => data.asset.id !== props.pinnedAsset?.id
);
return (
<AgGrid
{...props}
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No accounts')}
getRowId={({ data }: { data: AccountFields }) => data.asset.id}
ref={ref}
tooltipShowDelay={500}
rowData={data}
defaultColDef={{
resizable: true,
tooltipComponent: TooltipCellComponent,
sortable: true,
comparator: accountValuesComparator,
}}
columnDefs={colDefs}
getRowHeight={getPinnedAssetRowHeight}
pinnedTopRowData={pinnedAsset ? [pinnedAsset] : undefined}
/>
);
}
);
+57 -15
View File
@@ -35,6 +35,7 @@ export const accountFields: AccountFieldsFragment[] = [
balance: '100000000',
market: null,
asset: {
// tEURO
__typename: 'Asset',
id: 'asset-id',
},
@@ -44,6 +45,7 @@ export const accountFields: AccountFieldsFragment[] = [
type: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
balance: '100000000',
asset: {
// tDAI
__typename: 'Asset',
id: 'asset-id-2',
},
@@ -57,6 +59,7 @@ export const accountFields: AccountFieldsFragment[] = [
id: 'market-2',
},
asset: {
// tEURO
__typename: 'Asset',
id: 'asset-id',
},
@@ -70,6 +73,7 @@ export const accountFields: AccountFieldsFragment[] = [
id: 'market-0',
},
asset: {
// AST0
__typename: 'Asset',
id: 'asset-0',
},
@@ -83,6 +87,7 @@ export const accountFields: AccountFieldsFragment[] = [
id: 'market-3',
},
asset: {
// AST0
__typename: 'Asset',
id: 'asset-0',
},
@@ -90,9 +95,10 @@ export const accountFields: AccountFieldsFragment[] = [
{
__typename: 'AccountBalance',
type: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
balance: '10000000000',
balance: '10000000',
market: null,
asset: {
// AST0
__typename: 'Asset',
id: 'asset-0',
},
@@ -104,6 +110,7 @@ export const accountFields: AccountFieldsFragment[] = [
balance: '100000001',
market: null,
asset: {
// tBTC (sepolia)
__typename: 'Asset',
id: 'cee709223217281d7893b650850ae8ee8a18b7539b5658f9b4cc24de95dd18ad',
},
@@ -114,6 +121,7 @@ export const accountFields: AccountFieldsFragment[] = [
balance: '100000002',
market: null,
asset: {
// tBTC (test)
__typename: 'Asset',
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
},
@@ -143,19 +151,53 @@ export const amendGeneralAccountBalance = (
marketId: string,
balance: string
) => {
if (accounts.party?.accountsConnection?.edges) {
const marginAccount = accounts.party.accountsConnection.edges.find(
(edge) => edge?.node.market?.id === marketId
);
if (marginAccount) {
const generalAccount = accounts.party.accountsConnection.edges.find(
(edge) =>
edge?.node.asset.id === marginAccount.node.asset.id &&
!edge?.node.market
);
if (generalAccount) {
generalAccount.node.balance = balance;
}
}
if (!accounts.party?.accountsConnection?.edges) {
return accounts;
}
const marginAccount = accounts.party?.accountsConnection?.edges?.find(
(edge) => edge?.node.market?.id === marketId
);
if (marginAccount) {
const edges = accounts.party.accountsConnection.edges.map((edge) =>
edge?.node.asset.id === marginAccount.node.asset.id && !edge?.node.market
? { ...edge, node: { ...edge.node, balance } }
: edge
);
return {
...accounts,
party: {
...accounts.party,
accountsConnection: {
...accounts.party.accountsConnection,
edges,
},
},
};
}
return accounts;
};
export const amendMarginAccountBalance = (
accounts: AccountsQuery,
marketId: string,
balance: string
) => {
if (!accounts.party?.accountsConnection?.edges) {
return accounts;
}
const edges = accounts.party?.accountsConnection?.edges?.map((edge) =>
edge?.node.market?.id === marketId
? { ...edge, node: { ...edge?.node, balance } }
: edge
);
return {
...accounts,
party: {
...accounts.party,
accountsConnection: {
...accounts.party.accountsConnection,
edges,
},
},
};
};
+29 -3
View File
@@ -4,6 +4,14 @@ import * as Types from '@vegaprotocol/types';
import type { AccountFields } from './accounts-data-provider';
import { getAccountData } from './accounts-data-provider';
const marginHealthChartTestId = 'margin-health-chart';
jest.mock('./margin-health-chart', () => ({
MarginHealthChart: () => {
return <div data-testid={marginHealthChartTestId}></div>;
},
}));
const singleRow = {
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
@@ -37,10 +45,10 @@ describe('BreakdownTable', () => {
render(<BreakdownTable data={singleRowData} />);
});
const headers = await screen.findAllByRole('columnheader');
expect(headers).toHaveLength(3);
expect(headers).toHaveLength(4);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual(['Market', 'Account type', 'Balance']);
).toEqual(['Market', 'Account type', 'Balance', 'Margin health']);
});
it('should apply correct formatting', async () => {
@@ -55,9 +63,27 @@ describe('BreakdownTable', () => {
'1,256.00',
'1,256.00',
];
cells.forEach((cell, i) => {
cells.slice(0, -1).forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
expect(screen.getByTestId(marginHealthChartTestId)).toBeInTheDocument();
});
it('displays margin health chart only for margin account', async () => {
await act(async () => {
render(
<BreakdownTable
data={[
{
...singleRow,
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
market: null,
},
]}
/>
);
});
expect(screen.queryByTestId(marginHealthChartTestId)).toBeNull();
});
it('should get correct account data', () => {
+111 -71
View File
@@ -1,67 +1,38 @@
import { forwardRef } from 'react';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { forwardRef, useMemo } from 'react';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Intent } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import { Intent, TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
import type { AccountFields } from './accounts-data-provider';
import { AccountTypeMapping } from '@vegaprotocol/types';
import type {
ValueProps,
VegaValueFormatterParams,
VegaICellRendererParams,
} from '@vegaprotocol/datagrid';
import { progressBarCellRendererSelector } from '@vegaprotocol/datagrid';
import { ProgressBarCell } from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid, PriceCell } from '@vegaprotocol/datagrid';
import type { ValueFormatterParams } from 'ag-grid-community';
import type { ColDef } from 'ag-grid-community';
import { accountValuesComparator } from './accounts-table';
export const progressBarValueFormatter = ({
data,
node,
}: ValueFormatterParams): ValueProps['valueFormatted'] | undefined => {
if (!data || node?.rowPinned) {
return undefined;
}
const min = BigInt(data.used);
const mid = BigInt(data.available);
const max = BigInt(data.total);
const range = max > min ? max : min;
return {
low: addDecimalsFormatNumber(min.toString(), data.asset.decimals),
high: addDecimalsFormatNumber(mid.toString(), data.asset.decimals),
value: range ? Number((min * BigInt(100)) / range) : 0,
intent: Intent.Warning,
};
};
import { MarginHealthChart } from './margin-health-chart';
import { MarketNameCell } from '@vegaprotocol/datagrid';
import { AccountType } from '@vegaprotocol/types';
interface BreakdownTableProps extends AgGridReactProps {
data: AccountFields[] | null;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}
const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
({ data }, ref) => {
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('Collateral not used')}
rowData={data}
getRowId={({ data }: { data: AccountFields }) =>
`${data.asset.id}-${data.type}-${data.market?.id}`
}
ref={ref}
rowHeight={34}
components={{ PriceCell }}
tooltipShowDelay={500}
defaultColDef={{
flex: 1,
resizable: true,
sortable: true,
}}
>
<AgGridColumn
headerName={t('Market')}
field="market.tradableInstrument.instrument.name"
valueFormatter={({
const coldefs = useMemo(() => {
const defs: ColDef[] = [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.name',
valueFormatter: ({
value,
}: VegaValueFormatterParams<
AccountFields,
@@ -69,32 +40,101 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
>) => {
if (!value) return 'None';
return value;
}}
minWidth={200}
/>
<AgGridColumn
headerName={t('Account type')}
field="type"
maxWidth={300}
valueFormatter={({
},
minWidth: 200,
},
{
headerName: t('Account type'),
field: 'type',
maxWidth: 300,
valueFormatter: ({
value,
}: VegaValueFormatterParams<AccountFields, 'type'>) =>
value
}: VegaValueFormatterParams<AccountFields, 'type'>) => {
return value
? AccountTypeMapping[value as keyof typeof AccountTypeMapping]
: ''
}
/>
: '';
},
},
{
headerName: t('Balance'),
field: 'used',
flex: 2,
maxWidth: 500,
type: 'rightAligned',
tooltipComponent: TooltipCellComponent,
tooltipValueGetter: ({ value, data }) => {
return addDecimalsFormatNumber(value, data.asset.decimals);
},
cellRenderer: ({
data,
node,
}: VegaICellRendererParams<AccountFields, 'used'>) => {
if (!data || node?.rowPinned) {
return undefined;
}
const min = BigInt(data.used);
const mid = BigInt(data.available);
const max = BigInt(data.total);
const range = max > min ? max : min;
const formattedData = {
low: addDecimalsFormatNumberQuantum(
min.toString(),
data.asset.decimals,
data.asset.quantum
),
high: addDecimalsFormatNumberQuantum(
mid.toString(),
data.asset.decimals,
data.asset.quantum
),
value: range ? Number((min * BigInt(100)) / range) : 0,
intent: Intent.Warning,
};
return <ProgressBarCell valueFormatted={formattedData} />;
},
comparator: accountValuesComparator,
},
{
headerName: t('Margin health'),
field: 'market.id',
flex: 2,
maxWidth: 500,
sortable: false,
cellRenderer: ({
data,
}: VegaICellRendererParams<AccountFields, 'market.id'>) =>
data?.market?.id &&
data.type === AccountType['ACCOUNT_TYPE_MARGIN'] &&
data?.asset.id ? (
<MarginHealthChart
marketId={data.market.id}
assetId={data.asset.id}
/>
) : null,
},
];
return defs;
}, []);
<AgGridColumn
headerName={t('Balance')}
field="used"
flex={2}
maxWidth={500}
cellRendererSelector={progressBarCellRendererSelector}
valueFormatter={progressBarValueFormatter}
comparator={accountValuesComparator}
/>
</AgGrid>
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('Collateral not used')}
rowData={data}
getRowId={({ data }: { data: AccountFields }) =>
`${data.asset.id}:${data.type}:${data.market?.id}`
}
ref={ref}
rowHeight={34}
components={{ PriceCell, MarketNameCell, ProgressBarCell }}
tooltipShowDelay={500}
defaultColDef={{
flex: 1,
resizable: true,
sortable: true,
}}
columnDefs={coldefs}
/>
);
}
);
+3
View File
@@ -8,3 +8,6 @@ export * from './use-account-balance';
export * from './get-settlement-account';
export * from './use-market-account-balance';
export * from './transfer-dialog';
export * from './__generated__/Margins';
export { MarginHealthChart } from './margin-health-chart';
export * from './margin-data-provider';
@@ -7,13 +7,13 @@ import {
import {
MarginsSubscriptionDocument,
MarginsDocument,
} from './__generated__/Positions';
} from './__generated__/Margins';
import type {
MarginsQuery,
MarginFieldsFragment,
MarginsSubscriptionSubscription,
MarginsQueryVariables,
} from './__generated__/Positions';
} from './__generated__/Margins';
const update = (
data: MarginFieldsFragment[] | null,
@@ -0,0 +1,242 @@
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { Tooltip, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketMarginDataProvider } from './margin-data-provider';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { useAccountBalance } from './use-account-balance';
import { useMarketAccountBalance } from './use-market-account-balance';
const MarginHealthChartTooltipRow = ({
label,
value,
decimals,
href,
}: {
label: string;
value: string;
decimals: number;
href?: string;
}) => (
<>
<div
className="float-left clear-left"
key="label"
data-testid="margin-health-tooltip-label"
>
{href ? (
<ExternalLink href={href} target="_blank">
{label}
</ExternalLink>
) : (
label
)}
</div>
<div
className="float-right"
key="value"
data-testid="margin-health-tooltip-value"
>
{addDecimalsFormatNumber(value, decimals)}
</div>
</>
);
export const MarginHealthChartTooltip = ({
maintenanceLevel,
searchLevel,
initialLevel,
collateralReleaseLevel,
decimals,
marginAccountBalance,
}: {
maintenanceLevel: string;
searchLevel: string;
initialLevel: string;
collateralReleaseLevel: string;
decimals: number;
marginAccountBalance?: string;
}) => {
const tooltipContent = [
<MarginHealthChartTooltipRow
key={'maintenance'}
label={t('maintenance level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-maintenance"
value={maintenanceLevel}
decimals={decimals}
/>,
<MarginHealthChartTooltipRow
key={'search'}
label={t('search level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-searching-for-collateral"
value={searchLevel}
decimals={decimals}
/>,
<MarginHealthChartTooltipRow
key={'initial'}
label={t('initial level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-initial"
value={initialLevel}
decimals={decimals}
/>,
<MarginHealthChartTooltipRow
key={'release'}
label={t('release level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-releasing-collateral"
value={collateralReleaseLevel}
decimals={decimals}
/>,
];
if (marginAccountBalance) {
const balance = (
<MarginHealthChartTooltipRow
key={'balance'}
label={t('balance')}
value={marginAccountBalance}
decimals={decimals}
/>
);
if (BigInt(marginAccountBalance) < BigInt(searchLevel)) {
tooltipContent.splice(1, 0, balance);
} else if (BigInt(marginAccountBalance) < BigInt(initialLevel)) {
tooltipContent.splice(2, 0, balance);
} else if (BigInt(marginAccountBalance) < BigInt(collateralReleaseLevel)) {
tooltipContent.splice(3, 0, balance);
} else {
tooltipContent.push(balance);
}
}
return (
<div className="overflow-hidden" data-testid="margin-health-tooltip">
{tooltipContent}
</div>
);
};
export const MarginHealthChart = ({
marketId,
assetId,
}: {
marketId: string;
assetId: string;
}) => {
const { data: assetsMap } = useAssetsMapProvider();
const { pubKey: partyId } = useVegaWallet();
const { data } = useDataProvider({
dataProvider: marketMarginDataProvider,
variables: { marketId, partyId: partyId ?? '' },
skip: !partyId,
});
const { accountBalance: rawGeneralAccountBalance } =
useAccountBalance(assetId);
const { accountBalance: rawMarginAccountBalance } =
useMarketAccountBalance(marketId);
const asset = assetsMap && assetsMap[assetId];
if (!data || !asset) {
return null;
}
const { decimals } = asset;
const collateralReleaseLevel = Number(data.collateralReleaseLevel);
const initialLevel = Number(data.initialLevel);
const maintenanceLevel = Number(data.maintenanceLevel);
const searchLevel = Number(data.searchLevel);
const marginAccountBalance = Number(rawMarginAccountBalance);
const generalAccountBalance = Number(rawGeneralAccountBalance);
const max = Math.max(
marginAccountBalance + generalAccountBalance,
collateralReleaseLevel
);
const red = maintenanceLevel / max;
const orange = (searchLevel - maintenanceLevel) / max;
const yellow = ((searchLevel + initialLevel) / 2 - searchLevel) / max;
const green = (collateralReleaseLevel - initialLevel) / max + yellow;
const balanceMarker = marginAccountBalance / max;
const tooltip = (
<MarginHealthChartTooltip
maintenanceLevel={data.maintenanceLevel}
searchLevel={data.searchLevel}
initialLevel={data.initialLevel}
collateralReleaseLevel={data.collateralReleaseLevel}
marginAccountBalance={rawMarginAccountBalance}
decimals={decimals}
/>
);
return (
<div data-testid="margin-health-chart">
{addDecimalsFormatNumber(
(BigInt(marginAccountBalance) - BigInt(maintenanceLevel)).toString(),
decimals
)}{' '}
{t('above')}{' '}
<ExternalLink href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-maintenance">
{t('maintenance level')}
</ExternalLink>
<Tooltip description={tooltip}>
<div
data-testid="margin-health-chart-track"
className="relative bg-vega-green-650"
style={{
height: '6px',
marginBottom: '1px',
display: 'flex',
}}
>
<div
data-testid="margin-health-chart-red"
className="bg-vega-pink-550"
style={{
height: '100%',
width: `${red * 100}%`,
}}
></div>
<div
data-testid="margin-health-chart-orange"
className="bg-vega-orange"
style={{
height: '100%',
width: `${orange * 100}%`,
}}
></div>
<div
data-testid="margin-health-chart-yellow"
className="bg-vega-yellow"
style={{
height: '100%',
width: `${yellow * 100}%`,
}}
></div>
<div
data-testid="margin-health-chart-green"
className="bg-vega-green-600"
style={{
height: '100%',
width: `${green * 100}%`,
}}
></div>
{balanceMarker > 0 && balanceMarker < 100 && (
<div
data-testid="margin-health-chart-balance"
className="absolute bg-vega-blue"
style={{
height: '8px',
width: '8px',
top: '-1px',
transform: 'translate(-4px, 0px)',
borderRadius: '50%',
border: '1px solid white',
backgroundColor: 'blue',
left: `${balanceMarker * 100}%`,
}}
></div>
)}
</div>
</Tooltip>
</div>
);
};
@@ -0,0 +1,154 @@
import {
MarginHealthChart,
MarginHealthChartTooltip,
} from './margin-health-chart';
import { act, render, screen } from '@testing-library/react';
import type { MarginFieldsFragment } from './__generated__/Margins';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
const asset: AssetFieldsFragment = {
id: 'assetId',
decimals: 2,
} as AssetFieldsFragment;
const margins: MarginFieldsFragment = {
asset: {
id: 'assetId',
},
collateralReleaseLevel: '1000',
initialLevel: '800',
searchLevel: '600',
maintenanceLevel: '400',
market: {
id: 'marketId',
},
};
const getMargins = jest.fn(() => margins);
const getBalance = jest.fn(() => '0');
jest.mock('./margin-data-provider', () => ({}));
jest.mock('@vegaprotocol/assets', () => ({
useAssetsMapProvider: () => {
return {
data: {
assetId: asset,
},
};
},
}));
jest.mock('@vegaprotocol/wallet', () => ({
useVegaWallet: () => {
return {
pubKey: 'partyId',
};
},
}));
jest.mock('@vegaprotocol/data-provider', () => ({
useDataProvider: () => {
return {
data: getMargins(),
};
},
}));
jest.mock('./use-account-balance', () => ({
useAccountBalance: () => {
return {
accountBalance: getBalance(),
};
},
}));
jest.mock('./use-market-account-balance', () => ({
useMarketAccountBalance: () => {
return {
accountBalance: '700',
};
},
}));
describe('MarginHealthChart', () => {
it('should render correct values', async () => {
render(<MarginHealthChart marketId="marketId" assetId="assetId" />);
const chart = screen.getByTestId('margin-health-chart');
expect(chart).toHaveTextContent('3.00 above maintenance level');
const red = screen.getByTestId('margin-health-chart-red');
const orange = screen.getByTestId('margin-health-chart-orange');
const yellow = screen.getByTestId('margin-health-chart-yellow');
const green = screen.getByTestId('margin-health-chart-green');
const balance = screen.getByTestId('margin-health-chart-balance');
expect(parseInt(red.style.width)).toBe(40);
expect(parseInt(orange.style.width)).toBe(20);
expect(parseInt(yellow.style.width)).toBe(10);
expect(parseInt(green.style.width)).toBe(30);
expect(parseInt(balance.style.left)).toBe(70);
});
it('should use correct scale', async () => {
getBalance.mockReturnValueOnce('1300');
await act(async () => {
render(<MarginHealthChart marketId="marketId" assetId="assetId" />);
});
await screen.findByTestId('margin-health-chart');
const red = screen.getByTestId('margin-health-chart-red');
expect(parseInt(red.style.width)).toBe(20);
});
});
describe('MarginHealthChartTooltip', () => {
it('renders correct values and labels', async () => {
await act(async () => {
render(
<MarginHealthChartTooltip
{...margins}
decimals={asset.decimals}
marginAccountBalance="500"
/>
);
});
const labels = await screen.findAllByTestId('margin-health-tooltip-label');
const expectedLabels = [
'maintenance level',
'balance',
'search level',
'initial level',
'release level',
];
labels.forEach((value, i) => {
expect(value).toHaveTextContent(expectedLabels[i]);
});
const values = await screen.findAllByTestId('margin-health-tooltip-value');
const expectedValues = ['4.00', '5.00', '6.00', '8.00', '10.00'];
values.forEach((value, i) => {
expect(value).toHaveTextContent(expectedValues[i]);
});
});
it('renders balance in correct place', async () => {
const { rerender } = render(
<MarginHealthChartTooltip
{...margins}
decimals={asset.decimals}
marginAccountBalance="700"
/>
);
let values = await screen.findAllByTestId('margin-health-tooltip-value');
expect(values[2]).toHaveTextContent('7.00');
rerender(
<MarginHealthChartTooltip
{...margins}
decimals={asset.decimals}
marginAccountBalance="900"
/>
);
values = await screen.findAllByTestId('margin-health-tooltip-value');
expect(values.length).toBe(5);
expect(values[3]).toHaveTextContent('9.00');
});
});
+8
View File
@@ -290,6 +290,14 @@ export const TransferFee = ({
decimals?: number;
}) => {
if (!feeFactor || !amount || !transferAmount || !fee) return null;
if (
isNaN(Number(feeFactor)) ||
isNaN(Number(amount)) ||
isNaN(Number(transferAmount)) ||
isNaN(Number(fee))
) {
return null;
}
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
@@ -23,7 +23,6 @@ export const useAccountBalance = (assetId?: string) => {
},
[assetId]
);
useDataProvider({
dataProvider: accountsDataProvider,
variables,
@@ -22,7 +22,6 @@ export const useMarketAccountBalance = (marketId: string) => {
},
[marketId]
);
useDataProvider({
dataProvider: accountsDataProvider,
variables: { partyId: pubKey || '' },
+1
View File
@@ -28,3 +28,4 @@ export * from '../trades/src/lib/trades.mock';
export * from '../withdraws/src/lib/withdrawal.mock';
export * from '../proposals/src/lib/protocol-upgrade-proposals/protocol-statistics-proposals.mock';
export * from '../proposals/src/lib/protocol-upgrade-proposals/block-statistics.mock';
export * from '../liquidity/src/lib/liquidity.mock';

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