Compare commits

..
Author SHA1 Message Date
sam-keen e9691e0950 feat(3742): tweak to imports after merge from develop 2023-05-17 09:04:46 +01:00
sam-keen 688eb6f5cb Merge remote-tracking branch 'origin/develop' into fix/3742-unknown-pubkey-validator-error
# Conflicts:
#	apps/governance/src/routes/staking/node/nodes-container.tsx
2023-05-17 09:03:56 +01:00
daro-maj 09778a5d3f test(trading): pegged orders e2e tests (#3787) 2023-05-17 09:54:19 +02:00
MaciekandMatthew Russell 59993e3ea6 chore(trading): suppress sentry logs when eth fails (#3726)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-05-16 23:31:53 -07:00
dexturr 54161a6dba chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-17 06:07:24 +00:00
Matthew Russell 26f524ea07 feat(trading): sidebar market list adjustments (#3792) 2023-05-16 18:45:17 -07:00
dexturr db13e6f21d chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-17 00:17:38 +00:00
dexturr 4a679813ba chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-16 18:07:35 +00:00
Matthew Russell a4279d5b5e feat(trading): sidebar market list (#3776) 2023-05-16 16:57:36 +00:00
m.ray 1a274a67c3 feat(trading): oracle banner check termination and settlement data oracles (#3707) 2023-05-16 15:12:22 +02:00
dexturr 1e9ff31fd7 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-16 12:08:54 +00:00
Sam Keen aa2b755b93 fix(governance): reward aggregation for validator types (#3773) 2023-05-16 08:40:36 +01:00
Matthew Russell 4a7296312a fix(smart-contracts,deposits): add different abi for tether (#3775) 2023-05-16 08:40:05 +01:00
dexturr 1418a2259f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-16 06:07:24 +00:00
dexturr 7e5ef0157a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-16 00:14:15 +00:00
dexturr 0cf764f11e chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-15 18:09:01 +00:00
Sam Keen 1c626aeeb2 fix(governance): useWeb3ConnectStore instead of app state for eth connect d… (#3750) 2023-05-15 16:21:20 +01:00
Bartłomiej Głownia c4cec47b0b feat(trading): add oracle dialog trigger to oracle banner, display status reas… (#3727) 2023-05-15 16:20:30 +01:00
sam-keen fbe1233178 fix(3742): correct apollo error policy for the staking query allows for simpler error handling 2023-05-15 16:16:02 +01:00
sam-keen d4abb4db39 fix(3742): fixing claim url in sentry capture 2023-05-15 15:27:11 +01:00
sam-keen fc994ba52b fix(3742): do not send error to sentry 2023-05-15 15:24:38 +01:00
dexturr 1a67c41bb4 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-15 12:08:57 +00:00
Matthew Russell fc66240390 fix(governance): check value of telemetry local storage entry (#3762) 2023-05-15 09:48:42 +01:00
Maciek fc0da98683 fix(trading): match link as active more strictly (#3754) 2023-05-15 10:08:21 +02:00
Maciek 4d0dafb113 fix(trading): match link as active more strictly (#3754) 2023-05-15 10:08:04 +02:00
dexturr eda431be9e chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-15 06:07:48 +00:00
dexturr 4ff1749e54 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-15 00:17:19 +00:00
dexturr e7a108fd7a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-14 18:07:19 +00:00
dexturr 5d3f2ea7d8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-14 12:08:55 +00:00
dexturr 1ab27574ca chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-14 06:09:03 +00:00
dexturr 0ea3c43e78 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-14 00:17:56 +00:00
dexturr 9e183568b0 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-13 18:08:39 +00:00
dexturr 69fd5b26b6 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-13 12:09:50 +00:00
dexturr 2d5b926609 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-13 06:07:23 +00:00
dexturr 98a2ba79a8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-13 00:13:37 +00:00
dexturr 0e6e986937 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-12 18:07:19 +00:00
sam-keen b3b86f95c2 fix(3742): avoid unknown pubkey causing validator page error 2023-05-12 17:26:46 +01:00
Mikołaj Młodzikowski 15d6ea0c05 feat(ci): push directly to main branch on vega.xyz 2023-05-12 17:40:52 +02:00
Mikołaj Młodzikowski 0358ce6293 feat(ci): try to merge to vega.xyz using standard PR 2023-05-12 17:13:33 +02:00
Mikołaj Młodzikowski 0791be4636 feat(ci): push directly to main branch on vega.xyz 2023-05-12 16:40:45 +02:00
Dexter Edwardsandsam-keen 07442848ee fix(governance): claim not encoding ascii correctly (#3747)
Co-authored-by: sam-keen <samuel.kleinmann@gmail.com>
2023-05-12 14:04:06 +00:00
Mikołaj Młodzikowski d5f0bd419e feat(ci): update vega landing page whenever new console is released (#3751) 2023-05-12 15:18:15 +02:00
Sam KeenandBartłomiej Głownia 2c7934a8c7 fix(governance): fix time delay notice for withdrawal of 0.00 assets (#3740)
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
2023-05-12 15:04:53 +02:00
Joe Tsang fae61c28ca chore(cypress): remove cypress project id (#3739) 2023-05-12 13:44:22 +01:00
dexturr fcd4541625 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-12 12:10:20 +00:00
Sam KeenandJoe 8e0a92fdec fix(governance): network upgrade proposal e2e test tweak (#3748)
Co-authored-by: Joe <joe@vega.xyz>
2023-05-12 09:58:10 +01:00
dexturr 6e7109ed87 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-12 06:09:45 +00:00
dexturr f2c698ce06 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-12 00:16:15 +00:00
dexturr ad9a3a3400 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-11 18:10:08 +00:00
Sam Keen efeac49d58 feat(governance): change status labels for network upgrade proposals (#3711) 2023-05-11 15:43:27 +00:00
Mikołaj Młodzikowski 2d430710ab feat(ci): use actual secret in the script 2023-05-11 17:31:12 +02:00
Mikołaj Młodzikowski bca1a98985 fix(ci): remove old dockerfile 2023-05-11 17:16:28 +02:00
Edd 8a080d3279 fix(explorer): fix bug where no votes displayed a yes icon (#3699) 2023-05-11 14:55:00 +00:00
Mikołaj Młodzikowski f121836b4e feat(ci): fleek updates (#3724) 2023-05-11 15:42:35 +02:00
Sam Keen b641c82ad8 feat(governance): voting blog link and button title (#3717) 2023-05-11 12:59:28 +00:00
Sam Keen bf73559c30 fix(governance): change proposal sort order (#3716) 2023-05-11 12:39:30 +00:00
dexturr 653cec2592 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-11 12:07:57 +00:00
m.ray 579c884a5a feat(trading): show pegged order offset and reference in the order table (#3675) 2023-05-11 12:43:20 +01:00
Bartłomiej Głownia b2279c7e47 feat(trading): hook up new EstimatePosition and EstimateFees api methods (#3634) 2023-05-11 12:58:12 +02:00
Sam Keen a8c17b6807 fix(governance): tranches data (#3708) 2023-05-11 10:22:52 +00:00
Sam Keen b4c7dc6f59 fix(governance): more thorough catch for url in error reporting (#3686) 2023-05-11 10:21:16 +00:00
daro-maj d44392bebf test(trading): show full oracle profile info in markets test (#3695) 2023-05-11 08:18:00 +02:00
dexturr bd679957e2 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-11 06:07:33 +00:00
dexturr e30d48555e chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-11 00:16:19 +00:00
Sam Keen b4b2416780 fix(governance): my stake share 2 dp (#3685) 2023-05-10 16:23:37 +00:00
Maciek dc7832ac81 chore(candles-chart): fill up missing candles (#3664) 2023-05-10 15:26:27 +00:00
Joe Tsang 91207d31ee chore(governance): fix failing validator tests (#3703) 2023-05-10 15:03:47 +00:00
dexturr 58efb460a6 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-10 12:10:00 +00:00
Mikołaj Młodzikowski 044f98777b feat(ci): lint makefile 2023-05-10 13:43:33 +02:00
Mikołaj Młodzikowski 00fdc5e81a fix(ci): preparing dist bash syntax 2023-05-10 13:33:17 +02:00
Mikołaj Młodzikowski adfad3bafb feat(ci): fix typo 2023-05-10 13:21:53 +02:00
dexturr e6ae88905c chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-10 06:07:15 +00:00
dexturr 3953ed9953 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-10 00:15:02 +00:00
Matthew Russell b48ab58e6a chore: add VEGA_URL for explorer mainnet 2023-05-09 14:06:10 -07:00
Matthew Russell e44cf1ff53 chore(environment,trading): fix unit tests for footer and node switcher (#3682) 2023-05-09 13:39:17 -07:00
Matthew Russell bded1d32ba feat(explorer,governance): always allow selecting a node, add node guard (#3678) 2023-05-09 12:58:09 -07:00
Edd 76ddf45f4c fix(explorer): add envs back to env (#3677) 2023-05-09 12:57:38 -07:00
Dexter Edwards 6a2c9004d8 fix(governance, console, explorer): show vega node in list, cache bust (#3676) 2023-05-09 12:57:11 -07:00
dexturr 77ee88b43e chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-09 18:09:50 +00:00
Dexter e4958ce3c0 fix: do not show validators with 0 self stake in list 2023-05-09 18:25:20 +01:00
Mikołaj Młodzikowski ecedb879f8 fix(ci): define which app ipfs hash is set for 2023-05-09 18:47:50 +02:00
m.ray 89700803d6 fix(trading): fix release script for mainnet (#3669) 2023-05-09 16:30:05 +00:00
Mikołaj Młodzikowski 65c9fa5df2 feat(ci): establish build based on docker to produce ipfs-hashes (#3612) 2023-05-09 18:24:39 +02:00
ArtandMadalina Raicu 02399c40fc fix(trading): welcome dialog links (#3656)
Co-authored-by: Madalina Raicu <madalina@raygroup.uk>
2023-05-09 15:43:36 +00:00
272 changed files with 6269 additions and 3516 deletions
+3 -3
View File
@@ -5,6 +5,8 @@ on:
branches:
- release/*
- develop
tags:
- v*
pull_request:
types:
- opened
@@ -97,15 +99,13 @@ jobs:
- name: See affected apps
run: |
echo ">>>> debug"
echo "NX Version: $nx_version"
echo "NX_BASE: ${{ env.NX_BASE }}"
echo "NX_HEAD: ${{ env.NX_HEAD }}"
echo ">>>> eof debug"
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
echo -n "Affected projects: $affected"
branch_slug="$(echo ${{ github.head_ref || github.ref_name }} | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
branch_slug="$(echo '${{ github.head_ref || github.ref_name }}' | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
projects_e2e=""
preview_governance="not deployed"
preview_trading="not deployed"
-1
View File
@@ -33,5 +33,4 @@ jobs:
config: baseUrl=${{ github.event.inputs.url }}
env: grepTags=@live
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -66,7 +66,7 @@ jobs:
######
- name: Run Cypress tests
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome --env.grepTags="${{ inputs.tags }}"
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --browser chrome --env.grepTags="${{ inputs.tags }}"
working-directory: frontend-monorepo
env:
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
+90 -34
View File
@@ -30,13 +30,21 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry
- name: Log in to the Container registry (ghcr)
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
with:
# registry: registry.hub.docker.com
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup node
uses: actions/setup-node@v3
with:
@@ -54,30 +62,24 @@ jobs:
- name: Define variables
run: |
envName=''
dockerfile="dist.Dockerfile"
if [[ "${{ github.event_name }}" = "push" ]]; then
domain="vega.rocks"
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
if [[ "${{ github.ref }}" =~ .*mainnet.* ]]; then
domain="vega.community"
if [[ "${{ matrix.app }}" = "trading" ]]; then
dockerfile="ipfs.Dockerfile"
fi
fi
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
envName="stagnet1"
elif [[ "${{ matrix.app}}" = "trading" ]] && [[ ${{ startsWith(github.ref, 'refs/tags/v') && 'true' || 'false' }} = "true" ]]; then
envName="mainnet"
fi
bucketName="${{ matrix.app }}.${envName}.${domain}"
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
fi
nodeVersion=$(cat .nvmrc | head -n 1)
echo ENV_NAME=${envName} >> $GITHUB_ENV
echo NODE_VERSION=${nodeVersion} >> $GITHUB_ENV
echo DOCKERFILE=docker/${dockerfile} >> $GITHUB_ENV
- name: Build local dist
if: ${{ env.DOCKERFILE != 'docker/ipfs.Dockerfile' }}
run: |
flags=""
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
@@ -98,62 +100,60 @@ jobs:
- name: Build and export to local Docker
id: docker_build
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
uses: docker/build-push-action@v3
with:
context: .
file: ${{ env.DOCKERFILE }}
file: docker/node-outside-docker.Dockerfile
load: true
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ env.NODE_VERSION }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
- name: Image digest
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
if: ${{ github.event_name == 'pull_request' }}
run: echo ${{ steps.docker_build.outputs.digest }}
- name: Sanity check docker image
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
run: |
echo "Check ipfs-hash"
if [[ "${{ env.DOCKERFILE }}" = "docker/ipfs.Dockerfile" ]]; then
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
fi
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash > ${{ matrix.app }}-ipfs-hash
echo "List html directory"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree .'
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree /usr/share/nginx/html'
- name: Copy dist to local filesystem
if: ${{ env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' }}
run: |
docker create --name=dist ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
docker cp dist:/usr/share/nginx/html dist
echo "check local dist files"
tree dist/html
mv dist/html dist-result
- name: Publish dist as docker image
- name: Publish dist as docker image (ghcr)
uses: docker/build-push-action@v3
if: ${{ github.event_name == 'pull_request' }}
if: ${{ github.event_name == 'pull_request' || (matrix.app == 'trading' && github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') ) }}
with:
context: .
file: ${{ env.DOCKERFILE }}
file: docker/node-outside-docker.Dockerfile
push: true
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ env.NODE_VERSION }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
- 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') }}
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.ref_name }}
vegaprotocol/${{ matrix.app }}:mainnet
# bucket creation in github.com/vegaprotocol/terraform//frontend
- name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master
if: ${{ github.event_name == 'push' }}
if: ${{ github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') }}
with:
args: --acl private --follow-symlinks --delete
env:
@@ -169,3 +169,59 @@ jobs:
with:
labels: ${{ matrix.app }}-preview
number: ${{ github.event.number }}
- name: Add ipfs hash to release
uses: softprops/action-gh-release@v1
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
with:
files: ${{ matrix.app }}-ipfs-hash
- name: Trigger fleek deployment
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
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
# 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
- name: Checkout vega.xyz
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
uses: actions/checkout@v3
with:
repository: vegaprotocol/vega.xyz
path: './vega-xyz'
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update hash on interstitial page
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
run: |
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"
which ipfs
new_hash=$(cat ${{ matrix.app }}-ipfs-hash)
cd vega-xyz
./interstital-allow-update.sh ${new_hash}
git config --global user.email "vega-ci-bot@vega.xyz"
git config --global user.name "vega-ci-bot"
git add interstitial-allow.json netlify.toml
commit_msg="feat(ci): Update CID for console release @ ${{ github.ref }}"
git commit -m "${commit_msg}"
git push origin main
# branch_name=${{ github.ref_name }}-hash-update
# git checkout -b "${branch_name}"
# git add interstitial-allow.json netlify.toml
# git push -u origin "${branch_name}"
# pr_url="$(gh pr create --title "${commit_msg}" --body 'update ipfs hash for console @ ${{ github.ref }}')"
# echo $pr_url
# gh pr merge --admin --auto $pr_url
+1
View File
@@ -2,6 +2,7 @@
# compiled output
/dist
/dist-result
/tmp
/out-tsc
/tools/executors/**/*.js
+20
View File
@@ -0,0 +1,20 @@
.PHONY: latest-release
latest-release:
gh release list | head -n 1 | awk '{print $1}'
.PHONY: show-latest-release
show-latest-release:
gh release view `gh release list | head -n 1 | awk '{print $1}'`
.PHONY: recalculate-ipfs
recalculate-ipfs:
echo "ipfs hash inside the image"
docker run --rm ${TAG} cat /ipfs-hash
echo "recalculating ipfs hash"
docker run --rm ${TAG} ipfs add -r /usr/share/nginx/html
.PHONY: eject-ipfs-hash
unpack:
docker create --name=dist ${TAG}
docker cp dist:/usr/share/nginx/html dist
docker rm dist
+55 -12
View File
@@ -103,25 +103,68 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
# Docker & Vegacapsule
# 🐋 Hosting a console
## Docker
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
The [Dockerfile](./dockerfiles) for running the frontends is pretty basic, merely building the application with the APP arg that is passed in and serving the application from [nginx](./nginx/nginx.conf). The only complexity that exists is that there is a script which allows the passing of run time environment variables to the containers. See configuration below for how to do this.
You can build any of the containers locally with the following command:
```bash
docker build --dockerfile dockerfiles/Dockerfile.cra . --build-arg APP=[YOUR APP] --tag=[TAG]
```
In order to run a container:
In order to run a container on port 3000:
```bash
docker run -p 3000:80 [TAG]
```
Images ending with `.dist` are to pack locally created transpiled HTML files into nginx container for non-compatible with yarn architectures like M1 Mac
## Build instructions
The [`docker`](./docker) subfolder has some docker configurations for easily setting up your own hosted version of console either for the web, or ready for pinning on IPFS
### nx build outside the docker
Packaging prepared dist into [`nginx`](https://hub.docker.com/_/nginx)([server configuration](./nginx/nginx.conf)) docker image involves building the application on docker host machine from source.
As a prerequisite you need to perform build of `dist` directory and move its content for specific application to `dist-result` directory. Use following script to do it with a single command:
```bash
./docker/prepare-dist.sh
```
You can build any of the containers locally with the following command:
```bash
docker build --dockerfile docker/node-outside-docker.Dockerfile . --tag=[TAG]
```
### nx build inside the docker
Using multistage dockerfile dist is compiled using [node](https://hub.docker.com/_/node) image and later packed to nginx as in [dist build](#dist-build) example.
```bash
docker build --build-arg APP=[YOUR APP] --build-arg NODE_VERSION=$(cat .nvmrc) --build-arg ENV_NAME=mainnet -t [TAG] -f docker/node-inside-docker.Dockerfile .
```
### Computing ipfs-hash of the build
At the moment this feature is important only for `trading` (console) releases.
Each docker build finishes with hash calculation for dist directory. Resulting hash is added to file named as `/ipfs-hash`. Once docker image is produced you can run following commad to display ipfs-hash:
```bash
make recalculate-ipfs TAG=vegaprotocol/trading:{YOUR_VERSION}
```
**updating hash:** recompiling dist directory (even if there are no changed to source code) results in different hash computed by ipfs command.
### Verifying ipfs-hash of existing current application version
An IPFS CID will be attached to every [release](https://github.com/vegaprotocol/frontend-monorepo/releases). If you are intending to pin an application on IPFS, you can check that your build matches by running the following steps:
1. Show latest release by runnning: `make latest-release`. You need to configure [`gh`](https://cli.github.com/) for this step to work, otherwise please provide release manually from [github](https://github.com/vegaprotocol/frontend-monorepo/releases) or [dockerhub](https://hub.docker.com/r/vegaprotocol/trading)
2. Set RELEASE environment variable to value that you want to validate: `export RELEASE=$(make latest-release)` or `export RELEASE=vXX.XX.XX`
3. Set TAG environment variable to image that you want to validate: `export TAG=vegaprotocol/trading:$RELEASE`
4. Download docker image with the desired release `docker pull $TAG`.
5. Recalculate hash: `make recalculate-ipfs`
6. You should see exactly same hash produced by ipfs command as one placed with the release notes: `make show-latest-release`
7. If you want to extract dist from docker image to your local filesystem you can run following command: `make unpack`
8. Now `dist` directory contains valid application build. **it is not possible to calculate same ipfs hash on files that are result of copy operation**
## Config
+1
View File
@@ -13,6 +13,7 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_VEGA_GOVERNANCE_URL=https://stagnet1.governance.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.jsoo
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}'
# App flags
NX_EXPLORER_ASSETS=1
+1
View File
@@ -2,6 +2,7 @@
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
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_ETHERSCAN_URL=https://etherscan.io
+2 -1
View File
@@ -1 +1,2 @@
# .env is stagnet1, so there are no overrides required
# .env is stagnet1, so there are no overrides required
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz","STAGNET3":"https://stagnet3.explorer.vega.xyz"}'
+24 -2
View File
@@ -1,9 +1,18 @@
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
import {
AppFailure,
NetworkLoader,
NodeGuard,
NodeSwitcherDialog,
useEnvironment,
useInitializeEnv,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router-config';
import { t } from '@vegaprotocol/i18n';
const splashLoading = (
<Splash>
@@ -12,10 +21,23 @@ const splashLoading = (
);
function App() {
const { VEGA_URL } = useEnvironment();
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useNodeSwitcherStore(
(store) => [store.dialogOpen, store.setDialogOpen]
);
return (
<TendermintWebsocketProvider>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<RouterProvider router={router} fallbackElement={splashLoading} />
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
>
<RouterProvider router={router} fallbackElement={splashLoading} />
</NodeGuard>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</NetworkLoader>
</TendermintWebsocketProvider>
);
@@ -1,13 +1,19 @@
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment';
import {
useEnvironment,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
import { useMemo, useState } from 'react';
import { useMemo } from 'react';
import { ENV } from '../../config/env';
export const Footer = () => {
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
const setNodeSwitcherOpen = useNodeSwitcherStore(
(store) => store.setDialogOpen
);
const { screenSize } = useScreenDimensions();
const showFullFeedbackLabel = useMemo(
() => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
@@ -15,46 +21,40 @@ export const Footer = () => {
);
return (
<>
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
<div className="flex justify-between gap-2 align-middle">
{GIT_COMMIT_HASH && (
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
<p data-testid="git-commit-hash">
{t('Version')}:{' '}
<Link
href={
GIT_ORIGIN_URL
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
: undefined
}
target={GIT_ORIGIN_URL ? '_blank' : undefined}
>
{GIT_COMMIT_HASH}
</Link>
</p>
</div>
)}
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
{t('Change')}
</Link>
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
<div className="flex justify-between gap-2 align-middle">
{GIT_COMMIT_HASH && (
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
<p data-testid="git-commit-hash">
{t('Version')}:{' '}
<Link
href={
GIT_ORIGIN_URL
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
: undefined
}
target={GIT_ORIGIN_URL ? '_blank' : undefined}
>
{GIT_COMMIT_HASH}
</Link>
</p>
</div>
)}
<div className="flex pl-2 content-center">
<ExternalLink href={ENV.addresses.feedback}>
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
</ExternalLink>
</div>
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
{t('Change')}
</Link>
</div>
</footer>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</>
<div className="flex pl-2 content-center">
<ExternalLink href={ENV.addresses.feedback}>
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
</ExternalLink>
</div>
</div>
</footer>
);
};
@@ -1,3 +1,5 @@
import { Icon } from '@vegaprotocol/ui-toolkit';
// https://github.com/vegaprotocol/vega/blob/develop/core/blockchain/response.go
export const ErrorCodes = new Map([
[51, 'Transaction failed validation'],
@@ -28,7 +30,11 @@ export const ChainResponseCode = ({
}: ChainResponseCodeProps) => {
const isSuccess = successCodes.has(code);
const icon = isSuccess ? '✅' : '❌';
const icon = isSuccess ? (
<Icon name="tick-circle" className="fill-vega-green-550" />
) : (
<Icon name="cross" className="fill-vega-pink-550" />
);
const label = ErrorCodes.get(code) || 'Unknown response code';
// Hack for batches with many errors - see https://github.com/vegaprotocol/vega/issues/7245
@@ -36,7 +42,7 @@ export const ChainResponseCode = ({
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
return (
<div title={`Response code: ${code} - ${label}`} className="inline-block">
<div title={`Response code: ${code} - ${label}`} className=" inline-block">
<span
className="mr-2"
aria-label={isSuccess ? 'Success' : 'Warning'}
@@ -4,6 +4,7 @@ import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import ProposalLink from '../../links/proposal-link/proposal-link';
import { VoteIcon } from '../../vote-icon/vote-icon';
interface TxProposalVoteProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -30,27 +31,22 @@ export const TxProposalVote = ({
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const vote = txData.command.voteSubmission.value ? '👍' : '👎';
const vote = txData.command.voteSubmission.value === 'VALUE_YES';
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Proposal ID')}</TableCell>
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Proposal details')}</TableCell>
<TableCell>
<ProposalLink id={txData.command.voteSubmission.proposalId} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Proposal')}</TableCell>
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Vote')}</TableCell>
<TableCell>{vote}</TableCell>
<TableCell>
<VoteIcon vote={vote} />
</TableCell>
</TableRow>
</TableWithTbody>
);
@@ -1,5 +1,6 @@
import { t } from '@vegaprotocol/i18n';
import type { components } from '../../../types/explorer';
import { VoteIcon } from '../vote-icon/vote-icon';
interface TxOrderTypeProps {
orderType: string;
@@ -137,12 +138,15 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
let type = displayString[orderType] || orderType;
let colours =
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-150';
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-250';
// This will get unwieldy and should probably produce a different colour of tag
if (type === 'Chain Event' && !!command?.chainEvent) {
type = getLabelForChainEvent(command.chainEvent);
colours = 'text-white dark-text-white bg-vega-pink dark:bg-vega-pink';
} else if (type === 'Validator Heartbeat') {
colours =
'text-white dark-text-white bg-vega-light-200 dark:bg-vega-dark-100';
} else if (type === 'Proposal' || type === 'Governance Proposal') {
if (command && !!command.proposalSubmission) {
type = getLabelForProposal(command.proposalSubmission);
@@ -150,6 +154,16 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
colours = 'text-black bg-vega-yellow';
}
if (type === 'Vote on Proposal') {
return (
<VoteIcon
vote={command?.voteSubmission?.value === 'VALUE_YES'}
yesText="Proposal vote"
noText="Proposal vote"
/>
);
}
if (type === 'Vote on Proposal' || type === 'Vote Submission') {
colours = 'text-black bg-vega-yellow';
}
@@ -98,6 +98,6 @@ describe('Txs infinite list item', () => {
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
expect(screen.getByTestId('tx-success')).toHaveTextContent('Success: ✅');
expect(screen.getByTestId('tx-success')).toHaveTextContent('Success');
});
});
@@ -31,7 +31,7 @@ export const TxsInfiniteListItem = ({
return (
<div
data-testid="transaction-row"
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10 py-2"
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10"
>
<div
className="text-sm col-span-10 md:col-span-3 leading-none"
@@ -83,7 +83,7 @@ export const TxsInfiniteListItem = ({
data-testid="tx-success"
>
<span className="md:hidden uppercase text-vega-dark-300">
Success:&nbsp;
Success&nbsp;
</span>
{isNumber(code) ? (
<ChainResponseCode code={code} hideLabel={true} />
@@ -0,0 +1,37 @@
import { render } from '@testing-library/react';
import { VoteIcon } from './vote-icon';
describe('Vote TX icon', () => {
it('should use the text For by default for yes votes', () => {
const yes = render(<VoteIcon vote={true} />);
expect(yes.getByTestId('label')).toHaveTextContent('For');
});
it('should use the yesText for yes votes if specified', () => {
const yes = render(<VoteIcon vote={true} yesText="Test" />);
expect(yes.getByTestId('label')).toHaveTextContent('Test');
});
it('should display the tick icon for yes votes', () => {
const no = render(<VoteIcon vote={true} />);
expect(no.getByRole('img')).toHaveAttribute(
'aria-label',
'tick-circle icon'
);
});
it('should use the text Against by default for no votes', () => {
const no = render(<VoteIcon vote={false} />);
expect(no.getByTestId('label')).toHaveTextContent('Against');
});
it('should use the noText for no votes if specified', () => {
const no = render(<VoteIcon vote={false} noText="Test" />);
expect(no.getByTestId('label')).toHaveTextContent('Test');
});
it('should display the delete icon for no votes', () => {
const no = render(<VoteIcon vote={false} />);
expect(no.getByRole('img')).toHaveAttribute('aria-label', 'delete icon');
});
});
@@ -0,0 +1,40 @@
import { Icon } from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@vegaprotocol/ui-toolkit';
export interface VoteIconProps {
// True is a yes vote, false is undefined or no vorte
vote: boolean;
// Defaults to 'For', but can be any text
yesText?: string;
// Defaults to 'Against', but can be any text
noText?: string;
}
/**
* Displays a lozenge with an icon representing the way a user voted for a proposal.
* The yes and no text can be overridden
*
* @returns
*/
export function VoteIcon({
vote,
yesText = 'For',
noText = 'Against',
}: VoteIconProps) {
const label = vote ? yesText : noText;
const bg = vote ? 'bg-vega-green-550' : 'bg-vega-pink-550';
const icon: IconName = vote ? 'tick-circle' : 'delete';
const fill = vote ? 'vega-green-300' : 'vega-pink-300';
const text = vote ? 'vega-green-200' : 'vega-pink-200';
return (
<div
className={`voteicon inline-block my-1 py-1 px-2 py rounded-md text-white leading-one sm align-top ${bg}`}
>
<Icon name={icon} size={3} className={`mr-2 p-0 fill-${fill}`} />
<span className={`text-base text-${text}`} data-testid="label">
{label}
</span>
</div>
);
}
+4
View File
@@ -60,3 +60,7 @@
--ag-row-hover-color: theme(colors.neutral[800]);
--ag-font-size: 12px;
}
.voteicon svg {
vertical-align: baseline;
}
@@ -7,6 +7,7 @@ import {
waitForSpinner,
navigateTo,
navigation,
turnTelemetryOff,
} from '../../support/common.functions';
import {
clickOnValidatorFromList,
@@ -67,6 +68,7 @@ context(
'teardown wallet & drill into a specific validator',
function () {
cy.clearLocalStorage();
turnTelemetryOff();
cy.reload();
waitForSpinner();
cy.connectVegaWallet();
@@ -252,10 +254,10 @@ context(
waitForBeginningOfEpoch();
cy.getByTestId(stakeValidatorListStakePercentage).should(
'have.text',
'100%'
'50.02%'
);
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '2.00', '100.00%');
validateValidatorListTotalStakeAndShare('0', '3,002.00', '50.02%');
}
);
@@ -109,7 +109,7 @@ context(
);
cy.getByTestId('protocol-upgrade-proposal-status').should(
'have.text',
'Approved '
'Approved by validators '
);
});
});
@@ -136,7 +136,7 @@ context(
);
cy.getByTestId('protocol-upgrade-state').should(
'have.text',
'Approved'
'Approved by validators'
);
cy.getByTestId('protocol-upgrade-release-tag').should(
'have.text',
@@ -29,7 +29,7 @@ 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*\.?\d*$/;
const stakeNumberRegex = /^\d{1,3}(,\d{3})*(\.\d+)?$/;
context('Validators Page - verify elements on page', function () {
before('navigate to validators page', function () {
@@ -84,13 +84,13 @@ context('Validators Page - verify elements on page', function () {
cy.get(stakedByOperatorToolTip)
.invoke('text')
.should('contain', 'Staked by operator: 0.00');
.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: 0.00');
.should('contain', 'Total stake: 3,000.00');
});
it('Should be able to see validator normalised voting power', function () {
@@ -106,10 +106,10 @@ context('Validators Page - verify elements on page', function () {
cy.get(unnormalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Unnormalised voting power: 0.00%');
.should('contain', 'Unnormalised voting power: 20.00%');
cy.get(normalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Normalised voting power: 0.10%');
.should('contain', 'Normalised voting power: 50.00%');
});
// 2002-SINC-018
@@ -126,13 +126,13 @@ context('Validators Page - verify elements on page', function () {
cy.get(performancePenaltyToolTip)
.invoke('text')
.should('contain', 'Performance penalty: 100.00%');
.should('contain', 'Performance penalty: 0.00%');
cy.get(overstakedPenaltyToolTip)
.invoke('text')
.should('contain', 'Overstaked penalty:'); // value not asserted due to #2886
.should('contain', 'Overstaked penalty: 60.00%'); // value not asserted due to #2886
cy.get(totalPenaltyToolTip)
.invoke('text')
.should('contain', 'Total penalties: 0.00%');
.should('contain', 'Total penalties: 60.00%');
});
it('Should be able to see validator pending stake', function () {
+2
View File
@@ -33,4 +33,6 @@ before(() => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
// Self stake validators so they are displayed
cy.validatorsSelfDelegate();
});
@@ -3,7 +3,6 @@ import {
StakingBridge,
Token,
TokenVesting,
TokenFaucetable,
CollateralBridge,
} from '@vegaprotocol/smart-contracts';
import { ethers, Wallet } from 'ethers';
@@ -41,7 +40,7 @@ export async function depositAsset(
decimalPlaces: number
) {
// Approve asset
const faucet = new TokenFaucetable(assetEthAddress, signer);
const faucet = new Token(assetEthAddress, signer);
cy.wrap(
faucet.approve(Erc20BridgeAddress, amount + '0'.repeat(decimalPlaces + 1)),
{
@@ -62,7 +61,7 @@ export async function depositAsset(
}
export async function faucetAsset(assetEthAddress: string) {
const faucet = new TokenFaucetable(assetEthAddress, signer);
const faucet = new Token(assetEthAddress, signer);
await promiseWithTimeout(faucet.faucet(), 10 * 60 * 1000, 'faucet asset');
}
+90 -27
View File
@@ -2,7 +2,6 @@ import './i18n';
import React, { useEffect } from 'react';
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
import { BrowserRouter as Router, useLocation } from 'react-router-dom';
import { AppLoader } from './app-loader';
import { NetworkInfo } from '@vegaprotocol/network-info';
@@ -37,6 +36,10 @@ import {
useEnvironment,
NetworkLoader,
useInitializeEnv,
NodeGuard,
AppFailure,
NodeSwitcherDialog,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { ENV } from './config';
import type { InMemoryCacheConfig } from '@apollo/client';
@@ -48,6 +51,7 @@ import {
TELEMETRY_ON,
} from './components/telemetry-dialog/telemetry-dialog';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useTranslation } from 'react-i18next';
const cache: InMemoryCacheConfig = {
typePolicies: {
@@ -179,32 +183,83 @@ const ScrollToTop = () => {
return null;
};
const removeQueryParams = (url: string) => {
return url.split('?')[0];
};
const AppContainer = () => {
const { config, loading, error } = useEthereumConfig();
const { VEGA_ENV, GIT_COMMIT_HASH, GIT_BRANCH, ETHEREUM_PROVIDER_URL } =
useEnvironment();
const {
VEGA_ENV,
VEGA_URL,
GIT_COMMIT_HASH,
GIT_BRANCH,
ETHEREUM_PROVIDER_URL,
} = useEnvironment();
const [telemetryOn] = useLocalStorage(TELEMETRY_ON);
const { t } = useTranslation();
const [nodeSwitcherOpen, setNodeSwitcher] = useNodeSwitcherStore((store) => [
store.dialogOpen,
store.setDialogOpen,
]);
useEffect(() => {
if (ENV.dsn && telemetryOn) {
if (ENV.dsn && telemetryOn === 'true') {
Sentry.init({
dsn: ENV.dsn,
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 0.1,
enabled: true,
environment: VEGA_ENV,
release: GIT_COMMIT_HASH,
beforeSend(event) {
if (event.request?.url?.includes('/claim?')) {
return {
...event,
request: {
...event.request,
url: event.request?.url.split('?')[0],
},
};
beforeSend(event, hint) {
const error = hint?.originalException;
const errorIsString = typeof error === 'string';
const errorIsObject = error instanceof Error;
const requestUrl = event.request?.url;
const transaction = event.transaction;
if (
(errorIsString && error.includes('failed to get party ID')) ||
(errorIsObject &&
error?.message?.includes('failed to get party ID'))
) {
// This error is caused by a pubkey making an API request before
// it has interacted with the chain. This isn't needed in Sentry.
return null;
}
return event;
const updatedRequest =
requestUrl && requestUrl.includes('/claim?')
? { ...event.request, url: removeQueryParams(requestUrl) }
: event.request;
const updatedTransaction =
transaction && transaction.includes('/claim?')
? removeQueryParams(transaction)
: transaction;
const updatedBreadcrumbs = event.breadcrumbs?.map((breadcrumb) => {
if (
breadcrumb.type === 'navigation' &&
breadcrumb.data?.to?.includes('/claim?')
) {
return {
...breadcrumb,
data: {
...breadcrumb.data,
to: removeQueryParams(breadcrumb.data.to),
},
};
}
return breadcrumb;
});
return {
...event,
request: updatedRequest,
transaction: updatedTransaction,
breadcrumbs: updatedBreadcrumbs ?? event.breadcrumbs,
};
},
});
Sentry.setTag('branch', GIT_BRANCH);
@@ -219,21 +274,29 @@ const AppContainer = () => {
<ScrollToTop />
<AppStateProvider>
<div className="grid min-h-full text-white">
<AsyncRenderer<EthereumConfig | null>
loading={loading}
data={config}
error={error}
render={(cnf) =>
cnf && (
<Web3Container
chainId={Number(cnf.chain_id)}
providerUrl={ETHEREUM_PROVIDER_URL}
/>
)
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={
<AppFailure title={t('NodeUnsuitable', { url: VEGA_URL })} />
}
/>
>
<AsyncRenderer<EthereumConfig | null>
loading={loading}
data={config}
error={error}
render={(cnf) =>
cnf && (
<Web3Container
chainId={Number(cnf.chain_id)}
providerUrl={ETHEREUM_PROVIDER_URL}
/>
)
}
/>
</NodeGuard>
</div>
</AppStateProvider>
<NodeSwitcherDialog open={nodeSwitcherOpen} setOpen={setNodeSwitcher} />
</Router>
);
};
@@ -1,23 +1,14 @@
import { Button } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
export const EthConnectPrompt = () => {
const { t } = useTranslation();
const { appDispatch } = useAppState();
const { open } = useWeb3ConnectStore();
return (
<Button
variant="default"
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
onClick={open}
fill={true}
data-testid="connect-to-eth-btn"
>
@@ -2,10 +2,7 @@ import { Button } from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
export const EthWalletContainer = ({
children,
@@ -14,21 +11,12 @@ export const EthWalletContainer = ({
}) => {
const { account } = useWeb3React();
const { t } = useTranslation();
const { appDispatch } = useAppState();
const { open } = useWeb3ConnectStore();
if (!account) {
return (
<div className="w-full text-center">
<Button
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
>
{t('connectEthWallet')}
</Button>
<Button onClick={open}>{t('connectEthWallet')}</Button>
</div>
);
}
@@ -27,7 +27,11 @@ import {
import { Loader } from '@vegaprotocol/ui-toolkit';
import colors from 'tailwindcss/colors';
import { useBalances } from '../../lib/balances/balances-store';
import { useEthereumConfig, useWeb3Disconnect } from '@vegaprotocol/web3';
import {
useEthereumConfig,
useWeb3ConnectStore,
useWeb3Disconnect,
} from '@vegaprotocol/web3';
import { getChainName } from '@vegaprotocol/web3';
const removeLeadingAddressSymbol = (key: string) => {
@@ -189,6 +193,7 @@ export const EthWallet = () => {
const pendingTxs = usePendingTransactions();
const disconnect = useWeb3Disconnect(connector);
const { config } = useEthereumConfig();
const { open } = useWeb3ConnectStore();
return (
<WalletCard>
@@ -233,12 +238,7 @@ export const EthWallet = () => {
) : (
<Button
fill={true}
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
onClick={open}
data-testid="connect-to-eth-wallet-button"
>
{t('connectEthWalletToAssociate')}
@@ -25,20 +25,26 @@ export function Web3Connector({
connectors,
chainId,
}: Web3ConnectorProps) {
const { appState, appDispatch } = useAppState();
const { open, close, isOpen } = useWeb3ConnectStore();
const setDialogOpen = useCallback(
(isOpen: boolean) => {
appDispatch({ type: AppStateActionType.SET_ETH_WALLET_OVERLAY, isOpen });
if (isOpen) {
open();
} else {
close();
}
},
[appDispatch]
[open, close]
);
const appChainId = Number(chainId);
return (
<>
<Web3Content appChainId={appChainId}>{children}</Web3Content>
<Web3ConnectDialog
connectors={connectors}
dialogOpen={appState.ethConnectOverlay}
dialogOpen={isOpen}
setDialogOpen={setDialogOpen}
desiredChainId={appChainId}
/>
@@ -34,9 +34,6 @@ export interface AppState {
/** Whether or not the manage VEGA wallet overlay is open */
vegaWalletManageOverlay: boolean;
/** Whether or not the connect to Ethereum wallet overlay is open */
ethConnectOverlay: boolean;
/** Whether or not the transaction modal is open */
transactionOverlay: boolean;
/**
@@ -57,7 +54,6 @@ export enum AppStateActionType {
REFRESH_BALANCES,
SET_VEGA_WALLET_OVERLAY,
SET_VEGA_WALLET_MANAGE_OVERLAY,
SET_ETH_WALLET_OVERLAY,
SET_DRAWER,
REFRESH_ASSOCIATED_BALANCES,
SET_ASSOCIATION_BREAKDOWN,
@@ -81,10 +77,6 @@ export type AppStateAction =
type: AppStateActionType.SET_VEGA_WALLET_MANAGE_OVERLAY;
isOpen: boolean;
}
| {
type: AppStateActionType.SET_ETH_WALLET_OVERLAY;
isOpen: boolean;
}
| {
type: AppStateActionType.SET_DRAWER;
isOpen: boolean;
@@ -16,7 +16,6 @@ const initialAppState: AppState = {
totalSupply: new BigNumber(0),
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
@@ -45,12 +44,6 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
vegaWalletOverlay: action.isOpen ? false : state.vegaWalletOverlay,
};
}
case AppStateActionType.SET_ETH_WALLET_OVERLAY: {
return {
...state,
ethConnectOverlay: action.isOpen,
};
}
case AppStateActionType.SET_DRAWER: {
return {
...state,
@@ -202,6 +202,7 @@
"tokenVotes": "Token votes",
"liquidityVotes": "Liquidity votes",
"castYourVote": "Cast your vote",
"yourVote": "Your vote",
"for": "For",
"against": "Against",
"majorityRequired": "Majority Required",
@@ -787,9 +788,9 @@
"homeVegaTokenButtonText": "Manage tokens",
"downloadProposalJson": "Download proposal as JSON",
"networkUpgrade": "Network Upgrade",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED": "Approved",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING": "Pending",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED": "Rejected",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED": "Approved by validators",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING": "Waiting for validator votes",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED": "Declined by validators",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED": "Unspecified",
"vegaRelease{release}": "Vega Release {{release}}",
"upgradeBlockHeight": "Upgrade block height",
@@ -811,5 +812,6 @@
"OptOutOfTelemetry": "You can opt out any time via settings",
"NoThanks": "No thanks",
"ShareData": "Share data",
"ContinueSharingData": "Continue sharing data"
"ContinueSharingData": "Continue sharing data",
"NodeUnsuitable": "Node: {{url}} is unsuitable"
}
@@ -50,7 +50,22 @@ export const useTranches = create<TranchesStore>()((set) => ({
?.map((t) => {
const tranche_progress =
t.duration !== 0 ? (now - t.cliff_start) / t.duration : 0;
const lockedDecimal = tranche_progress < 0 ? 1 : 1 - tranche_progress;
let lockedDecimal;
if (t.duration !== 0) {
if (tranche_progress < 0) {
lockedDecimal = 1;
} else {
lockedDecimal = 1 - tranche_progress;
}
} else {
if (now < t.cliff_start) {
lockedDecimal = 1;
} else {
lockedDecimal = 0;
}
}
const clampedLockedDecimal = Math.max(0, Math.min(1, lockedDecimal));
return {
tranche_id: t.tranche_id,
tranche_start: secondsToDate(t.cliff_start),
@@ -60,7 +75,7 @@ export const useTranches = create<TranchesStore>()((set) => ({
toBigNum(t.current_balance, decimals)
),
locked_amount: toBigNum(t.initial_balance, decimals).times(
lockedDecimal
clampedLockedDecimal
),
users: t.users,
};
@@ -53,7 +53,6 @@ const mockAppState: AppState = {
totalSupply: new BigNumber(65000000),
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
@@ -24,7 +24,6 @@ const openProposalClosesNextMonth = generateProposal({
},
terms: {
closingDatetime: nextMonth.toString(),
enactmentDatetime: nextMonth.toString(),
},
});
@@ -36,7 +35,6 @@ const openProposalClosesNextWeek = generateProposal({
},
terms: {
closingDatetime: nextWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
});
@@ -45,7 +43,6 @@ const enactedProposalClosedLastWeek = generateProposal({
state: ProposalState.STATE_ENACTED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: lastWeek.toString(),
},
});
@@ -54,7 +51,6 @@ const failedProposalClosedLastMonth = generateProposal({
state: ProposalState.STATE_FAILED,
terms: {
closingDatetime: lastMonth.toString(),
enactmentDatetime: lastMonth.toString(),
},
});
@@ -32,7 +32,9 @@ describe('ProtocolUpgradeProposalDetailInfo', () => {
it('should render the state', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('protocol-upgrade-state')).toHaveTextContent('Pending');
expect(getByTestId('protocol-upgrade-state')).toHaveTextContent(
'Waiting for validator votes'
);
});
it('should render the vega release tag', () => {
@@ -26,28 +26,34 @@ describe('ProtocolUpgradeProposalsListItem', () => {
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED,
icon: 'protocol-upgrade-proposal-status-icon-rejected',
text: 'Declined by validators',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
icon: 'protocol-upgrade-proposal-status-icon-pending',
text: 'Waiting for validator votes',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED,
icon: 'protocol-upgrade-proposal-status-icon-approved',
text: 'Approved by validators',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED,
icon: 'protocol-upgrade-proposal-status-icon-unspecified',
text: 'Unspecified',
},
];
statuses.forEach(({ status, icon }) => {
statuses.forEach(({ status, icon, text }) => {
renderComponent({ ...proposal, status });
const statusIcon = screen.getByTestId(icon);
const textContent = screen.getByText(text);
expect(statusIcon).toBeInTheDocument();
expect(textContent).toBeInTheDocument();
});
});
@@ -111,7 +111,9 @@ describe('Vote buttons', () => {
</AppStateProvider>
);
expect(
screen.getByText('You need some VEGA tokens to participate in governance')
screen.getByText(
'You need some VEGA tokens to participate in governance.'
)
).toBeTruthy();
});
@@ -2,7 +2,12 @@ import { format } from 'date-fns';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { AsyncRenderer, Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
import {
AsyncRenderer,
Button,
ButtonLink,
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { addDecimal, toBigNum } from '@vegaprotocol/utils';
import { ProposalState, VoteValue } from '@vegaprotocol/types';
import {
@@ -161,7 +166,12 @@ export const VoteButtons = ({
{changeVote || (voteState === VoteState.NotCast && proposalVotable) ? (
<>
{currentStakeAvailable.isLessThanOrEqualTo(0) && (
<p data-testid="no-stake-available">{t('noGovernanceTokens')}</p>
<>
<p data-testid="no-stake-available">{t('noGovernanceTokens')}.</p>
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
{t('findOutMoreAboutHowToVote')}
</ExternalLink>
</>
)}
<div className="flex gap-4" data-testid="vote-buttons">
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { formatDistanceToNow } from 'date-fns';
import { RoundedWrapper, Icon } from '@vegaprotocol/ui-toolkit';
import { RoundedWrapper, Icon, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ProposalState } from '@vegaprotocol/types';
import { useVoteSubmit, VoteProgress } from '@vegaprotocol/proposals';
@@ -202,7 +202,12 @@ export const VoteDetails = ({
)}
<section className="mt-10">
<SubHeading title={t('castYourVote')} />
{proposal?.state === ProposalState.STATE_OPEN ? (
<SubHeading title={t('castYourVote')} />
) : (
<SubHeading title={t('yourVote')} />
)}
{pubKey ? (
proposal && (
<VoteButtonsContainer
@@ -224,6 +229,9 @@ export const VoteDetails = ({
<Icon name={'info-sign'} />
<div>{t('connectAVegaWalletToVote')}</div>
</div>
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
{t('findOutMoreAboutHowToVote')}
</ExternalLink>
</div>
<ConnectToVega />
</RoundedWrapper>
@@ -16,7 +16,6 @@ const mockAppState: AppState = {
totalSupply: mockTotalSupply,
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
@@ -20,8 +20,11 @@ import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals';
const orderByDate = (arr: ProposalFieldsFragment[]) =>
orderBy(
arr,
[(p) => new Date(p?.terms?.closingDatetime).getTime(), (p) => p.id],
['desc', 'desc']
[
(p) => new Date(p?.terms?.closingDatetime).getTime(),
(p) => new Date(p?.datetime).getTime(),
],
['asc', 'asc']
);
const orderByUpgradeBlockHeight = (
@@ -54,22 +54,16 @@ export const TrancheItem = ({
{formatNumber(total, 2)}
</span>
</div>
<table className="w-full">
<tbody>
<tr>
<td>{t('Starts unlocking')}</td>
<td className="text-right">
{format(tranche.tranche_start, DATE_FORMAT_LONG)}
</td>
</tr>
<tr>
<td>{t('Fully unlocked')}</td>
<td className="text-right">
{format(tranche.tranche_end, DATE_FORMAT_LONG)}
</td>
</tr>
</tbody>
</table>
<div className="grid grid-cols-2 my-2">
<div>
<span>{t('Starts unlocking')}:</span>{' '}
<span>{format(tranche.tranche_start, DATE_FORMAT_LONG)}</span>
</div>
<div className="justify-self-end">
<span>{t('Fully unlocked')}:</span>{' '}
<span>{format(tranche.tranche_end, DATE_FORMAT_LONG)}</span>
</div>
</div>
<LockedProgress
locked={locked}
unlocked={unlocked}
@@ -79,7 +79,24 @@ export const generateEpochIndividualRewardsList = ({
epoch?.rewards.push(asset);
}
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
if (!asset.rewardTypes[rewardType]) {
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
} else {
const previousAmount = asset.rewardTypes[rewardType]?.amount;
const previousPercentageOfTotal =
asset.rewardTypes[rewardType]?.percentageOfTotal;
asset.rewardTypes[rewardType] = {
amount: previousAmount
? new BigNumber(previousAmount).plus(amount).toString()
: amount,
percentageOfTotal: previousPercentageOfTotal
? new BigNumber(previousPercentageOfTotal)
.plus(percentageOfTotal)
.toString()
: percentageOfTotal,
};
}
// totalAmount is the sum of all rewardTypes amounts
asset.totalAmount = Object.values(asset.rewardTypes).reduce(
@@ -34,6 +34,7 @@ import {
formatNumberPercentage,
toBigNum,
} from '@vegaprotocol/utils';
import { VALIDATOR_LOGO_MAP } from './logo-map';
interface CanonisedConsensusNodeProps {
id: string;
@@ -161,6 +162,11 @@ export const ConsensusValidatorsTable = ({
pendingUserStake,
userStakeShare,
}) => {
const logo = VALIDATOR_LOGO_MAP[id]
? VALIDATOR_LOGO_MAP[id]
: avatarUrl
? avatarUrl
: null;
const {
rawValidatorScore: previousEpochValidatorScore,
performanceScore: previousEpochPerformanceScore,
@@ -171,7 +177,7 @@ export const ConsensusValidatorsTable = ({
id,
[ValidatorFields.RANKING_INDEX]: votingPowerRanking,
[ValidatorFields.VALIDATOR]: {
avatarUrl,
avatarUrl: logo,
name,
},
[ValidatorFields.STAKE]: stakedTotal,
@@ -210,7 +216,7 @@ export const ConsensusValidatorsTable = ({
: undefined,
[ValidatorFields.PENDING_USER_STAKE]: pendingUserStake,
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
? formatNumberPercentage(new BigNumber(userStakeShare))
? formatNumberPercentage(new BigNumber(userStakeShare), 2)
: undefined,
};
}
@@ -0,0 +1,14 @@
export const VALIDATOR_LOGO_MAP: { [key: string]: string } = {
'55504e9bfd914a7bbefa342c82f59a2f4dee344e5b6863a14c02a812f4fbde32':
'https://pbs.twimg.com/profile_images/1586047492629712897/ZVMWBE94_400x400.jpg',
efbdf943443bd7595e83b0d7e88f37b7932d487d1b94aab3d004997273bb43fc:
'https://pbs.twimg.com/profile_images/1026823609979949057/3e-LCHHm_400x400.jpg',
'126751c5830b50d39eb85412fb2964f46338cce6946ff455b73f1b1be3f5e8cc':
'https://pbs.twimg.com/profile_images/1228627868542029824/9aoaLiIx_400x400.jpg',
'43697a3e911d8b70c0ce672adde17a5c38ca8f6a0486bf85ed0546e1b9a82887':
'https://pbs.twimg.com/profile_images/1352167987478843392/XzX82gIb_400x400.jpg',
ac735acc9ab11cf1d8c59c2df2107e00092b4ac96451cb137a1629af5b66242a:
'https://pbs.twimg.com/profile_images/1159254945885016064/vhhp1wL4_400x400.jpg',
'74023df02b8afc9eaf3e3e2e8b07eab1d2122ac3e74b1b0222daf4af565ad3dd':
'https://cdn4.telegram-cdn.org/file/n_OJ6upnglNG95E7u5FyfLmqqBUhf25LqtyZD0UM27gMM_WWi8rywrp9JaAA83quP7vx-WU9Q86quvIRdk9wuUrC2tdD08cYxah4dXHa4OgphFg7Vm5UYFjuGxuHn916OIlzIuVOjJWK_nricGdlPcZQK_T4kBJ_5v4pp94j76_DQ1eSy8DjUxnchZxqpGFv-lGN8U4SjRMK0RC_6DusNaRYEn9Ni2ZQHig7nMiqud-nkK5RgUrClfXFwyhoMFVfhHpGUzwpSv1pSEVLXVh7E1mVmWYrW9hzdQGGrouWCGRqtf_hRa9enS2fv0P9mAB23BcURWuLOVtMdJTF4wfc-w.jpg',
};
@@ -16,6 +16,7 @@ import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import type { StakingDelegationFieldsFragment } from '../../__generated__/Staking';
import type { ValidatorsView } from './validator-tables';
import { VALIDATOR_LOGO_MAP } from './logo-map';
export enum ValidatorFields {
RANKING_INDEX = 'rankingIndex',
@@ -102,13 +103,18 @@ interface ValidatorRendererProps {
export const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
const { t } = useTranslation();
const { avatarUrl, name } = data.validator;
const logo = VALIDATOR_LOGO_MAP[data.id]
? VALIDATOR_LOGO_MAP[data.id]
: avatarUrl
? avatarUrl
: null;
return (
<div className="w-[238px] grid grid-cols-[1fr_auto] gap-2 items-center">
<span className="flex overflow-hidden">
{avatarUrl && (
{logo && (
<img
className="h-6 w-6 rounded-full mr-2"
src={avatarUrl}
src={logo}
alt={`Avatar icon for ${name}`}
onError={(e) => (e.currentTarget.style.display = 'none')}
/>
@@ -78,37 +78,38 @@ export const ValidatorTables = ({
const nextDelegation = delegations?.find(
(d) => d.node.id === validatorId && d.epoch === Number(epochId) + 1
);
switch (validator.rankingScore?.status) {
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT:
acc.consensusValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ:
acc.standbyValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_PENDING:
acc.pendingValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
if (validator.stakedByOperator !== '0') {
switch (validator.rankingScore?.status) {
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT:
acc.consensusValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ:
acc.standbyValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_PENDING:
acc.pendingValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
}
}
return acc;
},
@@ -28,6 +28,7 @@ export const NodeContainer = ({
const { t } = useTranslation();
const { pubKey } = useVegaWallet();
const { delegationsPagination } = ENV;
const { data, loading, error, refetch } = useStakingQuery({
variables: {
partyId: pubKey || '',
@@ -37,7 +38,9 @@ export const NodeContainer = ({
}
: undefined,
},
errorPolicy: 'all',
});
const { data: previousEpochData } = usePreviousEpochQuery({
variables: {
epochId: (Number(data?.epoch.id) - 1).toString(),
@@ -47,7 +50,7 @@ export const NodeContainer = ({
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
if (error) {
if (error && !error.message.includes('failed to get party for ID')) {
return (
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
<pre>
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -20,7 +20,7 @@ module.exports = defineConfig({
chromeWebSecurity: false,
projectId: 'et4snf',
defaultCommandTimeout: 10000,
viewportWidth: 1440,
viewportWidth: 1800,
viewportHeight: 900,
responseTimeout: 50000,
requestTimeout: 20000,
@@ -12,6 +12,7 @@ import {
createMarketsDataFragment,
assetQuery,
networkParamsQuery,
nodeGuardQuery,
} from '@vegaprotocol/mock';
import {
addDecimalsFormatNumber,
@@ -158,6 +159,7 @@ describe('Closed markets', { tags: '@smoke' }, () => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
aliasGQLQuery(
req,
@@ -6,6 +6,10 @@ const row = 'key-value-table-row';
const marketTitle = 'accordion-title';
const externalLink = 'external-link';
const accordionContent = 'accordion-content';
const providerName = 'provider-name';
const oracleBannerStatus = 'oracle-banner-status';
const oracleBannerDialogTrigger = 'oracle-banner-dialog-trigger';
const oracleFullProfile = 'oracle-full-profile';
describe('market info is displayed', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -28,7 +32,11 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
it('show oracle banner', () => {
cy.getByTestId(marketTitle).contains('Oracle').click();
cy.getByTestId('oracle-status').should('contain.text', 'COMPROMISED');
cy.getByTestId(oracleBannerStatus).should('contain.text', 'COMPROMISED');
cy.getByTestId(oracleBannerDialogTrigger)
.should('contain.text', 'Show more')
.click();
cy.getByTestId(oracleFullProfile).should('exist');
});
it('current fees displayed', () => {
@@ -181,9 +189,19 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
cy.getByTestId(marketTitle).contains('Oracle').click();
cy.getByTestId(accordionContent)
.getByTestId('provider-name')
.getByTestId(providerName)
.and('contain', 'Another oracle');
cy.getByTestId(providerName).should('be.visible').click();
cy.getByTestId('dialog-content')
.eq(1)
.within(() => {
cy.getByTestId('block-explorer-link').contains('Block explorer');
cy.getByTestId('github-link').contains('Oracle repository');
});
cy.getByTestId('dialog-close').click();
cy.getByTestId(accordionContent)
.getByTestId('verified-proofs')
.and('contain', '1');
@@ -0,0 +1,105 @@
import {
AuctionTrigger,
MarketState,
MarketTradingMode,
} from '@vegaprotocol/types';
describe('markets selector', { tags: '@smoke' }, () => {
const list = 'market-selector-list';
const searchInput = 'search-term';
beforeEach(() => {
cy.window().then((window) => {
window.localStorage.setItem('marketId', 'market-1');
});
cy.mockTradingPage(
MarketState.STATE_ACTIVE,
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockSubscription();
cy.visit('/');
cy.wait('@Markets');
cy.wait('@MarketsData');
cy.wait('@MarketsCandles');
});
it('can toggle the sidebar', () => {
cy.getByTestId('market-selector').should('be.visible');
cy.getByTestId('sidebar-toggle').click();
cy.getByTestId('market-selector').should('not.exist');
cy.getByTestId('sidebar-toggle').click();
cy.getByTestId('market-selector').should('be.visible');
});
// need function keyword as we need 'this' to access market data
it('displays data as expected', () => {
// TODO: load data from mocks in. Using alias and wrap intermittently fails
const data = [
{
code: 'AAPL.MF21',
name: 'Apple Monthly (30 Jun 2022)',
markPrice: '46,126.90058',
change: '+200.00%',
},
{
code: 'BTCUSD.MF21',
name: 'ACTIVE MARKET',
markPrice: '46,126.90058',
change: '+200.00%',
},
{
code: 'ETHBTC.QM21',
name: 'ETHBTC Quarterly (30 Jun 2022)',
markPrice: '46,126.90058',
change: '+200.00%',
},
{
code: 'SOLUSD',
name: 'SUSPENDED MARKET',
markPrice: '84.41',
change: '+200.00%',
},
];
cy.getByTestId(list)
.find('a')
.each((item, i) => {
const market = data[i];
// 6001-MARK-021
expect(item.find('h3').text()).equals(market.code);
// 6001-MARK-022
expect(item.find('h4').text()).equals(market.name);
// 6001-MARK-024
expect(item.find('[data-testid="market-item-price"]').text()).equals(
market.markPrice
);
// 6001-MARK-023
expect(item.find('[data-testid="market-item-change"]').text()).equals(
market.change
);
// 6001-MARK-025
expect(item.find('[data-testid="sparkline-svg"]')).to.exist;
});
});
// 6001-MARK-27
it('can use the filter options', () => {
// product type
cy.getByTestId('product-Spot').click();
cy.getByTestId(list).contains('Spot markets coming soon.');
cy.getByTestId('product-Perpetual').click();
cy.getByTestId(list).contains('Perpetual markets coming soon.');
cy.getByTestId('product-Future').click();
cy.getByTestId(list).find('a').should('have.length', 4);
// 6001-MARK-29
cy.getByTestId(searchInput).clear().type('btc');
cy.getByTestId(list).find('a').should('have.length', 2);
cy.getByTestId(list).find('a').eq(0).contains('BTCUSD.MF21');
cy.getByTestId(list).find('a').eq(1).contains('ETHBTC.QM21');
cy.getByTestId(searchInput).clear();
cy.getByTestId(list).find('a').should('have.length', 4);
});
});
@@ -31,7 +31,7 @@ describe('Market trading page', () => {
// 7002-SORD-001
// 7002-SORD-002
it('must display market name', () => {
cy.getByTestId('popover-trigger').should('not.be.empty');
cy.getByTestId('header-title').should('not.be.empty');
});
it('must see market expiry', () => {
@@ -4,7 +4,6 @@ import { marketsQuery } from '@vegaprotocol/mock';
import { getDateTimeFormat } from '@vegaprotocol/utils';
const dialogCloseBtn = 'dialog-close';
const popoverTrigger = 'popover-trigger';
describe('markets table', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -31,36 +30,6 @@ describe('markets table', { tags: '@smoke' }, () => {
cy.getByTestId('sparkline-svg').should('be.visible');
});
it('renders market list drop down', () => {
openMarketDropDown();
cy.getByTestId('price').invoke('text').should('not.be.empty');
cy.getByTestId('trading-mode-col').should('not.be.empty');
cy.getByTestId('taker-fee').should('contain.text', '%');
cy.getByTestId('market-volume').should('not.be.empty');
cy.getByTestId('market-name').should('not.be.empty');
cy.getByTestId('trading-mode-col')
.contains('Monitoring auction - liquidity')
.eq(0)
.realHover();
cy.get('[data-testid="trading-mode-tooltip"] p').should('have.class', '');
cy.get(
'[data-testid="market-trading-mode"] [data-testid="item-value"]'
).realHover();
cy.get('[data-testid="trading-mode-tooltip"] p').should(
'have.class',
'mb-4'
);
});
it('able to select market from dropdown', () => {
openMarketDropDown();
cy.getByTestId('market-link-market-0').first().should('be.visible').click();
cy.contains('ACTIVE MARKET').should('be.visible');
cy.url().should('include', '/markets/market-0');
cy.getByTestId('popover-trigger').should('not.be.empty');
});
it('able to open and sort full market list - market page', () => {
const ExpectedSortedMarkets = [
'AAPL.MF21',
@@ -253,15 +222,3 @@ describe('markets table', { tags: '@smoke' }, () => {
});
});
});
function openMarketDropDown() {
cy.getByTestId(dialogCloseBtn).then((button) => {
if (button.is(':visible')) {
cy.get('[data-testid^="market-link-"]').should('not.be.empty');
cy.getByTestId(dialogCloseBtn).click();
}
cy.get('[data-testid^="ask-vol-"]').should('be.visible');
cy.getByTestId(popoverTrigger).click({ force: true });
cy.contains('Loading market data...').should('not.exist');
});
}
@@ -16,6 +16,10 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.wait('@Markets');
});
beforeEach(() => {
cy.mockTradingPage();
});
describe('limit order', () => {
before(() => {
cy.getByTestId(toggleLimit).click();
@@ -98,7 +102,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
'have.text',
'Total margin available'
);
cy.get('.text-neutral-500').should('have.text', '~100,000.01 tDAI');
cy.get('.text-neutral-500').should('have.text', '100,000.01 tDAI');
});
});
});
@@ -1,10 +1,6 @@
import * as Schema from '@vegaprotocol/types';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import {
accountsQuery,
amendGeneralAccountBalance,
estimateOrderQuery,
} from '@vegaprotocol/mock';
import { accountsQuery, amendGeneralAccountBalance } from '@vegaprotocol/mock';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
@@ -44,13 +40,10 @@ describe(
cy.setVegaWallet();
cy.mockTradingPage();
const accounts = accountsQuery();
amendGeneralAccountBalance(accounts, 'market-0', '100000000');
amendGeneralAccountBalance(accounts, 'market-0', '1');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
cy.mockGQL((req) => {
aliasGQLQuery(req, 'EstimateOrder', estimateOrderQuery());
});
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
@@ -66,7 +59,7 @@ describe(
);
cy.getByTestId('dealticket-warning-margin').should(
'contain.text',
'You may not have enough margin available to open this position. 2,354.72283 tDAI is currently required. You have only 1,000.01 tDAI available.'
'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.'
);
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
cy.getByTestId('dialog-content')
@@ -291,12 +291,58 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
.should('have.text', '200.00');
});
it('must see a pegged order - ask', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_BUY,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_ASK,
offset: '250000',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Ask - 2.50 Peg limit');
});
it('must see a pegged order - bid', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_SELL,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_BID,
offset: '100',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Bid + 0.001 Peg limit');
});
it('must see a pegged order - mid', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_SELL,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_MID,
offset: '0.5',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Mid + 0.00001 Peg limit');
});
it('for market typy must not see a price for active or parked orders', () => {
// 7003-MORD-005
updateOrder({
id: orderId,
type: Schema.OrderType.TYPE_MARKET,
status: Schema.OrderStatus.STATUS_PARKED,
peggedOrder: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="price"]')
@@ -337,6 +383,7 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
liquidityProvisionId: '6536',
});
cy.get(`[row-id=${orderId}]`)
@@ -17,7 +17,6 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
it('can connect', () => {
// 0004-EWAL-001
cy.wait('@NetworkParams');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
cy.getByTestId('connect-eth-wallet-btn').click();
@@ -30,7 +29,6 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
it('should see QR code modal for WalletConnect', () => {
// 0004-EWAL-003
cy.wait('@NetworkParams');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
cy.getByTestId('connect-eth-wallet-btn').click();
+15 -3
View File
@@ -1,7 +1,7 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import * as Schema from '@vegaprotocol/types';
import type { CyHttpMessages } from 'cypress/types/net-stubbing';
import type { Provider, Status } from '@vegaprotocol/oracles';
import type { Provider, Status } from '@vegaprotocol/market-info';
import {
accountsQuery,
assetQuery,
@@ -10,7 +10,7 @@ import {
chainIdQuery,
chartQuery,
depositsQuery,
estimateOrderQuery,
estimateFeesQuery,
marginsQuery,
marketCandlesQuery,
marketDataQuery,
@@ -20,12 +20,16 @@ import {
marketsDataQuery,
marketsQuery,
networkParamsQuery,
nodeGuardQuery,
ordersQuery,
estimatePositionQuery,
positionsQuery,
proposalListQuery,
statisticsQuery,
tradesQuery,
withdrawalsQuery,
protocolUpgradeProposalsQuery,
blockStatisticsQuery,
} from '@vegaprotocol/mock';
import type { PartialDeep } from 'type-fest';
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/market-list';
@@ -82,6 +86,7 @@ const mockTradingPage = (
) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
aliasGQLQuery(
req,
'Markets',
@@ -155,9 +160,16 @@ const mockTradingPage = (
aliasGQLQuery(req, 'Candles', candlesQuery());
aliasGQLQuery(req, 'Withdrawals', withdrawalsQuery());
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
aliasGQLQuery(req, 'EstimateOrder', estimateOrderQuery());
aliasGQLQuery(req, 'EstimateFees', estimateFeesQuery());
aliasGQLQuery(req, 'EstimatePosition', estimatePositionQuery());
aliasGQLQuery(req, 'ProposalsList', proposalListQuery());
aliasGQLQuery(req, 'Deposits', depositsQuery());
aliasGQLQuery(
req,
'ProtocolUpgradeProposals',
protocolUpgradeProposalsQuery()
);
aliasGQLQuery(req, 'BlockStatistics', blockStatisticsQuery());
};
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
+6
View File
@@ -0,0 +1,6 @@
function ReactMarkdown({ children }) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
export default ReactMarkdown;
@@ -0,0 +1,83 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AssetDropdown } from './asset-dropdown';
const createAssets = (count = 3) => {
return new Array(count).fill(null).map((_, i) => ({
id: i.toString(),
symbol: 'asset-1',
}));
};
describe('AssetDropdown', () => {
const assets = createAssets();
it('renders and selects chosen assets', async () => {
const mockOnSelect = jest.fn();
render(
<AssetDropdown
checkedAssets={[]}
assets={assets}
onSelect={mockOnSelect}
onReset={jest.fn()}
/>
);
await userEvent.click(screen.getByRole('button'));
const items = screen.getAllByRole('menuitemcheckbox');
expect(items).toHaveLength(assets.length);
expect(items.map((i) => i.textContent)).toEqual(
assets.map((a) => a.symbol)
);
await userEvent.click(items[0]);
expect(mockOnSelect).toHaveBeenCalledWith(assets[0].id, true);
});
it('unselects already selected assets', async () => {
const mockOnSelect = jest.fn();
render(
<AssetDropdown
checkedAssets={[assets[0].id]}
assets={assets}
onSelect={mockOnSelect}
onReset={jest.fn()}
/>
);
await userEvent.click(screen.getByRole('button'));
const items = screen.getAllByRole('menuitemcheckbox');
await userEvent.click(items[0]);
expect(mockOnSelect).toHaveBeenCalledWith(assets[0].id, false);
});
it('can be reset clearing all assets', async () => {
const mockOnSelect = jest.fn();
const mockOnReset = jest.fn();
render(
<AssetDropdown
checkedAssets={assets.map((a) => a.id)}
assets={assets}
onSelect={mockOnSelect}
onReset={mockOnReset}
/>
);
await userEvent.click(screen.getByRole('button'));
const items = screen.getAllByRole('menuitemcheckbox');
// all should be checked
items.forEach((item) => {
expect(item).toBeChecked();
});
await userEvent.click(screen.getByText('Reset'));
expect(mockOnReset).toHaveBeenCalled();
});
it('doesnt render if no assets provided', async () => {
const { container } = render(
<AssetDropdown
checkedAssets={[]}
assets={[]}
onSelect={jest.fn()}
onReset={jest.fn()}
/>
);
expect(container).toBeEmptyDOMElement();
});
});
@@ -0,0 +1,58 @@
import { t } from '@vegaprotocol/i18n';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuItemIndicator,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from '@vegaprotocol/ui-toolkit';
export const AssetDropdown = ({
assets,
checkedAssets,
onSelect,
onReset,
}: {
assets: Array<{ id: string; symbol: string }> | undefined;
checkedAssets: string[];
onSelect: (id: string, checked: boolean) => void;
onReset: () => void;
}) => {
if (!assets?.length) {
return null;
}
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger data-testid="asset-trigger">
<span className="px-1">$</span>
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
<DropdownMenuItem onClick={onReset}>{t('Reset')}</DropdownMenuItem>
<DropdownMenuSeparator />
{assets?.map((a) => {
return (
<DropdownMenuCheckboxItem
key={a.id}
checked={checkedAssets.includes(a.id)}
onCheckedChange={(checked) => {
if (typeof checked === 'boolean') {
onSelect(a.id, checked);
}
}}
data-testid={`asset-id-${a.id}`}
>
{a.symbol}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -5,13 +5,7 @@ import { MarketProposalNotification } from '@vegaprotocol/proposals';
import type { Market } from '@vegaprotocol/market-list';
import { getExpiryDate, getMarketExpiryDate } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
ColumnKind,
SelectMarketPopover,
} from '../../components/select-market';
import type { OnCellClickHandler } from '../../components/select-market';
import { Header, HeaderStat } from '../../components/header';
import { NO_MARKET } from './constants';
import { HeaderStat } from '../../components/header';
import { MarketMarkPrice } from '../../components/market-mark-price';
import { Last24hPriceChange, Last24hVolume } from '@vegaprotocol/market-info';
import { MarketState } from '../../components/market-state';
@@ -19,102 +13,90 @@ import { HeaderStatMarketTradingMode } from '../../components/market-trading-mod
import { MarketLiquiditySupplied } from '../../components/liquidity-supplied';
import { MarketState as State } from '@vegaprotocol/types';
interface TradeMarketHeaderProps {
interface HeaderStatsProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
}
export const TradeMarketHeader = ({
market,
onSelect,
}: TradeMarketHeaderProps) => {
export const HeaderStats = ({ market }: HeaderStatsProps) => {
const { VEGA_EXPLORER_URL } = useEnvironment();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const asset = market?.tradableInstrument.instrument.product?.settlementAsset;
const onCellClick: OnCellClickHandler = (e, kind, value) => {
if (value && kind === ColumnKind.Asset) {
openAssetDetailsDialog(value, e.target as HTMLElement);
}
};
return (
<Header
title={
<SelectMarketPopover
marketCode={market?.tradableInstrument.instrument.code || NO_MARKET}
marketName={market?.tradableInstrument.instrument.name || NO_MARKET}
onSelect={onSelect}
onCellClick={onCellClick}
/>
}
>
<HeaderStat
heading={t('Expiry')}
description={
market && (
<ExpiryTooltipContent
market={market}
explorerUrl={VEGA_EXPLORER_URL}
/>
)
}
testId="market-expiry"
>
<ExpiryLabel market={market} />
</HeaderStat>
<HeaderStat heading={t('Price')} testId="market-price">
<MarketMarkPrice
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Change (24h)')} testId="market-change">
<Last24hPriceChange
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat
heading={t('Volume (24h)')}
testId="market-volume"
description={t(
'The total number of contracts traded in the last 24 hours.'
)}
>
<Last24hVolume
marketId={market?.id}
positionDecimalPlaces={market?.positionDecimalPlaces}
/>
</HeaderStat>
<HeaderStatMarketTradingMode
marketId={market?.id}
initialTradingMode={market?.tradingMode}
/>
<MarketState market={market} />
{asset ? (
<HeaderStat
heading={t('Settlement asset')}
testId="market-settlement-asset"
<div className="flex flex-col justify-end lg:pt-4">
<div className="xl:flex xl:gap-4 items-end">
<div
data-testid="header-summary"
className="flex flex-nowrap items-end xl:flex-1 w-full overflow-x-auto text-xs"
>
<div>
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
}}
<HeaderStat
heading={t('Expiry')}
description={
market && (
<ExpiryTooltipContent
market={market}
explorerUrl={VEGA_EXPLORER_URL}
/>
)
}
testId="market-expiry"
>
<ExpiryLabel market={market} />
</HeaderStat>
<HeaderStat heading={t('Price')} testId="market-price">
<MarketMarkPrice
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Change (24h)')} testId="market-change">
<Last24hPriceChange
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat
heading={t('Volume (24h)')}
testId="market-volume"
description={t(
'The total number of contracts traded in the last 24 hours.'
)}
>
<Last24hVolume
marketId={market?.id}
positionDecimalPlaces={market?.positionDecimalPlaces}
/>
</HeaderStat>
<HeaderStatMarketTradingMode
marketId={market?.id}
initialTradingMode={market?.tradingMode}
/>
<MarketState market={market} />
{asset ? (
<HeaderStat
heading={t('Settlement asset')}
testId="market-settlement-asset"
>
{asset.symbol}
</ButtonLink>
</div>
</HeaderStat>
) : null}
<MarketLiquiditySupplied
marketId={market?.id}
assetDecimals={asset?.decimals || 0}
/>
<MarketProposalNotification marketId={market?.id} />
</Header>
<div>
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
}}
>
{asset.symbol}
</ButtonLink>
</div>
</HeaderStat>
) : null}
<MarketLiquiditySupplied
marketId={market?.id}
assetDecimals={asset?.decimals || 0}
/>
<MarketProposalNotification marketId={market?.id} />
</div>
</div>
</div>
);
};
@@ -0,0 +1,111 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketSelectorItem } from './market-selector-item';
import { MemoryRouter } from 'react-router-dom';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type {
MarketDataUpdateFieldsFragment,
MarketDataUpdateSubscription,
} from '@vegaprotocol/market-list';
import { MarketDataUpdateDocument } from '@vegaprotocol/market-list';
import {
AuctionTrigger,
MarketState,
MarketTradingMode,
} from '@vegaprotocol/types';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
describe('MarketSelectorItem', () => {
const market = createMarketFragment({
id: 'market-0',
decimalPlaces: 2,
// @ts-ignore fragment doesn't contain candles
candles: [{ close: '5' }, { close: '10' }],
});
const marketData: MarketDataUpdateFieldsFragment = {
__typename: 'ObservableMarketData',
marketId: market.id,
auctionEnd: null,
auctionStart: null,
bestBidPrice: '100',
bestBidVolume: '100',
bestOfferPrice: '100',
bestOfferVolume: '100',
bestStaticBidPrice: '100',
bestStaticBidVolume: '100',
bestStaticOfferPrice: '100',
bestStaticOfferVolume: '100',
indicativePrice: '100',
indicativeVolume: '100',
marketState: MarketState.STATE_ACTIVE,
marketTradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
marketValueProxy: '100',
markPrice: '50000',
midPrice: '100',
staticMidPrice: '100',
openInterest: '100',
suppliedStake: '1000000',
targetStake: '1000000',
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
priceMonitoringBounds: null,
};
const mock: MockedResponse<MarketDataUpdateSubscription> = {
request: {
query: MarketDataUpdateDocument,
variables: {
marketId: market.id,
},
},
result: {
data: {
marketsData: [marketData],
},
},
};
const mockOnSelect = jest.fn();
const renderJsx = () => {
return render(
<MemoryRouter>
<MockedProvider mocks={[mock]}>
<MarketSelectorItem
market={market}
currentMarketId={market.id}
style={{}}
onSelect={mockOnSelect}
/>
</MockedProvider>
</MemoryRouter>
);
};
it('renders market information', async () => {
renderJsx();
const link = screen.getByRole('link');
// link renders and is styled
expect(link).toHaveAttribute('href', '/markets/' + market.id);
expect(link).toHaveClass('ring-1');
expect(screen.getByTestId('market-item-price')).toHaveTextContent('-');
// candles are loaded immediately
expect(screen.getByTestId('market-item-change')).toHaveTextContent(
'+100.00%'
);
await waitFor(() => {
expect(screen.getByTestId('market-item-price')).toHaveTextContent(
addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
);
});
await userEvent.click(link);
expect(mockOnSelect).toHaveBeenCalledWith(market.id);
});
});
@@ -0,0 +1,115 @@
import type { CSSProperties } from 'react';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import {
addDecimalsFormatNumber,
formatNumber,
priceChangePercentage,
} from '@vegaprotocol/utils';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/market-list';
import { useMarketDataUpdateSubscription } from '@vegaprotocol/market-list';
import { Sparkline } from '@vegaprotocol/ui-toolkit';
export const MarketSelectorItem = ({
market,
style,
currentMarketId,
onSelect,
}: {
market: MarketMaybeWithDataAndCandles;
style: CSSProperties;
currentMarketId?: string;
onSelect?: (marketId: string) => void;
}) => {
const wrapperClasses = classNames(
'block bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg p-4',
'min-h-[120px]',
{
'ring-1 ring-vega-light-300 dark:ring-vega-dark-300':
currentMarketId === market.id,
}
);
return (
<div style={style} className="my-0.5 px-4">
<Link
to={`/markets/${market.id}`}
className={wrapperClasses}
onClick={() => {
onSelect && onSelect(market.id);
}}
>
<h3>{market.tradableInstrument.instrument.code}</h3>
<h4
title={market.tradableInstrument.instrument.name}
className="text-sm text-vega-light-300 dark:text-vega-dark-300 text-ellipsis whitespace-nowrap overflow-hidden"
>
{market.tradableInstrument.instrument.name}
</h4>
<MarketData market={market} />
</Link>
</div>
);
};
const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
const { data } = useMarketDataUpdateSubscription({
variables: {
marketId: market.id,
},
});
const marketData = data?.marketsData[0];
// use market data price if available as this is comes from
// the subscription
const price = marketData
? addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
: market.data
? addDecimalsFormatNumber(market.data.markPrice, market.decimalPlaces)
: '-';
return (
<div className="flex flex-nowrap justify-between items-center mt-1">
<div className="w-1/2">
<div
className="text-ellipsis whitespace-nowrap overflow-hidden"
data-testid="market-item-price"
>
{price}
</div>
{market.candles && (
<PriceChange candles={market.candles.map((c) => c.close)} />
)}
</div>
<div className="w-1/2 max-w-[120px]">
{market.candles ? (
<Sparkline
width={120}
height={20}
data={market.candles.filter(Boolean).map((c) => Number(c.close))}
/>
) : (
'-'
)}
</div>
</div>
);
};
const PriceChange = ({ candles }: { candles: string[] }) => {
const priceChange = candles ? priceChangePercentage(candles) : undefined;
const priceChangeClasses = classNames('text-xs', {
'text-vega-pink': priceChange && priceChange < 0,
'text-vega-green': priceChange && priceChange > 0,
});
let prefix = '';
if (priceChange && priceChange > 0) {
prefix = '+';
}
const formattedChange = formatNumber(Number(priceChange), 2);
return (
<div className={priceChangeClasses} data-testid="market-item-change">
{priceChange ? `${prefix}${formattedChange}%` : '-'}
</div>
);
};
@@ -0,0 +1,289 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MarketSelector } from './market-selector';
import { useMarketList } from '@vegaprotocol/market-list';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketState } from '@vegaprotocol/types';
import { MemoryRouter } from 'react-router-dom';
import type { ReactNode } from 'react';
import type { SortType } from './sort-dropdown';
import { SortTypeMapping } from './sort-dropdown';
import { Sort } from './sort-dropdown';
import { subDays } from 'date-fns';
jest.mock('@vegaprotocol/market-list');
const mockUseMarketList = useMarketList as jest.Mock;
// mock market list items to avoid subscriptions starting
jest.mock('./market-selector-item', () => ({
MarketSelectorItem: (props: { market: { id: string } }) => (
<div data-testid={props.market.id} />
),
}));
// without a real DOM autosize won't render with an actual height or width
jest.mock('react-virtualized-auto-sizer', () => {
// eslint-disable-next-line react/display-name
return ({
children,
}: {
children: (size: { width: number; height: number }) => ReactNode;
}) => <div>{children({ width: 300, height: 1000 })}</div>;
});
describe('MarketSelector', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
code: 'a',
name: 'a',
product: {
settlementAsset: {
id: 'asset-0',
},
},
},
},
// @ts-ignore candles get joined outside this type
candles: [{ close: '100' }, { close: '200' }],
marketTimestamps: {
open: subDays(new Date(), 1).toISOString(),
},
}),
createMarketFragment({
id: 'market-1',
state: MarketState.STATE_SUSPENDED,
tradableInstrument: {
instrument: {
code: 'b',
name: 'b',
product: {
settlementAsset: {
id: 'asset-0',
},
},
},
},
// @ts-ignore candles get joined outside this type
candles: [{ close: '100' }, { close: '400' }],
marketTimestamps: {
open: subDays(new Date(), 2).toISOString(),
},
}),
createMarketFragment({
id: 'market-2',
state: MarketState.STATE_CLOSED,
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-1',
},
},
},
},
}),
createMarketFragment({
id: 'market-3',
state: MarketState.STATE_ACTIVE,
tradableInstrument: {
instrument: {
code: 'c',
name: 'c',
product: {
settlementAsset: {
id: 'asset-1',
},
},
},
},
// @ts-ignore candles get joined outside this type
candles: [{ close: '100' }, { close: '1000' }],
marketTimestamps: {
open: subDays(new Date(), 0).toISOString(),
},
}),
createMarketFragment({
id: 'market-4',
tradableInstrument: {
instrument: {
code: 'cd',
name: 'cd',
product: {
settlementAsset: {
id: 'asset-2',
},
},
},
},
// @ts-ignore candles get joined outside this type
candles: [{ close: '100' }, { close: '300' }],
marketTimestamps: {
open: subDays(new Date(), 3).toISOString(),
},
}),
];
const activeMarkets = markets.filter((m) =>
[MarketState.STATE_ACTIVE, MarketState.STATE_SUSPENDED].includes(m.state)
);
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
it('renders only active markets', () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(
activeMarkets.length
);
expect(screen.getByRole('link')).toHaveTextContent('All markets');
});
it('filters by product type', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('product-Spot'));
expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(0);
expect(screen.getByTestId('no-items')).toHaveTextContent(
'Spot markets coming soon.'
);
await userEvent.click(screen.getByTestId('product-Perpetual'));
expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(0);
expect(screen.getByTestId('no-items')).toHaveTextContent(
'Perpetual markets coming soon.'
);
await userEvent.click(screen.getByTestId('product-Future'));
expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(
activeMarkets.length
);
expect(screen.queryByTestId('no-items')).not.toBeInTheDocument();
});
it('filters by search term', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.type(screen.getByTestId('search-term'), 'zzz');
expect(screen.getByTestId('no-items')).toHaveTextContent('No markets');
const input = screen.getByTestId('search-term');
await userEvent.clear(input);
await userEvent.type(input, 'a');
expect(input).toHaveValue('a');
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(1);
expect(screen.getByTestId('market-0')).toBeInTheDocument();
await userEvent.clear(input);
await userEvent.type(input, 'b');
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(1);
expect(screen.getByTestId('market-1')).toBeInTheDocument();
await userEvent.clear(input);
await userEvent.type(input, 'c');
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(2);
expect(screen.getByTestId('market-3')).toBeInTheDocument();
expect(screen.getByTestId('market-4')).toBeInTheDocument();
});
it('filters by asset', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('asset-trigger'));
expect(screen.getAllByTestId(/asset-id/)).toHaveLength(3);
await userEvent.click(screen.getByTestId('asset-id-asset-0'));
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(2);
expect(screen.getByTestId('market-0')).toBeInTheDocument();
expect(screen.getByTestId('market-1')).toBeInTheDocument();
// reopen asset dropdown and add asset-1
await userEvent.click(screen.getByTestId('asset-trigger'));
await userEvent.click(screen.getByTestId('asset-id-asset-1'));
// all markets with asset-0 or asset-1 shown (no market id as market is closed)
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(3);
expect(screen.getByTestId('market-0')).toBeInTheDocument();
expect(screen.getByTestId('market-1')).toBeInTheDocument();
expect(screen.getByTestId('market-3')).toBeInTheDocument();
// reopen and uncheck asset-0
await userEvent.click(screen.getByTestId('asset-trigger'));
await userEvent.click(screen.getByTestId('asset-id-asset-0'));
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(1);
expect(screen.getByTestId('market-3')).toBeInTheDocument();
});
it('sorts by gained', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('sort-trigger'));
const options = screen.getAllByTestId(/sort-item/);
expect(options.map((o) => o.textContent)).toEqual(
Object.entries(Sort)
.filter(([key]) => key !== Sort.None)
.map(([key]) => SortTypeMapping[key as SortType])
);
await userEvent.click(screen.getByTestId('sort-item-Gained'));
expect(
screen
.getAllByTestId(/market-\d/)
.map((el) => el.getAttribute('data-testid'))
).toEqual([markets[3].id, markets[1].id, markets[4].id, markets[0].id]);
});
it('sorts by lost', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('sort-trigger'));
await userEvent.click(screen.getByTestId('sort-item-Lost'));
expect(
screen
.getAllByTestId(/market-\d/)
.map((el) => el.getAttribute('data-testid'))
).toEqual([markets[0].id, markets[4].id, markets[1].id, markets[3].id]);
});
it('sorts by new', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('sort-trigger'));
await userEvent.click(screen.getByTestId('sort-item-New'));
expect(
screen
.getAllByTestId(/market-\d/)
.map((el) => el.getAttribute('data-testid'))
).toEqual([markets[3].id, markets[0].id, markets[1].id, markets[4].id]);
});
});
@@ -0,0 +1,276 @@
import { t } from '@vegaprotocol/i18n';
import uniqBy from 'lodash/uniqBy';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/market-list';
import {
Input,
TinyScroll,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { CSSProperties } from 'react';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useMarketSelectorList } from './use-market-selector-list';
import type { ProductType } from './product-selector';
import { Product, ProductSelector } from './product-selector';
import { AssetDropdown } from './asset-dropdown';
import type { SortType } from './sort-dropdown';
import { Sort, SortDropdown } from './sort-dropdown';
import { MarketSelectorItem } from './market-selector-item';
export type Filter = {
searchTerm: string;
product: ProductType;
sort: SortType;
assets: string[];
};
/**
* Fetches market data and filters it given a set of filter properties
* defined in Filter
*/
export const MarketSelector = ({
currentMarketId,
onSelect,
}: {
currentMarketId?: string;
onSelect?: (marketId: string) => void;
}) => {
const [filter, setFilter] = useState<Filter>({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: [],
});
const { markets, data, loading, error } = useMarketSelectorList(filter);
return (
<div
className="grid grid-rows-[min-content_1fr_min-content] h-full"
data-testid="market-selector"
>
<div className="px-4 pt-2 pb-4">
<ProductSelector
product={filter.product}
onSelect={(product) => {
setFilter((curr) => ({ ...curr, product }));
}}
/>
<div className="text-sm flex gap-1 items-stretch">
<div className="flex-1">
<Input
onChange={(e) =>
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
}
value={filter.searchTerm}
type="text"
placeholder={t('Search')}
data-testid="search-term"
className="w-full"
appendElement={
filter.searchTerm.length ? (
<button
onClick={() =>
setFilter((curr) => ({ ...curr, searchTerm: '' }))
}
className="text-vega-light-200 dark:text-vega-dark-200"
>
<VegaIcon name={VegaIconNames.CROSS} />
</button>
) : (
<span />
)
}
/>
</div>
<AssetDropdown
assets={uniqBy(
data?.map(
(d) => d.tradableInstrument.instrument.product.settlementAsset
),
'id'
)}
checkedAssets={filter.assets}
onSelect={(id: string, checked) => {
setFilter((curr) => {
if (checked) {
if (curr.assets.includes(id)) {
return curr;
} else {
return { ...curr, assets: [...curr.assets, id] };
}
} else {
if (curr.assets.includes(id)) {
return {
...curr,
assets: curr.assets.filter((x) => x !== id),
};
}
}
return curr;
});
}}
onReset={() => setFilter((curr) => ({ ...curr, assets: [] }))}
/>
<SortDropdown
currentSort={filter.sort}
onSelect={(sort) => {
setFilter((curr) => {
if (curr.sort === sort) {
return { ...curr, sort: Sort.None };
}
return {
...curr,
sort,
};
});
}}
onReset={() => setFilter((curr) => ({ ...curr, sort: Sort.None }))}
/>
</div>
</div>
<div data-testid="market-selector-list">
<MarketList
data={markets}
loading={loading}
error={error}
searchTerm={filter.searchTerm}
currentMarketId={currentMarketId}
onSelect={onSelect}
noItems={
filter.product === Product.Perpetual
? t('Perpetual markets coming soon.')
: filter.product === Product.Spot
? t('Spot markets coming soon.')
: t('No markets')
}
/>
</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">
{t('All markets')}
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
</Link>
</span>
</div>
</div>
);
};
const MarketList = ({
data,
error,
loading,
currentMarketId,
onSelect,
noItems,
}: {
data: MarketMaybeWithDataAndCandles[];
error: Error | undefined;
loading: boolean;
searchTerm: string;
currentMarketId?: string;
onSelect?: (marketId: string) => void;
noItems: string;
}) => {
if (error) {
return <div>{error.message}</div>;
}
return (
<AutoSizer>
{({ width, height }) => (
<TinyScroll>
<List
data={data}
loading={loading}
width={width}
height={height}
currentMarketId={currentMarketId}
onSelect={onSelect}
noItems={noItems}
/>
</TinyScroll>
)}
</AutoSizer>
);
};
const List = ({
data,
loading,
width,
height,
onSelect,
noItems,
currentMarketId,
}: {
data: MarketMaybeWithDataAndCandles[];
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}
/>
);
};
if (!data || loading) {
return (
<div style={{ width, height }}>
<Skeleton />
<Skeleton />
</div>
);
}
if (!data.length) {
return (
<div style={{ width, height }} data-testid="no-items">
<div className="mb-2 px-4">
<div className="text-sm bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg px-4 py-2">
{noItems}
</div>
</div>
</div>
);
}
return (
<FixedSizeList
className="virtualized-list"
itemCount={data.length}
itemSize={130}
width={width}
height={height}
>
{row}
</FixedSizeList>
);
};
const Skeleton = () => {
return (
<div className="mb-2 px-2">
<div className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg p-4">
<div className="w-full h-3 bg-vega-light-200 dark:bg-vega-dark-200 mb-2" />
<div className="w-2/3 h-3 bg-vega-light-200 dark:bg-vega-dark-200" />
</div>
</div>
);
};
+3 -1
View File
@@ -9,7 +9,8 @@ import {
import { AsyncRenderer, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
import { marketProvider, marketDataProvider } from '@vegaprotocol/market-list';
import { useGlobalStore, usePageTitleStore } from '../../stores';
import { TradeGrid, TradePanels } from './trade-grid';
import { TradeGrid } from './trade-grid';
import { TradePanels } from './trade-panels';
import { useNavigate, useParams } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
@@ -100,6 +101,7 @@ export const MarketPage = () => {
/>
);
}, [largeScreen, data, onSelect, navigate]);
if (!data && marketId) {
return (
<Splash>
@@ -0,0 +1,50 @@
import classNames from 'classnames';
// Make sure these match the available __typename properties on product
export const Product = {
Spot: 'Spot',
Future: 'Future',
Perpetual: 'Perpetual',
} as const;
export type ProductType = keyof typeof Product;
const ProductTypeMapping: {
[key in ProductType]: string;
} = {
[Product.Spot]: 'Spot',
[Product.Future]: 'Futures',
[Product.Perpetual]: 'Perpetuals',
};
export const ProductSelector = ({
product,
onSelect,
}: {
product: ProductType;
onSelect: (product: ProductType) => void;
}) => {
return (
<div className="flex gap-3 mb-3">
{Object.keys(Product).map((t) => {
const classes = classNames('py-1 border-b-2', {
'border-vega-yellow text-black dark:text-white': t === product,
'border-transparent text-vega-light-300 dark:text-vega-dark-300':
t !== product,
});
return (
<button
key={t}
onClick={() => {
onSelect(t as ProductType);
}}
className={classes}
data-testid={`product-${t}`}
>
{ProductTypeMapping[t as ProductType]}
</button>
);
})}
</div>
);
};
@@ -0,0 +1,76 @@
import { t } from '@vegaprotocol/i18n';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
export const Sort = {
None: 'None',
Gained: 'Gained',
Lost: 'Lost',
New: 'New',
} as const;
export type SortType = keyof typeof Sort;
export const SortTypeMapping: {
[key in SortType]: string;
} = {
[Sort.None]: 'None',
[Sort.Gained]: 'Top gaining',
[Sort.Lost]: 'Top losing',
[Sort.New]: 'New markets',
};
export const SortDropdown = ({
currentSort,
onSelect,
onReset,
}: {
currentSort: SortType;
onSelect: (sort: SortType) => void;
onReset: () => void;
}) => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger data-testid="sort-trigger">
<VegaIcon name={VegaIconNames.TREND_UP} />
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={currentSort}
onValueChange={(value) => onSelect(value as SortType)}
>
<DropdownMenuItem onClick={onReset}>{t('Reset')}</DropdownMenuItem>
<DropdownMenuSeparator />
{Object.keys(Sort)
.filter((s) => s !== Sort.None)
.map((key) => {
return (
<DropdownMenuRadioItem
inset
key={key}
value={key}
data-testid={`sort-item-${key}`}
>
{SortTypeMapping[key as SortType]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
);
})}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
+66 -195
View File
@@ -1,120 +1,37 @@
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
import { MarketInfoAccordionContainer } from '@vegaprotocol/market-info';
import { OrderbookContainer } from '@vegaprotocol/market-depth';
import { OrderListContainer, Filter } from '@vegaprotocol/orders';
import type { OrderListContainerProps } from '@vegaprotocol/orders';
import { FillsContainer } from '@vegaprotocol/fills';
import { PositionsContainer } from '@vegaprotocol/positions';
import { TradesContainer } from '@vegaprotocol/trades';
import { memo, useState } from 'react';
import type { ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { LayoutPriority } from 'allotment';
import classNames from 'classnames';
import AutoSizer from 'react-virtualized-auto-sizer';
import { memo, useState } from 'react';
import type { ReactNode, ComponentProps } from 'react';
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
import { OracleBanner } from '../../components/banner';
import {
Tab,
LocalStoragePersistTabs as Tabs,
Splash,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { AccountsContainer } from '../../components/accounts-container';
import type { Market } from '@vegaprotocol/market-list';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { TradeMarketHeader } from './trade-market-header';
import { NO_MARKET } from './constants';
import { LiquidityContainer } from '../liquidity/liquidity';
import { useNavigate } from 'react-router-dom';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { t } from '@vegaprotocol/i18n';
import { OracleBanner } from '@vegaprotocol/market-info';
import type { Market } from '@vegaprotocol/market-list';
import { Filter } from '@vegaprotocol/orders';
import {
usePaneLayout,
useScreenDimensions,
} from '@vegaprotocol/react-helpers';
import {
Tab,
LocalStoragePersistTabs as Tabs,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
useMarketClickHandler,
useMarketLiquidityClickHandler,
} from '../../lib/hooks/use-market-click-handler';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { HeaderTitle } from '../../components/header';
import {
ResizableGrid,
ResizableGridPanel,
} from '../../components/resizable-grid';
type MarketDependantView =
| typeof CandlesChartContainer
| typeof DepthChartContainer
| typeof DealTicketContainer
| typeof MarketInfoAccordionContainer
| typeof OrderbookContainer
| typeof TradesContainer;
type MarketDependantViewProps = ComponentProps<MarketDependantView>;
const requiresMarket = (View: MarketDependantView) => {
const WrappedComponent = (props: MarketDependantViewProps) =>
props.marketId ? <View {...props} /> : <Splash>{NO_MARKET}</Splash>;
WrappedComponent.displayName = `RequiresMarket(${View.name})`;
return WrappedComponent;
};
const TradingViews = {
candles: {
label: 'Candles',
component: requiresMarket(CandlesChartContainer),
},
depth: {
label: 'Depth',
component: requiresMarket(DepthChartContainer),
},
liquidity: {
label: 'Liquidity',
component: requiresMarket(LiquidityContainer),
},
ticket: {
label: 'Ticket',
component: requiresMarket(DealTicketContainer),
},
info: {
label: 'Info',
component: requiresMarket(MarketInfoAccordionContainer),
},
orderbook: {
label: 'Orderbook',
component: requiresMarket(OrderbookContainer),
},
trades: {
label: 'Trades',
component: requiresMarket(TradesContainer),
},
positions: { label: 'Positions', component: PositionsContainer },
activeOrders: {
label: 'Active',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Open} />
),
},
closedOrders: {
label: 'Closed',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Closed} />
),
},
rejectedOrders: {
label: 'Rejected',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Rejected} />
),
},
orders: {
label: 'All',
component: OrderListContainer,
},
collateral: { label: 'Collateral', component: AccountsContainer },
fills: { label: 'Fills', component: FillsContainer },
};
type TradingView = keyof typeof TradingViews;
import { TradingViews } from './trade-views';
import { MarketSelector } from './market-selector';
import { HeaderStats } from './header-stats';
interface TradeGridProps {
market: Market | null;
@@ -410,18 +327,57 @@ const MainGrid = memo(
);
MainGrid.displayName = 'MainGrid';
export const TradeGrid = ({
market,
onSelect,
pinnedAsset,
}: TradeGridProps) => {
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
const [sidebarOpen, setSidebarOpen] = useState(true);
const wrapperClasses = classNames(
'h-full grid',
'grid-rows-[min-content_min-content_1fr]',
'grid-cols-[320px_1fr]'
);
const paneWrapperClasses = classNames('min-h-0', {
'col-span-2 col-start-1': !sidebarOpen,
});
return (
<div className="h-full grid grid-rows-[min-content_1fr]">
<div>
<TradeMarketHeader market={market} onSelect={onSelect} />
<div className={wrapperClasses}>
<div className="border-b border-r border-default">
<div className="flex gap-2 justify-between items-center px-4 py-2">
<HeaderTitle
primaryContent={market?.tradableInstrument.instrument.code}
secondaryContent={market?.tradableInstrument.instrument.name}
/>
<button
onClick={() => setSidebarOpen((x) => !x)}
className="p-2"
data-testid="sidebar-toggle"
>
<span
className={classNames('block', {
'rotate-90 translate-x-1': !sidebarOpen,
'-rotate-90 -translate-x-1': sidebarOpen,
})}
>
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
</span>
</button>
</div>
</div>
<div className="border-b border-default min-w-0">
<HeaderStats market={market} />
</div>
<div className="col-span-2 bg-vega-green">
<OracleBanner marketId={market?.id || ''} />
</div>
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
{sidebarOpen && (
<div className="border-r border-default min-h-0">
<div className="h-full pb-8">
<MarketSelector currentMarketId={market?.id} />
</div>
</div>
)}
<div className={paneWrapperClasses}>
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
</div>
</div>
);
};
@@ -439,88 +395,3 @@ const TradeGridChild = ({ children }: TradeGridChildProps) => {
</section>
);
};
interface TradePanelsProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}
export const TradePanels = ({
market,
onSelect,
onClickCollateral,
pinnedAsset,
}: TradePanelsProps) => {
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [view, setView] = useState<TradingView>('candles');
const renderView = () => {
const Component = memo<{
marketId: string;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}>(TradingViews[view].component);
if (!Component) {
throw new Error(`No component for view: ${view}`);
}
if (!market) return <Splash>{NO_MARKET}</Splash>;
return (
<Component
marketId={market?.id}
onSelect={onSelect}
onClickCollateral={onClickCollateral}
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
/>
);
};
return (
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
<div>
<TradeMarketHeader market={market} onSelect={onSelect} />
<OracleBanner marketId={market?.id || ''} />
</div>
<div className="h-full">
<AutoSizer>
{({ width, height }) => (
<div style={{ width, height }} className="overflow-auto">
{renderView()}
</div>
)}
</AutoSizer>
</div>
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{Object.keys(TradingViews).map((key) => {
const isActive = view === key;
const className = classNames('p-4 min-w-[100px] capitalize', {
'text-black dark:text-vega-yellow': isActive,
'bg-neutral-200 dark:bg-neutral-800': isActive,
});
return (
<button
data-testid={key}
onClick={() => setView(key as TradingView)}
className={className}
key={key}
>
{TradingViews[key as keyof typeof TradingViews].label}
</button>
);
})}
</div>
</div>
);
};
@@ -0,0 +1,146 @@
import type { PinnedAsset } from '@vegaprotocol/accounts';
import type { Market } from '@vegaprotocol/market-list';
import { OracleBanner } from '@vegaprotocol/market-info';
import {
useMarketClickHandler,
useMarketLiquidityClickHandler,
} from '../../lib/hooks/use-market-click-handler';
import type { TradingView } from './trade-views';
import { TradingViews } from './trade-views';
import { memo, useState } from 'react';
import {
Icon,
Splash,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { NO_MARKET } from './constants';
import AutoSizer from 'react-virtualized-auto-sizer';
import classNames from 'classnames';
import { HeaderStats } from './header-stats';
import * as DialogPrimitives from '@radix-ui/react-dialog';
import { HeaderTitle } from '../../components/header';
import { MarketSelector } from './market-selector';
interface TradePanelsProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}
export const TradePanels = ({
market,
onSelect,
onClickCollateral,
pinnedAsset,
}: TradePanelsProps) => {
const [drawerOpen, setDrawerOpen] = useState(false);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [view, setView] = useState<TradingView>('candles');
const renderView = () => {
const Component = memo<{
marketId: string;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}>(TradingViews[view].component);
if (!Component) {
throw new Error(`No component for view: ${view}`);
}
if (!market) return <Splash>{NO_MARKET}</Splash>;
return (
<Component
marketId={market?.id}
onSelect={onSelect}
onClickCollateral={onClickCollateral}
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
/>
);
};
return (
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
<div className="border-b border-default min-w-0">
<div className="flex gap-4 items-center px-4 py-2">
<HeaderTitle
primaryContent={market?.tradableInstrument.instrument.code}
secondaryContent={market?.tradableInstrument.instrument.name}
/>
<button onClick={() => setDrawerOpen((x) => !x)} className="p-2">
<span
className={classNames('block', {
'rotate-90 translate-x-1': !drawerOpen,
'-rotate-90 -translate-x-1': drawerOpen,
})}
>
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
</span>
</button>
</div>
<HeaderStats market={market} />
</div>
<div>
<OracleBanner marketId={market?.id || ''} />
</div>
<div className="h-full">
<AutoSizer>
{({ width, height }) => (
<div style={{ width, height }} className="overflow-auto">
{renderView()}
</div>
)}
</AutoSizer>
</div>
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{Object.keys(TradingViews).map((key) => {
const isActive = view === key;
const className = classNames('p-4 min-w-[100px] capitalize', {
'text-black dark:text-vega-yellow': isActive,
'bg-neutral-200 dark:bg-neutral-800': isActive,
});
return (
<button
data-testid={key}
onClick={() => setView(key as TradingView)}
className={className}
key={key}
>
{TradingViews[key as keyof typeof TradingViews].label}
</button>
);
})}
</div>
<DialogPrimitives.Root open={drawerOpen} onOpenChange={setDrawerOpen}>
<DialogPrimitives.Portal>
<DialogPrimitives.Overlay />
<DialogPrimitives.Content
className={classNames(
'fixed h-full max-w-[500px] w-[90vw] z-10 top-0 left-0 transition-transform',
'bg-white dark:bg-black',
'border-r border-default'
)}
>
<DialogPrimitives.Close className="absolute top-0 right-0 p-2">
<Icon name="cross" />
</DialogPrimitives.Close>
{drawerOpen && (
<MarketSelector onSelect={() => setDrawerOpen(false)} />
)}
</DialogPrimitives.Content>
</DialogPrimitives.Portal>
</DialogPrimitives.Root>
</div>
);
};
@@ -0,0 +1,90 @@
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
import { MarketInfoAccordionContainer } from '@vegaprotocol/market-info';
import { OrderbookContainer } from '@vegaprotocol/market-depth';
import { OrderListContainer, Filter } from '@vegaprotocol/orders';
import type { OrderListContainerProps } from '@vegaprotocol/orders';
import { FillsContainer } from '@vegaprotocol/fills';
import { PositionsContainer } from '@vegaprotocol/positions';
import { TradesContainer } from '@vegaprotocol/trades';
import type { ComponentProps } from 'react';
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { AccountsContainer } from '../../components/accounts-container';
import { NO_MARKET } from './constants';
import { LiquidityContainer } from '../liquidity/liquidity';
type MarketDependantView =
| typeof CandlesChartContainer
| typeof DepthChartContainer
| typeof DealTicketContainer
| typeof MarketInfoAccordionContainer
| typeof OrderbookContainer
| typeof TradesContainer;
type MarketDependantViewProps = ComponentProps<MarketDependantView>;
const requiresMarket = (View: MarketDependantView) => {
const WrappedComponent = (props: MarketDependantViewProps) =>
props.marketId ? <View {...props} /> : <Splash>{NO_MARKET}</Splash>;
WrappedComponent.displayName = `RequiresMarket(${View.name})`;
return WrappedComponent;
};
export type TradingView = keyof typeof TradingViews;
export const TradingViews = {
candles: {
label: 'Candles',
component: requiresMarket(CandlesChartContainer),
},
depth: {
label: 'Depth',
component: requiresMarket(DepthChartContainer),
},
liquidity: {
label: 'Liquidity',
component: requiresMarket(LiquidityContainer),
},
ticket: {
label: 'Ticket',
component: requiresMarket(DealTicketContainer),
},
info: {
label: 'Info',
component: requiresMarket(MarketInfoAccordionContainer),
},
orderbook: {
label: 'Orderbook',
component: requiresMarket(OrderbookContainer),
},
trades: {
label: 'Trades',
component: requiresMarket(TradesContainer),
},
positions: { label: 'Positions', component: PositionsContainer },
activeOrders: {
label: 'Active',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Open} />
),
},
closedOrders: {
label: 'Closed',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Closed} />
),
},
rejectedOrders: {
label: 'Rejected',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Rejected} />
),
},
orders: {
label: 'All',
component: OrderListContainer,
},
collateral: { label: 'Collateral', component: AccountsContainer },
fills: { label: 'Fills', component: FillsContainer },
};
@@ -0,0 +1,473 @@
import merge from 'lodash/merge';
import { renderHook } from '@testing-library/react';
import { useMarketSelectorList } from './use-market-selector-list';
import { Product } from './product-selector';
import { Sort } from './sort-dropdown';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketState } from '@vegaprotocol/types';
import { useMarketList } from '@vegaprotocol/market-list';
import type { Filter } from './market-selector';
import { subDays } from 'date-fns';
jest.mock('@vegaprotocol/market-list');
const mockUseMarketList = useMarketList as jest.Mock;
describe('useMarketSelectorList', () => {
const setup = (initialArgs?: Partial<Filter>) => {
const defaultArgs: Filter = {
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: [],
};
return renderHook((args) => useMarketSelectorList(args), {
initialProps: merge(defaultArgs, initialArgs),
});
};
it('returns all markets active and suspended markets', () => {
const markets = [
createMarketFragment({ id: 'market-0' }),
createMarketFragment({
id: 'market-1',
state: MarketState.STATE_SUSPENDED,
}),
createMarketFragment({
id: 'market-2',
state: MarketState.STATE_CLOSED,
}),
createMarketFragment({
id: 'market-3',
state: MarketState.STATE_PENDING,
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result } = setup();
const expectedFilteredMarkets = markets.filter((m) =>
[MarketState.STATE_ACTIVE, MarketState.STATE_SUSPENDED].includes(m.state)
);
expect(result.current).toEqual({
data: markets,
markets: expectedFilteredMarkets,
loading: false,
error: undefined,
});
});
it('filters by product', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
product: {
__typename: 'Future',
},
},
},
}),
createMarketFragment({
id: 'market-1',
tradableInstrument: {
instrument: {
product: {
__typename: 'Spot' as 'Future', // spot isn't in schema yet
},
},
},
}),
createMarketFragment({
id: 'market-2',
tradableInstrument: {
instrument: {
product: {
__typename: 'Perpetual' as 'Future', // spot isn't in schema yet
},
},
},
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result, rerender } = setup();
expect(result.current.markets).toEqual([markets[0]]);
rerender({
searchTerm: '',
product: Product.Spot as 'Future',
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[1]]);
rerender({
searchTerm: '',
product: Product.Perpetual as 'Future',
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[2]]);
});
it('filters by asset', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-0',
},
},
},
},
}),
createMarketFragment({
id: 'market-1',
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-0',
},
},
},
},
}),
createMarketFragment({
id: 'market-2',
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-1',
},
},
},
},
}),
createMarketFragment({
id: 'market-3',
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-2',
},
},
},
},
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result, rerender } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: ['asset-0'],
});
expect(result.current.markets).toEqual([markets[0], markets[1]]);
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: ['asset-0', 'asset-1'],
});
expect(result.current.markets).toEqual([
markets[0],
markets[1],
markets[2],
]);
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: ['asset-0', 'asset-1', 'asset-2'],
});
// all assets selected
expect(result.current.markets).toEqual(markets);
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: ['asset-invalid'],
});
expect(result.current.markets).toEqual([]);
});
it('filters by search term', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
code: 'abc',
name: 'aaa',
},
},
}),
createMarketFragment({
id: 'market-1',
tradableInstrument: {
instrument: {
code: 'def',
name: 'ggg',
},
},
}),
createMarketFragment({
id: 'market-2',
tradableInstrument: {
instrument: {
code: 'defg',
name: 'gggh',
},
},
}),
createMarketFragment({
id: 'market-3',
tradableInstrument: {
instrument: {
code: 'ggg',
name: 'foo',
},
},
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result, rerender } = setup({
searchTerm: 'abc',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[0]]);
rerender({
searchTerm: 'def',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[1], markets[2]]);
rerender({
searchTerm: 'defg',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[2]]);
rerender({
searchTerm: 'zzz',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([]);
// by name
rerender({
searchTerm: 'aaa',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[0]]);
rerender({
searchTerm: 'ggg',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([
markets[1],
markets[2],
markets[3],
]);
});
it('sorts albphabetically', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
code: 'd',
},
},
}),
createMarketFragment({
id: 'market-1',
tradableInstrument: {
instrument: {
code: 'b',
},
},
}),
createMarketFragment({
id: 'market-2',
tradableInstrument: {
instrument: {
code: 'a',
},
},
}),
createMarketFragment({
id: 'market-3',
tradableInstrument: {
instrument: {
code: 'c',
},
},
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([
markets[2],
markets[1],
markets[3],
markets[0],
]);
});
it('sorts by gained', () => {
const markets = [
createMarketFragment({
id: 'market-0',
// @ts-ignore actual fragment doesnt contain candles and is joined later
candles: [
{
close: '100',
},
{
close: '200',
},
],
}),
createMarketFragment({
id: 'market-1',
// @ts-ignore actual fragment doesnt contain candles and is joined later
candles: [
{
close: '100',
},
{
close: '1000',
},
],
}),
createMarketFragment({
id: 'market-2',
// @ts-ignore actual fragment doesnt contain candles and is joined later
candles: [
{
close: '100',
},
{
close: '400',
},
],
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result, rerender } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.Gained,
assets: [],
});
expect(result.current.markets).toEqual([
markets[1],
markets[2],
markets[0],
]);
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.Lost as 'Gained',
assets: [],
});
expect(result.current.markets).toEqual([
markets[0],
markets[2],
markets[1],
]);
});
it('sorts by open timestamp', () => {
const markets = [
createMarketFragment({
id: 'market-0',
marketTimestamps: {
open: subDays(new Date(), 3).toISOString(),
},
}),
createMarketFragment({
id: 'market-1',
marketTimestamps: {
open: subDays(new Date(), 1).toISOString(),
},
}),
createMarketFragment({
id: 'market-2',
marketTimestamps: {
open: subDays(new Date(), 2).toISOString(),
},
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.New,
assets: [],
});
expect(result.current.markets).toEqual([
markets[1],
markets[2],
markets[0],
]);
});
});
@@ -0,0 +1,82 @@
import { useMemo } from 'react';
import orderBy from 'lodash/orderBy';
import { MarketState } from '@vegaprotocol/types';
import { useMarketList } from '@vegaprotocol/market-list';
import { priceChangePercentage } from '@vegaprotocol/utils';
import type { Filter } from './market-selector';
import { Sort } from './sort-dropdown';
export const useMarketSelectorList = ({
product,
assets,
sort,
searchTerm,
}: Filter) => {
const { data, loading, error } = useMarketList();
const markets = useMemo(() => {
if (!data?.length) return [];
const markets = data
// only active
.filter((m) => {
return [MarketState.STATE_ACTIVE, MarketState.STATE_SUSPENDED].includes(
m.state
);
})
// only selected product type
.filter((m) => {
if (m.tradableInstrument.instrument.product.__typename === product) {
return true;
}
return false;
})
.filter((m) => {
if (assets.length === 0) return true;
return assets.includes(
m.tradableInstrument.instrument.product.settlementAsset.id
);
})
// filter based on search term
.filter((m) => {
const code = m.tradableInstrument.instrument.code.toLowerCase();
const name = m.tradableInstrument.instrument.name.toLowerCase();
if (
code.includes(searchTerm.toLowerCase()) ||
name.includes(searchTerm.toLowerCase())
) {
return true;
}
return false;
});
if (sort === Sort.None) {
return orderBy(markets, ['tradableInstrument.instrument.code'], ['asc']);
}
if (sort === Sort.Gained || sort === Sort.Lost) {
const dir = sort === Sort.Gained ? 'desc' : 'asc';
return orderBy(
markets,
[
(m) => {
if (!m.candles?.length) return 0;
return Number(priceChangePercentage(m.candles.map((c) => c.close)));
},
],
[dir]
);
}
if (sort === Sort.New) {
return orderBy(
markets,
[(m) => new Date(m.marketTimestamps.open).getTime()],
['desc']
);
}
return markets;
}, [data, product, searchTerm, assets, sort]);
return { markets, data, loading, error };
};
@@ -6,8 +6,8 @@ import { MarketState } from '@vegaprotocol/types';
import { subDays } from 'date-fns';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/oracles';
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/oracles';
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/market-info';
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/market-info';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type {
@@ -2,8 +2,8 @@ import { render, screen } from '@testing-library/react';
import type { Property } from '@vegaprotocol/types';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/oracles';
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/oracles';
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/market-info';
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/market-info';
import type { SettlementPriceCellProps } from './settlement-price-cell';
import { SettlementPriceCell } from './settlement-price-cell';
@@ -1,6 +1,6 @@
import { DApp, EXPLORER_ORACLE, useLinks } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { useOracleSpecBindingData } from '@vegaprotocol/oracles';
import { useOracleSpecBindingData } from '@vegaprotocol/market-info';
import { Link } from '@vegaprotocol/ui-toolkit';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
@@ -1,7 +1,7 @@
import { Icon, NavigationLink } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { Links, Routes } from '../../pages/client-router';
import { COG } from '@blueprintjs/icons/src/generated/iconNames';
import { IconNames } from '@blueprintjs/icons';
export const SettingsButton = ({ withMobile }: { withMobile?: boolean }) => {
return (
@@ -9,7 +9,7 @@ export const SettingsButton = ({ withMobile }: { withMobile?: boolean }) => {
{withMobile ? (
t('Settings')
) : (
<Icon name={COG} className="!align-middle" />
<Icon name={IconNames.COG} className="!align-middle" />
)}
</NavigationLink>
);
@@ -1,5 +1,6 @@
import type { InMemoryCacheConfig } from '@apollo/client';
import {
AppFailure,
NetworkLoader,
NodeGuard,
useEnvironment,
@@ -9,7 +10,6 @@ import { MaintenancePage } from '@vegaprotocol/ui-toolkit';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import dynamic from 'next/dynamic';
import type { ReactNode } from 'react';
import { AppFailure } from './app-failure';
import { Web3Provider } from './web3-provider';
export const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
@@ -1,3 +1,2 @@
export * from './app-failure';
export * from './app-loader';
export * from './web3-provider';
-1
View File
@@ -1,2 +1 @@
export * from './announcement-banner';
export * from './oracle-banner';
@@ -1,45 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { useMarketOracle } from '@vegaprotocol/market-info';
import ReactMarkdown from 'react-markdown';
import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit';
const oracleStatuses = {
UNKNOWN: t(
"This public key's proofs have not been verified yet, or no proofs have been provided yet."
),
GOOD: t("This public key's proofs have been verified."),
SUSPICIOUS: t(
'This public key is suspected to be acting in bad faith, pending investigation.'
),
MALICIOUS: t('This public key has been observed acting in bad faith.'),
RETIRED: t('This public key is no longer in use.'),
COMPROMISED: t(
'This public key is no longer in the control of its original owners.'
),
};
export const OracleBanner = ({ marketId }: { marketId: string }) => {
const oracle = useMarketOracle(marketId);
if (!oracle || oracle.status === 'GOOD') {
return null;
}
return (
<NotificationBanner intent={Intent.Danger}>
<div>
Oracle status for this market is{' '}
<span data-testId="oracle-status">{oracle.status}</span>.{' '}
{oracleStatuses[oracle.status]}
</div>
{oracle.status_reason ? (
<ReactMarkdown
className="react-markdown-container"
skipHtml={true}
disallowedElements={['img']}
linkTarget="_blank"
>
{oracle.status_reason}
</ReactMarkdown>
) : null}
</NotificationBanner>
);
};
@@ -4,18 +4,15 @@ import { NodeHealth, NodeUrl, HealthIndicator } from './footer';
import { MockedProvider } from '@apollo/client/testing';
import { Intent } from '@vegaprotocol/ui-toolkit';
const mockSetNodeSwitcher = jest.fn();
jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'),
useEnvironment: jest.fn().mockImplementation(() => ({
VEGA_URL: 'https://vega-url.wtf',
VEGA_INCIDENT_URL: 'https://blog.vega.community',
})),
}));
const mockSetNodeSwitcher = jest.fn();
jest.mock('../../stores', () => ({
...jest.requireActual('../../stores'),
useGlobalStore: () => mockSetNodeSwitcher,
useNodeSwitcherStore: jest.fn(() => mockSetNodeSwitcher),
}));
describe('NodeHealth', () => {
+6 -5
View File
@@ -1,11 +1,14 @@
import { useCallback } from 'react';
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
import {
useEnvironment,
useNodeHealth,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import type { Intent } from '@vegaprotocol/ui-toolkit';
import { Indicator, ExternalLink } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useGlobalStore } from '../../stores';
export const Footer = () => {
return (
@@ -20,9 +23,7 @@ export const Footer = () => {
export const NodeHealth = () => {
const { VEGA_URL, VEGA_INCIDENT_URL } = useEnvironment();
const setNodeSwitcher = useGlobalStore(
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
);
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
const { datanodeBlockHeight, text, intent } = useNodeHealth();
const onClick = useCallback(() => {
setNodeSwitcher(true);
+2 -2
View File
@@ -43,7 +43,7 @@ export const HeaderStat = ({
testId?: string;
}) => {
const itemClass =
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default text-neutral-500 dark:text-neutral-400';
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default first:border-none text-neutral-500 dark:text-neutral-400';
const itemHeading = 'text-black dark:text-white';
return (
@@ -72,7 +72,7 @@ export const HeaderTitle = ({
secondaryContent: ReactNode;
}) => {
return (
<div className="text-left">
<div className="text-left" data-testid="header-title">
<div className="text-sm md:text-md lg:text-lg whitespace-nowrap leading-4">
{primaryContent}
</div>
@@ -0,0 +1,42 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { Navbar } from './navbar';
describe('Navbar', () => {
const pubKey = 'pubKey';
it('should be properly rendered', () => {
render(
<MockedProvider>
<MemoryRouter>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Navbar theme="dark" />
</VegaWalletContext.Provider>
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByTestId('Markets')).toBeInTheDocument();
expect(screen.getByTestId('Trading')).toBeInTheDocument();
expect(screen.getByTestId('Portfolio')).toBeInTheDocument();
});
it('Markets page route should not match empty market page', () => {
render(
<MockedProvider>
<MemoryRouter initialEntries={['/markets/all']}>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Navbar theme="dark" />
</VegaWalletContext.Provider>
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByTestId('Markets')).toHaveClass('active');
expect(screen.getByTestId('Trading')).not.toHaveClass('active');
});
});
+1 -1
View File
@@ -75,7 +75,7 @@ export const Navbar = ({
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink data-testid="Trading" to={tradingPath}>
<NavigationLink data-testid="Trading" to={tradingPath} end>
{t('Trading')}
</NavigationLink>
</NavigationItem>
@@ -1,3 +1,2 @@
export * from './select-market-columns';
export * from './select-market-table';
export * from './select-market';
@@ -7,7 +7,7 @@ import {
} from '@vegaprotocol/market-list';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { PriceCell, signedNumberCssClass } from '@vegaprotocol/datagrid';
import { PriceCell } from '@vegaprotocol/datagrid';
import { Link as UILink, Sparkline, Tooltip } from '@vegaprotocol/ui-toolkit';
import isNil from 'lodash/isNil';
import type { CandleClose } from '@vegaprotocol/types';
@@ -341,187 +341,3 @@ export const columns = (
];
return selectMarketColumns;
};
export const columnsPositionMarkets = (
market: MarketMaybeWithDataAndCandles,
onSelect: (id: string, metaKey?: boolean) => void,
inViewRoot?: RefObject<HTMLElement>,
openVolume?: string,
onCellClick?: OnCellClickHandler
) => {
const candlesClose = market.candles
?.map((candle) => candle?.close)
.filter((c: string | undefined): c is CandleClose => !isNil(c));
const candleLow = market.candles && calcCandleLow(market.candles);
const candleHigh = market.candles && calcCandleHigh(market.candles);
const candleVolume = market.candles && calcCandleVolume(market.candles);
const selectMarketColumns: Column[] = [
{
kind: ColumnKind.Market,
value: (
<Link
to={Links[Routes.MARKET](market.id)}
data-testid={`market-link-${market.id}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onSelect(market.id, e.metaKey || e.ctrlKey);
}}
>
<UILink>{market.tradableInstrument.instrument.code}</UILink>
</Link>
),
className: cellClassNames,
onlyOnDetailed: false,
},
{
kind: ColumnKind.ProductType,
value: market.tradableInstrument.instrument.product.__typename,
className: `py-2 first:text-left hidden sm:table-cell`,
onlyOnDetailed: false,
},
{
kind: ColumnKind.LastPrice,
value: (
<MarketMarkPrice
marketId={market.id}
decimalPlaces={market?.decimalPlaces}
inViewRoot={inViewRoot}
initialValue={market.data?.markPrice}
asPriceCell
/>
),
className: cellClassNames,
onlyOnDetailed: false,
},
{
kind: ColumnKind.Change24,
value: (
<Last24hPriceChange
marketId={market.id}
decimalPlaces={market?.decimalPlaces}
inViewRoot={inViewRoot}
initialValue={candlesClose}
/>
),
className: cellClassNames,
onlyOnDetailed: false,
},
{
kind: ColumnKind.Sparkline,
value: candlesClose && (
<Sparkline
width={100}
height={20}
muted={false}
data={candlesClose.map((c: string) => Number(c))}
/>
),
className: `${cellClassNames} hidden lg:table-cell`,
onlyOnDetailed: false,
},
{
kind: ColumnKind.Asset,
value: (
<button
data-dialog-trigger
className="inline underline"
onClick={(e) => {
e.stopPropagation();
if (!onCellClick) return;
onCellClick(
e,
ColumnKind.Asset,
market.tradableInstrument.instrument.product.settlementAsset.id
);
}}
>
{market.tradableInstrument.instrument.product.settlementAsset.symbol}
</button>
),
className: `${cellClassNames} hidden sm:table-cell`,
onlyOnDetailed: false,
},
{
kind: ColumnKind.High24,
value: candleHigh ? (
<PriceCell
value={Number(candleHigh)}
valueFormatted={addDecimalsFormatNumber(
candleHigh.toString(),
market.decimalPlaces,
2
)}
/>
) : (
'-'
),
className: `${cellClassNames} hidden xl:table-cell font-mono`,
onlyOnDetailed: true,
},
{
kind: ColumnKind.Low24,
value: candleLow ? (
<PriceCell
value={Number(candleLow)}
valueFormatted={addDecimalsFormatNumber(
candleLow.toString(),
market.decimalPlaces,
2
)}
/>
) : (
'-'
),
className: `${cellClassNames} hidden xl:table-cell font-mono`,
onlyOnDetailed: true,
},
{
kind: ColumnKind.Volume,
value: (
<Last24hVolume
marketId={market.id}
inViewRoot={inViewRoot}
positionDecimalPlaces={market.positionDecimalPlaces}
initialValue={candleVolume}
formatDecimals={2}
/>
),
className: `${cellClassNames} hidden lg:table-cell font-mono`,
onlyOnDetailed: true,
dataTestId: 'market-volume',
},
{
kind: ColumnKind.TradingMode,
value: (
<MarketTradingMode
marketId={market?.id}
inViewRoot={inViewRoot}
initialTradingMode={market.tradingMode}
initialTrigger={market.data?.trigger}
/>
),
className: `${cellClassNames} hidden lg:table-cell`,
onlyOnDetailed: true,
dataTestId: 'trading-mode-col',
},
{
kind: ColumnKind.Fee,
value: <FeesCell feeFactors={market.fees.factors} />,
className: `${cellClassNames} hidden xl:table-cell font-mono`,
onlyOnDetailed: true,
},
{
kind: ColumnKind.Position,
value: (
<p className={signedNumberCssClass(openVolume || '')}>
{openVolume &&
addDecimalsFormatNumber(openVolume, market.positionDecimalPlaces)}
</p>
),
className: `${cellClassNames} hidden xxl:table-cell font-mono`,
onlyOnDetailed: true,
},
];
return selectMarketColumns;
};
@@ -1,7 +1,6 @@
import { columnHeaders } from './select-market-columns';
import classNames from 'classnames';
import type { Column } from './select-market-columns';
import type { ReactNode } from 'react';
export const SelectMarketTableHeader = ({
detailed = false,
@@ -62,23 +61,3 @@ export const SelectMarketTableRow = ({
</tr>
);
};
export const SelectMarketTableRowSplash = ({
children,
colSpan,
}: {
children: ReactNode;
colSpan: number;
}) => {
return (
<tr className={`relative`}>
<td
className="text-center p-10 pt-14 text-xs"
key="splash"
colSpan={colSpan}
>
{children}
</td>
</tr>
);
};
@@ -1,203 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as Schema from '@vegaprotocol/types';
import { SelectAllMarketsTableBody } from './select-market';
import type {
MarketMaybeWithCandles,
MarketMaybeWithData,
MarketData,
} from '@vegaprotocol/market-list';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
type Market = MarketMaybeWithCandles & MarketMaybeWithData;
type PartialMarket = Partial<
Omit<Market, 'data'> & { data: Partial<MarketData> }
>;
const MARKET_A: PartialMarket = {
__typename: 'Market',
id: '1',
decimalPlaces: 2,
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '1',
code: 'ABCDEF',
name: 'ABCDEF 1-Day',
product: {
__typename: 'Future',
quoteName: 'ABCDEF',
settlementAsset: {
__typename: 'Asset',
id: 'asset-ABC',
name: '',
decimals: 2,
symbol: 'ABC',
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: 'oracleId',
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: 'oracleId',
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
tradingTerminationProperty: 'trading-termination-property',
settlementDataProperty: 'settlement-data-property',
},
},
metadata: {
__typename: 'InstrumentMetadata',
tags: ['ABC', 'DEF'],
},
},
},
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
infrastructureFee: '0.01',
liquidityFee: '0.01',
makerFee: '0.01',
},
},
data: {
__typename: 'MarketData',
market: {
__typename: 'Market',
id: '1',
},
markPrice: '90',
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING,
marketState: Schema.MarketState.STATE_PENDING,
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
indicativeVolume: '1000',
},
candles: [
{
__typename: 'Candle',
high: '100',
low: '10',
open: '10',
close: '80',
volume: '1000',
periodStart: '2022-11-01T15:49:00Z',
},
{
__typename: 'Candle',
high: '10',
low: '1',
open: '1',
close: '100',
volume: '1000',
periodStart: '2022-11-01T15:50:00Z',
},
],
};
const MARKET_B: PartialMarket = {
__typename: 'Market',
id: '2',
decimalPlaces: 2,
positionDecimalPlaces: 0,
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '2',
code: 'XYZ',
name: 'XYZ 1-Day',
product: {
__typename: 'Future',
quoteName: 'XYZ',
settlementAsset: {
__typename: 'Asset',
id: 'asset-XYZ',
name: 'asset-XYZ',
decimals: 2,
symbol: 'XYZ',
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: 'oracleId',
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: 'oracleId',
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
tradingTerminationProperty: 'trading-termination-property',
settlementDataProperty: 'settlement-data-property',
},
},
metadata: {
__typename: 'InstrumentMetadata',
tags: ['XYZ'],
},
},
},
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
infrastructureFee: '0.01',
liquidityFee: '0.01',
makerFee: '0.01',
},
},
data: {
__typename: 'MarketData',
market: {
__typename: 'Market',
id: '2',
},
markPrice: '123.123',
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING,
marketState: Schema.MarketState.STATE_PENDING,
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
indicativeVolume: '2000',
},
candles: [
{
__typename: 'Candle',
high: '100',
low: '10',
open: '10',
close: '80',
volume: '1000',
periodStart: '2022-11-01T15:49:00Z',
},
],
};
describe('SelectMarket', () => {
const table = document.createElement('table');
it('should render the SelectAllMarketsTableBody', () => {
const onSelect = jest.fn();
const onCellClick = jest.fn();
const { container } = render(
<MemoryRouter>
<SelectAllMarketsTableBody
markets={[MARKET_A as Market, MARKET_B as Market]}
onCellClick={onCellClick}
onSelect={onSelect}
/>
</MemoryRouter>,
{ wrapper: MockedProvider, container: document.body.appendChild(table) }
);
expect(screen.getByText('ABCDEF')).toBeTruthy(); // name
expect(screen.getByText('25.00%')).toBeTruthy(); // price change
expect(container).toHaveTextContent(/1,000/); // volume
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
expect(onSelect).toHaveBeenCalledWith('1', false);
});
});
@@ -1,214 +0,0 @@
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import type { RefObject } from 'react';
import { useMarketList } from '@vegaprotocol/market-list';
import { positionsDataProvider } from '@vegaprotocol/positions';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { ExternalLink, Icon, Loader, Popover } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
columnHeaders,
columnHeadersPositionMarkets,
columns,
columnsPositionMarkets,
} from './select-market-columns';
import {
SelectMarketTableHeader,
SelectMarketTableRow,
SelectMarketTableRowSplash,
} from './select-market-table';
import type { ReactNode } from 'react';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/market-list';
import type { PositionFieldsFragment } from '@vegaprotocol/positions';
import type { Column, OnCellClickHandler } from './select-market-columns';
import {
DApp,
TOKEN_NEW_MARKET_PROPOSAL,
useLinks,
} from '@vegaprotocol/environment';
import { HeaderTitle } from '../header';
export const SelectAllMarketsTableBody = ({
markets,
positions,
onSelect,
onCellClick,
inViewRoot,
headers = columnHeaders,
tableColumns = (market) => columns(market, onSelect, onCellClick, inViewRoot),
}: {
markets?: MarketMaybeWithDataAndCandles[] | null;
positions?: PositionFieldsFragment[];
title?: string;
onSelect: (id: string, metaKey?: boolean) => void;
onCellClick: OnCellClickHandler;
headers?: Column[];
tableColumns?: (
market: MarketMaybeWithDataAndCandles,
inViewRoot?: RefObject<HTMLDivElement>,
openVolume?: string
) => Column[];
inViewRoot?: RefObject<HTMLDivElement>;
}) => {
const tokenLink = useLinks(DApp.Token);
if (!markets) return null;
return (
<>
<thead className="bg-neutral-100 dark:bg-neutral-800">
<SelectMarketTableHeader detailed={true} headers={headers} />
</thead>
{/* Border styles required to create space between tbody elements margin/padding don't work */}
<tbody className="border-b-[10px] border-transparent">
{markets.length > 0 ? (
markets?.map((market, i) => (
<SelectMarketTableRow
marketId={market.id}
key={i}
detailed
onSelect={onSelect}
columns={tableColumns(
market,
inViewRoot,
positions &&
positions.find((p) => p.market.id === market.id)?.openVolume
)}
/>
))
) : (
<SelectMarketTableRowSplash colSpan={12}>
{t('No markets ')}
<ExternalLink href={tokenLink(TOKEN_NEW_MARKET_PROPOSAL)}>
{t('Propose a new market')}
</ExternalLink>
</SelectMarketTableRowSplash>
)}
</tbody>
</>
);
};
export const SelectMarketPopover = ({
marketCode,
marketName,
onSelect,
onCellClick,
}: {
marketCode: string;
marketName: string;
onSelect: (id: string, metaKey?: boolean) => void;
onCellClick: OnCellClickHandler;
}) => {
const { pubKey } = useVegaWallet();
const [open, setOpen] = useState(false);
const inViewRoot = useRef<HTMLDivElement>(null);
const {
data,
loading: marketsLoading,
reload: marketListReload,
} = useMarketList();
const {
data: positions,
loading: positionsLoading,
reload,
} = useDataProvider({
dataProvider: positionsDataProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const onSelectMarket = useCallback(
(marketId: string, metaKey?: boolean) => {
onSelect(marketId, metaKey);
setOpen(false);
},
[onSelect]
);
const iconClass = open ? 'rotate-180' : '';
const markets = useMemo(
() =>
data?.filter((market) =>
positions?.find((node) => node.market.id === market.id)
),
[data, positions]
);
useEffect(() => {
if (open) {
reload();
marketListReload();
}
}, [open, marketListReload, reload]);
return (
<Popover
open={open}
onChange={setOpen}
trigger={
<div className="flex items-center gap-2">
<HeaderTitle
primaryContent={marketCode}
secondaryContent={marketName}
/>
<Icon name="chevron-down" className={iconClass} size={6} />
</div>
}
>
<div
className="w-[90vw] max-h-[80vh] overflow-y-auto"
data-testid="select-market-list"
ref={inViewRoot}
>
{marketsLoading || (pubKey && positionsLoading) ? (
<div className="flex items-center gap-4">
<Loader size="small" />
{t('Loading market data')}
</div>
) : (
<table className="relative text-sm w-full whitespace-nowrap">
{pubKey && (positions?.length ?? 0) && (markets?.length ?? 0) ? (
<>
<TableTitle>{t('My markets')}</TableTitle>
<SelectAllMarketsTableBody
inViewRoot={inViewRoot}
markets={markets}
positions={positions || undefined}
onSelect={onSelectMarket}
onCellClick={onCellClick}
headers={columnHeadersPositionMarkets}
tableColumns={(market, inViewRoot, openVolume) =>
columnsPositionMarkets(
market,
onSelectMarket,
inViewRoot,
openVolume,
onCellClick
)
}
/>
</>
) : null}
<TableTitle>{t('All markets')}</TableTitle>
<SelectAllMarketsTableBody
inViewRoot={inViewRoot}
markets={data}
onSelect={onSelectMarket}
onCellClick={onCellClick}
/>
</table>
)}
</div>
</Popover>
);
};
const TableTitle = ({ children }: { children: ReactNode }) => {
return (
<thead>
<tr>
<th className="font-normal text-left">
<h3 className="text-lg">{children}</h3>
</th>
</tr>
</thead>
);
};
@@ -7,7 +7,13 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { RISK_ACCEPTED_KEY } from '../constants';
import { TelemetryApproval } from './telemetry-approval';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import {
DOCS_VEGA_WALLET,
GET_VEGA_WALLET_URL,
Networks,
useDocsLink,
useEnvironment,
} from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
interface Props {
@@ -46,8 +52,9 @@ export const RiskNoticeDialog = ({ onClose, network }: Props) => {
};
const TestnetContent = ({ network }: { network: Networks }) => {
const { VEGA_DOCS_URL, GITHUB_FEEDBACK_URL, VEGA_WALLET_URL } =
useEnvironment();
const { GITHUB_FEEDBACK_URL } = useEnvironment();
const docsLink = useDocsLink();
return (
<>
<p className="mb-4">
@@ -62,17 +69,17 @@ const TestnetContent = ({ network }: { network: Networks }) => {
[network]
)}
</p>
{VEGA_DOCS_URL && GITHUB_FEEDBACK_URL && VEGA_WALLET_URL && (
{GITHUB_FEEDBACK_URL && GET_VEGA_WALLET_URL && docsLink && (
<ul className="list-disc pl-4">
<li className="mb-1">
<Link href={VEGA_WALLET_URL} target="_blank">
<span className="underline">{t('Learn about Vega wallet')}</span>{' '}
<Link href={GET_VEGA_WALLET_URL} target="_blank">
<span className="underline">{t('Get a Vega Wallet')}</span>{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
</li>
<li className="mb-1">
<Link href={VEGA_DOCS_URL} target="_blank">
<span className="underline">{t('View documentation')}</span>{' '}
<Link href={docsLink(DOCS_VEGA_WALLET)} target="_blank">
<span className="underline">{t('Learn about Vega Wallet')}</span>{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
</li>
+8 -8
View File
@@ -21,9 +21,10 @@ import {
NodeSwitcherDialog,
useEnvironment,
useInitializeEnv,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import './styles.css';
import { useGlobalStore, usePageTitleStore } from '../stores';
import { usePageTitleStore } from '../stores';
import { Footer } from '../components/footer';
import DialogsContainer from './dialogs-container';
import ToastsManager from './toasts-manager';
@@ -80,7 +81,7 @@ function AppBody({ Component }: AppProps) {
const gridClasses = classNames(
'h-full relative z-0 grid',
'grid-rows-[repeat(3,min-content),1fr]'
'grid-rows-[repeat(3,min-content),minmax(0,1fr)]'
);
return (
@@ -92,7 +93,7 @@ function AppBody({ Component }: AppProps) {
<Title />
<div className={gridClasses}>
<AnnouncementBanner />
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'} />
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
<div data-testid="banners">
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
@@ -115,11 +116,10 @@ function AppBody({ Component }: AppProps) {
function VegaTradingApp(props: AppProps) {
const status = useEnvironment((store) => store.status);
const { nodeSwitcherOpen, setNodeSwitcher } = useGlobalStore((store) => ({
nodeSwitcherOpen: store.nodeSwitcherDialog,
setNodeSwitcher: (open: boolean) =>
store.update({ nodeSwitcherDialog: open }),
}));
const [nodeSwitcherOpen, setNodeSwitcher] = useNodeSwitcherStore((store) => [
store.dialogOpen,
store.setDialogOpen,
]);
useInitializeEnv();
+21
View File
@@ -127,3 +127,24 @@ html [data-theme='light'] {
.ag-theme-balham .ag-row.no-hover:hover {
background: var(--ag-background-color);
}
.virtualized-list {
/* Works on Firefox */
scrollbar-width: thin;
scrollbar-color: #999 #333;
}
/* Works on Chrome, Edge, and Safari */
.virtualized-list::-webkit-scrollbar {
width: 6px;
background-color: #999;
}
.virtualized-list::-webkit-scrollbar-thumb {
width: 6px;
background-color: #333;
}
.virtualized-list::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgb(0 0 0 / 30%);
background-color: #999;
}
-2
View File
@@ -3,7 +3,6 @@ import { create } from 'zustand';
import produce from 'immer';
interface GlobalStore {
nodeSwitcherDialog: boolean;
marketId: string | null;
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
shouldDisplayWelcomeDialog: boolean;
@@ -15,7 +14,6 @@ interface PageTitleStore {
}
export const useGlobalStore = create<GlobalStore>()((set) => ({
nodeSwitcherDialog: false,
marketId: LocalStorage.getItem('marketId') || null,
shouldDisplayWelcomeDialog: false,
update: (newState) => {

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