Compare commits

..
Author SHA1 Message Date
Bartłomiej Głownia ec12c7ecfd fix(trading): update cached Order objects (#3450) 2023-04-18 13:56:13 +01:00
m.ray 045dace274 feat(trading): add liquidity provision link to orders table (#3432) 2023-04-18 13:55:11 +01:00
Sam KeenandMatthew Russell 2346b2f1a0 feat(governance): full width Governance and Explorer header (#3453)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-04-18 13:06:30 +01:00
Bartłomiej Głownia 31859cb779 chore(trading): maintain always up to date active orders list (#3430) 2023-04-18 12:49:42 +02:00
dexturr 20507ecdd1 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-18 06:08:58 +00:00
dexturr 13ba661ea0 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-18 00:16:31 +00:00
dexturr 03f5b9fc7b chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-17 18:07:00 +00:00
dexturr b9fed65615 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-17 12:10:37 +00:00
Mikołaj Młodzikowski d671e51edf fix(ci): adjust timeouts 2023-04-17 12:45:30 +02:00
Mikołaj Młodzikowski 4f3295d3fc feat(ci): rebuild yarn on failed build 2023-04-17 11:24:53 +02:00
dexturr 7be7bab1ca chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-17 06:09:49 +00:00
dexturr 61380f06b9 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-17 00:14:49 +00:00
dexturr c113856d63 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-16 18:07:16 +00:00
dexturr 90884beee8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-16 12:07:18 +00:00
dexturr d9d2e428f4 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-16 06:09:55 +00:00
dexturr ba8d54b761 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-16 00:15:24 +00:00
dexturr dce4edb943 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-15 18:06:56 +00:00
dexturr b468a0fd84 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-15 12:07:02 +00:00
dexturr c98fbe7453 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-15 06:06:58 +00:00
dexturr d20928ec06 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-15 00:15:18 +00:00
dexturr 289559fe4f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-14 18:09:58 +00:00
Mikołaj Młodzikowski 071fd6128c feat: fix releasing dist 2023-04-14 15:45:14 +02:00
Mikołaj Młodzikowski 554d07a31b feat(ci): fix regressions detected after merging to develop (#3442) 2023-04-14 15:00:20 +02:00
dexturr 48b62380b0 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-14 12:10:09 +00:00
Mikołaj Młodzikowski a9f4604bad feat(governance,trading,explorer): pipelines improvements (#3422) 2023-04-14 11:47:41 +02:00
Sam Keen 54f2519a62 feat(governance): toggles design review tweaks (#3431) 2023-04-14 09:22:22 +01:00
dexturr 260e7bf9cf chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-14 06:09:41 +00:00
dexturr 83db796cdb chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-14 00:14:08 +00:00
m.ray 2f6800212e fix(trading): fix order toast intent on amendments (#3433) 2023-04-13 13:20:28 -07:00
dexturr 77e16ad1b7 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-13 18:09:10 +00:00
dexturr dcce983842 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-13 12:10:33 +00:00
Maciek 53ce9bfaa2 chore(assets): make query for asset list lighter (#3425) 2023-04-13 11:42:09 +00:00
Edd 6ae34d296f fix(explorer): tweak footer to show more detail on smaller screens (#3418) 2023-04-13 10:49:42 +01:00
Joe Tsang ee2aafb99c fix(governance): failing nightly tests (#3351) 2023-04-13 10:46:01 +01:00
dexturr e8fdb63323 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-13 06:09:41 +00:00
dexturr 3df9270b57 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-13 00:13:51 +00:00
dexturr ab26667265 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-12 18:08:22 +00:00
m.ray 9fc6b255c8 fix(orders): update order toast intent and title (#3424) 2023-04-12 15:19:15 +00:00
dexturr 91f61d8bb5 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-12 12:09:32 +00:00
Bartłomiej Głowniaandm.ray bbfda65bcc feat(withdraws): improve ux surrounding withdraw balances thresholds and delays (#3402)
Co-authored-by: m.ray <16125548+MadalinaRaicu@users.noreply.github.com>
2023-04-12 11:21:24 +02:00
Mikołaj Młodzikowski 45f2e926c3 feat(governance,trading,explorer): split steps by logic (#3387) 2023-04-12 09:38:26 +02:00
dexturr ccadbc2cb6 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-12 06:08:40 +00:00
dexturr 7128acb7b8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-12 00:14:18 +00:00
dexturr 357bdaa4f3 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-11 18:07:51 +00:00
Maciek f3fe43724e fix(deposits): deposit dialog doesn't respect given assetId (#3414) 2023-04-11 16:41:20 +02:00
m.ray 03f5ca3096 fix(trading): fix formatting on clicking on orderbook cell after decimal resolution update (#3409) 2023-04-11 12:23:43 +00:00
dexturr 03d19349af chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-11 12:08:24 +00:00
daro-maj 1f53b2cbb5 test(trading): increase coverage for the remaining ACs (#3412) 2023-04-11 11:59:21 +02:00
Maciek b010c98346 fix(trading): welcome dialog with markets list click on market close window (#3404) 2023-04-11 11:34:44 +02:00
Matthew Russell 29d57af2ba fix(trading): make light theme sell volume bar match candle color (#3405) 2023-04-11 09:06:35 +02:00
Bartłomiej Głownia fd7940c4cf chore(deal-ticket): remove not used code (#3406) 2023-04-11 08:13:41 +02:00
dexturr 8ab7ac8414 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-11 06:07:03 +00:00
dexturr ce7cd03c3e chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-11 00:15:30 +00:00
Matthew Russell dfa3355a68 chore(trading): prevent node poll if query fails (#3411) 2023-04-10 14:48:03 -07:00
dexturr 8553904d81 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-10 18:07:16 +00:00
dexturr 10afa3c1d4 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-10 12:10:31 +00:00
Maciek ecd362615e chore(deposits): impove UX on deposit dialog (#3401) 2023-04-10 12:59:02 +01:00
dexturr 0d9bd67465 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-10 06:07:18 +00:00
dexturr b5aa92f007 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-10 00:14:34 +00:00
dexturr 2955790be1 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-09 18:09:21 +00:00
dexturr 89ff9309e9 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-09 12:08:56 +00:00
dexturr e1df3489f6 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-09 06:09:36 +00:00
dexturr f0fa9ca5e1 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-09 00:17:04 +00:00
dexturr 9d92477c2a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-08 18:08:30 +00:00
dexturr b1a198be85 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-08 12:09:49 +00:00
dexturr 5fb385d43c chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-08 06:07:03 +00:00
dexturr 8014b279fe chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-08 00:14:19 +00:00
dexturr f4bde57294 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-07 18:09:32 +00:00
John Walley 2e5ff07824 chore(candles-chart): upgrade pennant to v1.8.1 (#3399) 2023-04-07 10:35:10 -07:00
Bartłomiej Głownia 7accb42793 fix(market-depth): make market data up to date, use fixed number of decimals in cumulative vol (#3361) 2023-04-07 10:34:51 -07:00
Matthew Russell 9229e7f686 chore(trading): update approval amount to be max uint256 (#3395) 2023-04-07 10:33:53 -07:00
daro-maj b570b9c4c1 test(trading): tests for post and reduce functionality (#3400) 2023-04-07 13:03:15 -04:00
m.ray ed78261aad fix(trading): order tx toasts title and intent should be set by status (#3368) 2023-04-07 13:01:12 -04:00
m.ray 363ca9c6e1 fix(trading): add liquidity provision docs (#3403) 2023-04-07 13:00:57 -04:00
m.ray bafd3c384c feat(trading): include fees in transfer amount (#3345) 2023-04-07 13:00:11 -04:00
dexturr 3b830abe07 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-07 12:09:01 +00:00
dexturr 1787e22b69 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-07 06:07:23 +00:00
dexturr f105133959 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-07 00:14:21 +00:00
dexturr aff0e46d23 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-06 18:08:45 +00:00
Maciek 7e4aafcb77 chore(trading): add filters to the proposals table (#3375) 2023-04-06 15:30:21 +02:00
dexturr 643d5408cd chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-06 12:08:03 +00:00
m.ray 2a2af3ba9c fix(trading): fix docs resources (#3382) 2023-04-06 12:53:08 +01:00
Sam Keen 8d4e4a1228 fix(governance): fix incorrect validator penalties (#3381) 2023-04-06 13:31:21 +02:00
dexturr 84068c8081 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-06 06:09:30 +00:00
dexturr e1185b9a96 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-06 00:13:22 +00:00
dexturr f390448c24 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-05 18:09:37 +00:00
Maciek 70943c523c chore(deposits): persistence of deposit form state (#3348) 2023-04-05 14:53:55 +02:00
dexturr dcb79e70d3 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-05 12:10:10 +00:00
128 changed files with 5017 additions and 3635 deletions
+85 -5
View File
@@ -4,14 +4,54 @@ on:
push:
branches:
- release/*
- develop
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
- reopened
- edited
- synchronize
jobs:
node-modules:
runs-on: ubuntu-22.04
name: 'Cache yarn modules'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Cache node modules
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
# comment out "resotre-keys" if you need to rebuild yarn from 0
restore-keys: |
${{ runner.os }}-cache-node-modules-
- name: Setup node
uses: actions/setup-node@v3
if: steps.cache.outputs.cache-hit != 'true'
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: yarn install
if: steps.cache.outputs.cache-hit != 'true'
run: yarn install --pure-lockfile
lint-pr-title:
needs: node-modules
if: ${{ github.event_name == 'pull_request' }}
name: Verify PR title
uses: ./.github/workflows/lint-pr.yml
secrets: inherit
lint-test-build:
timeout-minutes: 20
needs: node-modules
runs-on: ubuntu-22.04
name: '(CI) lint + unit test + build'
steps:
@@ -27,8 +67,11 @@ jobs:
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: Install root dependencies
run: yarn install --frozen-lockfile
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v3
@@ -48,7 +91,7 @@ jobs:
run: yarn nx affected:test
- name: Build affected
run: yarn nx affected:build
run: yarn nx affected:build || (yarn install && yarn nx affected:build)
# See affected apps
- name: See affected apps
@@ -95,6 +138,43 @@ jobs:
with:
projects: ${{ needs.lint-test-build.outputs.projects }}
dist-check:
runs-on: ubuntu-latest
needs: publish-dist
if: ${{ github.event_name == 'pull_request' }}
name: '(CD) comment preview links'
steps:
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: Previews
- name: Inject slug/short variables
if: ${{ steps.fc.outputs.comment-id == 0 }}
uses: rlespinasse/github-slug-action@v4
with:
prefix: CI_
- name: Create comment
if: ${{ steps.fc.outputs.comment-id == 0 }}
uses: peter-evans/create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
Previews
- explorer https://explorer.${{ env.CI_GITHUB_REF_NAME }}.vega.rocks
- trading https://trading.${{ env.CI_GITHUB_REF_NAME }}.vega.rocks
- governance https://governance.${{ env.CI_GITHUB_REF_NAME }}.vega.rocks
cypress-check:
name: '(CI) cypress - check'
runs-on: ubuntu-latest
needs: cypress
steps:
- run: echo Done!
# Report single result at the end, to avoid mess with required checks in PR
cypress-result:
if: ${{ always() }}
@@ -2,15 +2,11 @@
name: Verify PR title
on:
pull_request:
types:
- opened
- ready_for_review
- reopened
- edited
- synchronize
workflow_call:
jobs:
lint_pr:
timeout-minutes: 10
runs-on: ubuntu-22.04
steps:
- name: Checkout
@@ -23,8 +19,11 @@ jobs:
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: Install root dependencies
run: yarn install --frozen-lockfile
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
- name: Check PR title
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
+92 -29
View File
@@ -15,6 +15,7 @@ jobs:
app: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.app }}
runs-on: ubuntu-22.04
timeout-minutes: 20
steps:
- name: Check out code
uses: actions/checkout@v3
@@ -36,66 +37,131 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# https://docs.github.com/en/actions/learn-github-actions/contexts
- name: Check node version
id: tags
run: |
nodeVersion=$(cat .nvmrc | head -n 1)
echo ::set-output name=nodeVersion::${nodeVersion}
- name: Setup node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
# https://docs.github.com/en/actions/learn-github-actions/contexts
- name: Define variables
run: |
envName=''
dockerfile="dist.Dockerfile"
if [[ "${{ github.event_name }}" = "push" ]]; then
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
bucketName="${{ github.event.repository.name }}-$envName"
echo ::set-output name=bucketName::${bucketName}
echo ::set-output name=envName::${envName}
domain="vega.rocks"
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
if [[ "${{ github.ref }}" =~ .*mainnet.* ]]; then
domain="vega.community"
if [[ "${{ matrix.app }}" = "trading" ]]; then
dockerfile="ipfs.Dockerfile"
fi
fi
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
envName="stagnet3"
fi
bucketName="${{ matrix.app }}.${envName}.${domain}"
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
fi
nodeVersion=$(cat .nvmrc | head -n 1)
echo ENV_NAME=${envName} >> $GITHUB_ENV
echo NODE_VERSION=${nodeVersion} >> $GITHUB_ENV
echo DOCKERFILE=docker/${dockerfile} >> $GITHUB_ENV
- name: Build local dist
if: ${{ env.DOCKERFILE != 'docker/ipfs.Dockerfile' }}
run: |
flags=""
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
if [[ "${{ env.ENV_NAME }}" != "ops-vega" ]]; then
flags="--env=${{ env.ENV_NAME }}"
fi
fi
if [ "${{ matrix.app }}" = "trading" ]; then
yarn nx export trading $flags || (yarn install && yarn nx export trading $flags)
DIST_LOCATION=dist/apps/trading/exported
else
yarn nx build ${{ matrix.app }} $flags || (yarn install && yarn nx build ${{ matrix.app }} $flags)
DIST_LOCATION=dist/apps/${{ matrix.app }}
fi
mv $DIST_LOCATION dist-result
tree dist-result
- name: Build and export to local Docker
id: docker_build
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
uses: docker/build-push-action@v3
with:
context: .
file: ${{ env.DOCKERFILE }}
load: true
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.nodeVersion }}
ENV_NAME=${{ steps.tags.outputs.envName || '' }}
NODE_VERSION=${{ env.NODE_VERSION }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
- name: Image digest
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
run: echo ${{ steps.docker_build.outputs.digest }}
- name: Sanity check docker image
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
run: |
echo "Check ipfs-hash"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat ipfs-hash
if [[ "${{ env.DOCKERFILE }}" = "docker/ipfs.Dockerfile" ]]; then
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
fi
echo "List html directory"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local ls -lah
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree .'
echo "Copy dist to local filesystem"
- name: Copy dist to local filesystem
if: ${{ env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' }}
run: |
docker create --name=dist ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
docker cp dist:/usr/share/nginx/html dist
echo "Check local dist"
ls -al dist
echo "check local dist files"
tree dist/html
mv dist/html dist-result
- name: Publish dist as docker image
uses: docker/build-push-action@v3
if: ${{ github.event_name == 'pull_request' }}
with:
context: .
file: ${{ env.DOCKERFILE }}
push: true
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.nodeVersion }}
NODE_VERSION=${{ env.NODE_VERSION }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
# - uses: shallwefootball/s3-upload-action@master
# if: ${{ github.event_name == 'push' }}
# name: Upload dist S3
# with:
# aws_key_id: ${{ secrets.AWS_KEY_ID }}
# aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY}}
# aws_bucket: ${{ steps.tags.outputs.bucketName }}
# source_dir: 'dist'
# bucket creation in github.com/vegaprotocol/terraform//frontend
- name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master
if: ${{ github.event_name == 'push' }}
with:
args: --acl private --follow-symlinks --delete
env:
AWS_S3_BUCKET: ${{ env.BUCKET_NAME }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: 'eu-west-1'
SOURCE_DIR: 'dist-result'
- name: Add preview label
uses: actions-ecosystem/action-add-labels@v1
@@ -103,6 +169,3 @@ jobs:
with:
labels: ${{ matrix.app }}-preview
number: ${{ github.event.number }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
@@ -10,7 +10,7 @@ export const Footer = () => {
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
const { screenSize } = useScreenDimensions();
const showFullFeedbackLabel = useMemo(
() => ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
() => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
[screenSize]
);
+6 -4
View File
@@ -38,14 +38,16 @@ const DialogsContainer = () => {
export const Layout = () => {
const isHome = Boolean(useMatch(Routes.HOME));
const { ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
const fixedWidthClasses = 'w-full max-w-[1500px] mx-auto';
return (
<>
<div
className={classNames(
'max-w-[1500px] min-h-[100vh]',
'min-h-screen',
'mx-auto my-0',
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
'border-vega-light-200 dark:border-vega-dark-200 lg:border-l lg:border-r',
'border-vega-light-200 dark:border-vega-dark-200',
'antialiased text-black dark:text-white',
'overflow-hidden relative'
)}
@@ -59,13 +61,13 @@ export const Layout = () => {
)}
<Header />
</div>
<div>
<div className={fixedWidthClasses}>
<main className="p-4">
{!isHome && <BreadcrumbsContainer className="mb-4" />}
<Outlet />
</main>
</div>
<div>
<div className={fixedWidthClasses}>
<Footer />
</div>
</div>
@@ -44,7 +44,8 @@ describe(
function () {
before('connect wallets and set approval limit', function () {
cy.visit('/');
vegaWalletSetSpecifiedApprovalAmount('1000');
ethereumWalletConnect();
cy.associateTokensToVegaWallet('1');
});
beforeEach('visit proposals tab', function () {
@@ -213,6 +214,7 @@ describe(
// 3001-VOTE-042, 3001-VOTE-057, 3001-VOTE-058, 3001-VOTE-059, 3001-VOTE-060
it('Newly created proposal details - ability to increase associated tokens - by voting again after association', function () {
vegaWalletSetSpecifiedApprovalAmount('1000');
createRawProposal();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getSubmittedProposalFromProposalList(rawProposal.rationale.title)
@@ -13,7 +13,6 @@ import {
createUpdateNetworkProposalTxBody,
createFreeFormProposalTxBody,
} from '../../support/proposal.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
@@ -33,10 +32,6 @@ context(
before('Connect wallets and set approval', function () {
cy.visit('/');
vegaWalletSetSpecifiedApprovalAmount('1000');
cy.connectVegaWallet();
ethereumWalletConnect();
ensureSpecifiedUnstakedTokensAreAssociated('1');
cy.clearLocalStorage();
});
beforeEach('visit proposals', function () {
@@ -114,7 +109,7 @@ context(
navigateTo(navigation.proposals);
cy.reload();
waitForSpinner();
cy.get(openProposals).within(() => {
cy.get(openProposals, { timeout: 6000 }).within(() => {
cy.contains(proposalTitle)
.parentsUntil('[data-testid="proposals-list-item"]')
.within(() => cy.get(viewProposalButton).click());
@@ -232,7 +232,7 @@ context(
it('Unable to create a freeform proposal - when json parent section contains unexpected field', function () {
const errorMsg =
'Invalid params: the transaction does not use a valid Vega command: unknown field unexpected" in vega.commands.v1.ProposalSubmission';
'Invalid params: the transaction does not use a valid Vega command: unknown field "unexpected" in vega.commands.v1.ProposalSubmission';
// 3001-VOTE-038 3002-PROP-013 3002-PROP-014
goToMakeNewProposal(governanceProposalType.RAW);
@@ -313,7 +313,7 @@ context(
it('Unable to vote on a proposal - when vega wallet disconnected - option to connect from within', function () {
createRawProposal();
cy.get('[data-testid="manage-vega-wallet"]').click();
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="disconnect"]').click();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getSubmittedProposalFromProposalList(
@@ -218,7 +218,7 @@ context(
it('Unable to submit new market proposal with missing/invalid fields', function () {
const errorMsg =
'Invalid params: the transaction is not a valid Vega command: unknown field "filters" in vega.DataSourceDefinition';
'Invalid params: the transaction does not use a valid Vega command: unknown field "invalid" in vega.NewMarket';
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.get(newProposalSubmitButton).should('be.visible').click();
@@ -436,7 +436,7 @@ context(
});
});
it.only('Able to submit update asset proposal using max deadline', function () {
it('Able to submit update asset proposal using max deadline', function () {
goToMakeNewProposal(governanceProposalType.UPDATE_ASSET);
enterUpdateAssetProposalDetails();
cy.get(maxVoteDeadline).click();
@@ -25,11 +25,11 @@ const vegaWalletUnstakedBalance =
'[data-testid="vega-wallet-balance-unstaked"]';
const txTimeout = Cypress.env('txTimeout');
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
const ethWalletAssociateButton = '[data-testid="associate-btn"]';
const ethWalletAssociateButton = '[data-testid="associate-btn"]:visible';
const associateWalletRadioButton = '[data-testid="associate-radio-wallet"]';
const tokenAmountInputBox = '[data-testid="token-amount-input"]';
const tokenSubmitButton = '[data-testid="token-input-submit-button"]';
const ethWalletDissociateButton = '[href="/token/disassociate"]';
const ethWalletDissociateButton = '[href="/token/disassociate"]:visible';
const vestingContractSection = '[data-testid="vega-in-vesting-contract"]';
const vegaInWalletSection = '[data-testid="vega-in-wallet"]';
const connectedVegaKey = '[data-testid="connected-vega-key"]';
@@ -78,12 +78,12 @@ context(
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
6
);
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
// 0005-ETXN-002
verifyEthWalletAssociatedBalance('2.0');
@@ -111,12 +111,12 @@ context(
stakingPageDisassociateTokens('2');
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
6
);
validateWalletCurrency('Associated', '2.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '0.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
cy.getByTestId('eth-wallet-associated-balances', txTimeout).should(
'not.exist'
);
@@ -192,12 +192,12 @@ context(
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
6
);
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
verifyEthWalletAssociatedBalance('2.0');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet).within(() => {
@@ -210,12 +210,12 @@ context(
});
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
6
);
validateWalletCurrency('Associated', '2.00');
validateWalletCurrency('Pending association', '1.00');
validateWalletCurrency('Total associated after pending', '1.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
verifyEthWalletAssociatedBalance('1.0');
verifyEthWalletTotalAssociatedBalance('1.0');
});
@@ -266,7 +266,7 @@ context(
// 1004-ASSO-008
// 1004-ASSO-010
// No warning visible as described in AC, but the button is disabled
cy.get(ethWalletAssociateButton).first().click();
cy.get(ethWalletAssociateButton).click();
cy.get(associateWalletRadioButton, { timeout: 30000 }).click();
cy.get(tokenSubmitButton, txTimeout).should('be.disabled'); // button disabled with no input
cy.get(tokenAmountInputBox, { timeout: 10000 }).type('6500000');
@@ -278,12 +278,12 @@ context(
vegaWalletAssociate('2');
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
6
);
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
validateWalletCurrency('Associated', '2.00');
});
@@ -294,24 +294,24 @@ context(
});
cy.getByTestId('currency-title', txTimeout).should(
'have.length.above',
3
6
);
validateWalletCurrency('Associated', '2.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '0.00');
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
validateWalletCurrency('Associated', '0.00');
});
it('Able to associate tokens to different public key of connected vega wallet', function () {
cy.get(ethWalletAssociateButton).first().click();
cy.get(ethWalletAssociateButton).click();
cy.get(associateWalletRadioButton).click();
cy.get(connectedVegaKey).should(
'have.text',
Cypress.env('vegaWalletPublicKey')
);
cy.get('[data-testid="manage-vega-wallet"]').click();
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
cy.get(connectedVegaKey).should(
'have.text',
@@ -166,6 +166,7 @@ export function goToMakeNewProposal(proposalType: string) {
navigateTo(navigation.proposals);
cy.get(newProposalButton).should('be.visible').click();
cy.url().should('include', '/proposals/propose');
cy.get(navigation.pageSpinner, { timeout: 20000 }).should('not.exist');
cy.get('li').should('contain.text', proposalType).and('be.visible');
cy.get('li').contains(proposalType).click();
}
@@ -8,6 +8,7 @@ import {
} from '@vegaprotocol/smart-contracts';
import { ethers, Wallet } from 'ethers';
const associatedAmountInWallet = '[data-testid="associated-amount"]:visible';
const vegaWalletContainer = 'aside [data-testid="vega-wallet"]';
const vegaWalletMnemonic = Cypress.env('vegaWalletMnemonic');
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey');
@@ -59,7 +60,7 @@ export async function faucetAsset(assetEthAddress: string) {
}
export async function vegaWalletTeardown() {
cy.get('[data-testid="associated-amount"]')
cy.get(associatedAmountInWallet)
.should('be.visible')
.invoke('text')
.then((associatedAmount) => {
@@ -68,12 +69,12 @@ export async function vegaWalletTeardown() {
$body.find('[data-testid="eth-wallet-associated-balances"]').length ||
associatedAmount != '0.00'
) {
vegaWalletTeardownVesting(vestingContract);
vegaWalletTeardownStaking(stakingBridgeContract);
vegaWalletTeardownVesting(vestingContract);
}
});
cy.get(vegaWalletContainer).within(() => {
cy.getByTestId('associated-amount', {
cy.get(associatedAmountInWallet, {
timeout: transactionTimeout,
}).contains('0.00', {
timeout: transactionTimeout,
@@ -90,7 +91,7 @@ export async function vegaWalletSetSpecifiedApprovalAmount(
await promiseWithTimeout(
token.approve(
ethStakingBridgeContractAddress,
resetAmount.concat('000000000000000000')
resetAmount + '0'.repeat(18)
),
10 * 60 * 1000,
'set approval amount'
@@ -104,12 +105,23 @@ async function vegaWalletTeardownStaking(stakingBridgeContract: StakingBridge) {
{ timeout: transactionTimeout, log: false }
).then((stakeBalance) => {
if (Number(stakeBalance) != 0) {
cy.wrap(
stakingBridgeContract.remove_stake(
String(stakeBalance),
vegaWalletPubKey
),
{ timeout: transactionTimeout, log: false }
cy.get('[data-testid="vega-wallet-balance-unstaked"]:visible').within(
() => {
cy.get(associatedAmountInWallet)
.invoke('text')
.then(($walletAmount) => {
cy.wrap(
stakingBridgeContract.remove_stake(
String(stakeBalance),
vegaWalletPubKey
),
{ timeout: transactionTimeout, log: false }
);
cy.get(associatedAmountInWallet, {
timeout: transactionTimeout,
}).should('not.have.text', $walletAmount);
});
}
);
}
});
@@ -124,7 +136,6 @@ async function vegaWalletTeardownVesting(vestingContract: TokenVesting) {
if (Number(vestingAmount) != 0) {
// Wait needed to allow time for ganache to process tx for stakingBridgeContract.remove_stake
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
cy.wrap(
vestingContract.remove_stake(String(vestingAmount), vegaWalletPubKey),
{ timeout: transactionTimeout, log: false }
@@ -1,15 +1,19 @@
import classNames from 'classnames';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { ReactNode } from 'react';
import { AnnouncementBanner } from '@vegaprotocol/announcements';
import { Nav } from '../nav';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import React from 'react';
interface AppLayoutProps {
children: ReactNode;
}
export const AppLayout = ({ children }: AppLayoutProps) => {
const { VEGA_ENV, ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
const { isReadOnly } = useVegaWallet();
const AppLayoutClasses = classNames(
'app w-full max-w-[1500px] mx-auto grid min-h-full',
'border-neutral-700 lg:border-l lg:border-r',
'app w-full max-w-[1500px] mx-auto grid',
'lg:text-body-large',
{
'grid-rows-[repeat(2,min-content)_1fr_min-content]': !isReadOnly,
@@ -17,5 +21,18 @@ export const AppLayout = ({ children }: AppLayoutProps) => {
}
);
return <div className={AppLayoutClasses}>{children}</div>;
return (
<div className="min-h-full">
<div className="lg:text-body-large">
{ANNOUNCEMENTS_CONFIG_URL && (
<AnnouncementBanner
app="governance"
configUrl={ANNOUNCEMENTS_CONFIG_URL}
/>
)}
<Nav theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
</div>
<div className={AppLayoutClasses}>{children}</div>
</div>
);
};
@@ -1,28 +1,16 @@
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
import { AnnouncementBanner } from '@vegaprotocol/announcements';
import { useVegaWallet } from '@vegaprotocol/wallet';
import React from 'react';
import { Nav } from '../nav';
export interface TemplateSidebarProps {
children: React.ReactNode;
sidebar: React.ReactNode[];
}
export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
const { VEGA_ENV, ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
return (
<>
{ANNOUNCEMENTS_CONFIG_URL && (
<AnnouncementBanner
app="governance"
configUrl={ANNOUNCEMENTS_CONFIG_URL}
/>
)}
<Nav theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
{isReadOnly ? (
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
) : null}
@@ -443,8 +443,8 @@
"rewardType": "Reward type",
"rewardsAndFeesReceived": "Rewards and fees received",
"ThisDoesNotIncludeFeesReceivedForMakersOrLiquidityProviders": "This does not include fees received for makers or liquidity providers",
"totalDistributed": "TOTAL DISTRIBUTED",
"earnedByMe": "EARNED BY ME",
"totalDistributed": "Total distributed",
"earnedByMe": "Earned by me",
"noRewardsHaveBeenDistributedYet": "NO REWARDS HAVE BEEN DISTRIBUTED YET",
"rewardsColAssetHeader": "ASSET",
"rewardsColStakingHeader": "STAKING",
@@ -114,7 +114,7 @@ export const RewardsPage = () => {
</p>
</div>
<div className="w-[440px]">
<div className="w-[360px]">
<Toggle
name="epoch-reward-view-toggle"
toggles={[
@@ -155,16 +155,16 @@ export const ValidatorTables = ({
return (
<section data-testid="validator-tables">
<div className="grid w-full justify-end">
<div className="w-[400px]">
<div className="w-[340px]">
<Toggle
name="validators-view-toggle"
toggles={[
{
label: t('ALL VALIDATORS'),
label: t('All validators'),
value: 'all',
},
{
label: t('STAKED BY ME'),
label: t('Staked by me'),
value: 'myStake',
},
]}
File diff suppressed because it is too large Load Diff
@@ -411,6 +411,7 @@ describe('capsule', { tags: '@slow' }, () => {
it('approved amount is less than deposit', function () {
// 1001-DEPO-006
// 1001-DEPO-007
cy.getByTestId(depositsTab).click();
cy.getByTestId('deposit-button').click();
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
@@ -430,8 +431,8 @@ describe('capsule', { tags: '@slow' }, () => {
});
it('withdraw - delay verification', function () {
// 1001-DEPO-007
// 1001-DEPO-024
// 1002-WITH-007
cy.visit('/#/portfolio');
cy.get('main[data-testid="/portfolio"]').should('exist');
@@ -76,6 +76,7 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
});
it('insufficient funds', () => {
// 1001-DEPO-004
mockWeb3DepositCalls({
allowance: '1000',
depositLifetimeLimit: '1000',
@@ -87,7 +88,10 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
.clear()
.type('850')
.next(`[data-testid="${formFieldError}"]`)
.should('have.text', 'Insufficient amount in Ethereum wallet');
.should(
'have.text',
"You can't deposit more than you have in your Ethereum wallet, 800 tEURO"
);
});
});
+81 -1
View File
@@ -1,5 +1,5 @@
import * as Schema from '@vegaprotocol/types';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { aliasGQLQuery, checkSorting } from '@vegaprotocol/cypress';
import { marketsQuery } from '@vegaprotocol/mock';
import { getDateTimeFormat } from '@vegaprotocol/utils';
@@ -85,6 +85,7 @@ describe('markets table', { tags: '@smoke' }, () => {
cy.getByTestId('view-market-list-link')
.should('have.attr', 'href', '#/markets/all')
.click();
cy.get('[data-testid="All markets"]').should(
'have.attr',
'data-state',
@@ -117,6 +118,85 @@ describe('markets table', { tags: '@smoke' }, () => {
`${Cypress.env('VEGA_TOKEN_URL')}/proposals/propose/new-market`
);
});
it('proposed markets tab should be sorted properly', () => {
cy.getByTestId('view-market-list-link').click();
cy.get('[data-testid="Proposed markets"]').click();
const marketColDefault = [
'ETHUSD',
'LINKUSD',
'ETHUSD',
'ETHDAI.MF21',
'AAPL.MF21',
'BTCUSD.MF21',
'TSLA.QM21',
'AAVEDAI.MF21',
'ETHBTC.QM21',
'UNIDAI.MF21',
];
const marketColAsc = [
'AAPL.MF21',
'AAVEDAI.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'ETHDAI.MF21',
'ETHUSD',
'ETHUSD',
'LINKUSD',
'TSLA.QM21',
'UNIDAI.MF21',
];
const marketColDesc = [
'UNIDAI.MF21',
'TSLA.QM21',
'LINKUSD',
'ETHUSD',
'ETHUSD',
'ETHDAI.MF21',
'ETHBTC.QM21',
'BTCUSD.MF21',
'AAVEDAI.MF21',
'AAPL.MF21',
];
checkSorting('market', marketColDefault, marketColAsc, marketColDesc);
const stateColDefault = [
'Open',
'Passed',
'Waiting for Node Vote',
'Open',
'Passed',
'Open',
'Passed',
'Open',
'Waiting for Node Vote',
'Open',
];
const stateColAsc = [
'Open',
'Open',
'Open',
'Open',
'Open',
'Passed',
'Passed',
'Passed',
'Waiting for Node Vote',
'Waiting for Node Vote',
];
const stateColDesc = [
'Waiting for Node Vote',
'Waiting for Node Vote',
'Passed',
'Passed',
'Passed',
'Open',
'Open',
'Open',
'Open',
'Open',
];
checkSorting('state', stateColDefault, stateColAsc, stateColDesc);
});
it('opening auction subsets should be properly displayed', () => {
cy.mockTradingPage(
@@ -41,6 +41,12 @@ describe('accounts', { tags: '@smoke' }, () => {
.should('have.text', '100,001.01');
});
it('asset detail should be properly rendered', () => {
cy.getByTestId('Collateral').click();
cy.getByTestId('asset').contains('tEURO').click();
cy.get('[data-testid$="_label"]').should('have.length', 16);
});
describe('sorting by ag-grid columns should work well', () => {
it('sorting by asset', () => {
cy.getByTestId('Collateral').click();
@@ -1,5 +1,5 @@
import * as Schema from '@vegaprotocol/types';
import { aliasGQLQuery, mockConnectWallet } from '@vegaprotocol/cypress';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { testOrderSubmission } from '../support/order-validation';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import {
@@ -13,8 +13,6 @@ const orderSizeField = 'order-size';
const orderPriceField = 'order-price';
const orderTIFDropDown = 'order-tif';
const placeOrderBtn = 'place-order';
const toggleShort = 'order-side-SIDE_SELL';
const toggleLong = 'order-side-SIDE_BUY';
const toggleLimit = 'order-type-TYPE_LIMIT';
const toggleMarket = 'order-type-TYPE_MARKET';
@@ -32,34 +30,6 @@ const displayTomorrow = () => {
return tomorrow.toISOString().substring(0, 16);
};
describe('time in force default values', () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must have market order set up to IOC by default', function () {
// 7002-SORD-031
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'IOC')[0].text
);
});
it('must have time in force set to GTC for limit order', function () {
// 7002-SORD-031
cy.getByTestId(toggleLimit).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'GTC')[0].text
);
});
});
describe('must submit order', { tags: '@smoke' }, () => {
// 7002-SORD-039
before(() => {
@@ -393,227 +363,6 @@ describe(
}
);
describe('deal ticket validation', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must show place order button and connect wallet if wallet is not connected', () => {
// 0003-WTXN-001
cy.getByTestId('connect-vega-wallet'); // Not connected
cy.getByTestId('order-connect-wallet').should('exist');
cy.getByTestId(placeOrderBtn).should('exist');
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
});
it('must be able to select order direction - long/short', function () {
// 7002-SORD-004
cy.getByTestId(toggleShort).click().children('input').should('be.checked');
cy.getByTestId(toggleLong).click().children('input').should('be.checked');
});
it('must be able to select order type - limit/market', function () {
// 7002-SORD-005
// 7002-SORD-006
// 7002-SORD-007
cy.getByTestId(toggleLimit).click().children('input').should('be.checked');
cy.getByTestId(toggleMarket).click().children('input').should('be.checked');
});
it('order connect vega wallet button should connect', () => {
mockConnectWallet();
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('101');
cy.getByTestId('order-connect-wallet').click();
cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.wait('@walletReq');
cy.getByTestId(placeOrderBtn).should('be.visible');
cy.getByTestId(toggleLimit).children('input').should('be.checked');
cy.getByTestId(orderPriceField).should('have.value', '101');
});
});
describe('deal ticket size validation', { tags: '@smoke' }, function () {
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must warn if order size input has too many digits after the decimal place', function () {
// 7002-SORD-016
cy.getByTestId('order-type-TYPE_MARKET').click();
cy.getByTestId(orderSizeField).clear().type('1.234');
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size must be whole numbers for this market'
);
});
it('must warn if order size is set to 0', function () {
cy.getByTestId('order-type-TYPE_MARKET').click();
cy.getByTestId(orderSizeField).clear().type('0');
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size cannot be lower than 1'
);
});
});
describe('limit order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleLimit).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
it('must see the price unit', function () {
// 7002-SORD-018
cy.getByTestId(orderPriceField)
.siblings('label')
.should('have.text', 'Price (DAI)');
});
it('must see warning when placing an order with expiry date in past', () => {
const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000);
const expiresAtInputValue = expiresAt.toISOString().substring(0, 16);
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
cy.log('choosing yesterday');
cy.getByTestId('date-picker-field').type(expiresAtInputValue);
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('dealticket-error-message-expiry').should(
'have.text',
'The expiry date that you have entered appears to be in the past'
);
});
it('must see warning if price has too many digits after decimal place', function () {
// 7002-SORD-059
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId('dealticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
);
});
describe('time in force validations', function () {
const validTIF = TIFlist;
validTIF.forEach((tif) => {
// 7002-SORD-023
// 7002-SORD-024
// 7002-SORD-025
// 7002-SORD-026
// 7002-SORD-027
// 7002-SORD-028
it(`must be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
});
});
it('selections should be remembered', () => {
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'IOC')[0].text
);
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_FOK');
cy.getByTestId(toggleLimit).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'GTT')[0].text
);
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'FOK')[0].text
);
});
});
});
describe('market order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleMarket).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
it('must not see the price unit', function () {
// 7002-SORD-019
cy.getByTestId(orderPriceField).should('not.exist');
});
describe('time in force validations', function () {
const validTIF = TIFlist.filter((tif) => ['FOK', 'IOC'].includes(tif.code));
const invalidTIF = TIFlist.filter(
(tif) => !['FOK', 'IOC'].includes(tif.code)
);
validTIF.forEach((tif) => {
// 7002-SORD-025
// 7002-SORD-026
it(`must be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
});
});
invalidTIF.forEach((tif) => {
// 7002-SORD-023
// 7002-SORD-024
// 7002-SORD-027
// 7002-SORD-028
it(`must not be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).should('not.contain', tif.text);
});
});
});
});
describe('suspended market validation', { tags: '@regression' }, () => {
before(() => {
cy.setVegaWallet();
@@ -765,6 +514,8 @@ describe('account validation', { tags: '@regression' }, () => {
it('must show error returned by wallet ', () => {
// 0003-WTXN-009
// 0003-WTXN-011
// 0002-WCON-016
// 0003-WTXN-008
//trigger error from the wallet
cy.intercept('POST', 'http://localhost:1789/api/v2/requests', (req) => {
@@ -788,6 +539,8 @@ describe('account validation', { tags: '@regression' }, () => {
'contain.text',
'The connection to your Vega Wallet has been lost.'
);
cy.getByTestId('connect-vega-wallet').click();
cy.getByTestId('dialog-content').should('be.visible');
});
it('must see that the order was rejected by the connected wallet', () => {
@@ -0,0 +1,329 @@
import * as Schema from '@vegaprotocol/types';
import { mockConnectWallet } from '@vegaprotocol/cypress';
const orderSizeField = 'order-size';
const orderPriceField = 'order-price';
const orderTIFDropDown = 'order-tif';
const placeOrderBtn = 'place-order';
const toggleShort = 'order-side-SIDE_SELL';
const toggleLong = 'order-side-SIDE_BUY';
const toggleLimit = 'order-type-TYPE_LIMIT';
const toggleMarket = 'order-type-TYPE_MARKET';
const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
return {
code: Schema.OrderTimeInForceCode[value],
value,
text: Schema.OrderTimeInForceMapping[value],
};
});
describe('time in force default values', () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must have market order set up to IOC by default', function () {
// 7002-SORD-030
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'IOC')[0].text
);
});
it('must have time in force set to GTC for limit order', function () {
// 7002-SORD-031
cy.getByTestId(toggleLimit).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'GTC')[0].text
);
});
});
describe('deal ticket validation', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must show place order button and connect wallet if wallet is not connected', () => {
// 0003-WTXN-001
cy.getByTestId('connect-vega-wallet'); // Not connected
cy.getByTestId('order-connect-wallet').should('exist');
cy.getByTestId(placeOrderBtn).should('exist');
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
});
it('must be able to select order direction - long/short', function () {
// 7002-SORD-004
cy.getByTestId(toggleShort).click().children('input').should('be.checked');
cy.getByTestId(toggleLong).click().children('input').should('be.checked');
});
it('must be able to select order type - limit/market', function () {
// 7002-SORD-005
// 7002-SORD-006
// 7002-SORD-007
cy.getByTestId(toggleLimit).click().children('input').should('be.checked');
cy.getByTestId(toggleMarket).click().children('input').should('be.checked');
});
it('order connect vega wallet button should connect', () => {
mockConnectWallet();
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('101');
cy.getByTestId('order-connect-wallet').click();
cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.wait('@walletReq');
cy.getByTestId(placeOrderBtn).should('be.visible');
cy.getByTestId(toggleLimit).children('input').should('be.checked');
cy.getByTestId(orderPriceField).should('have.value', '101');
});
});
describe('deal ticket size validation', { tags: '@smoke' }, function () {
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
it('must warn if order size input has too many digits after the decimal place', function () {
// 7002-SORD-016
cy.getByTestId('order-type-TYPE_MARKET').click();
cy.getByTestId(orderSizeField).clear().type('1.234');
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size must be whole numbers for this market'
);
});
it('must warn if order size is set to 0', function () {
cy.getByTestId('order-type-TYPE_MARKET').click();
cy.getByTestId(orderSizeField).clear().type('0');
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size cannot be lower than 1'
);
});
});
describe('limit order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleLimit).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
it('must see the price unit', function () {
// 7002-SORD-018
cy.getByTestId(orderPriceField)
.siblings('label')
.should('have.text', 'Price (DAI)');
});
it('must see warning when placing an order with expiry date in past', () => {
const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000);
const expiresAtInputValue = expiresAt.toISOString().substring(0, 16);
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
cy.log('choosing yesterday');
cy.getByTestId('date-picker-field').type(expiresAtInputValue);
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('dealticket-error-message-expiry').should(
'have.text',
'The expiry date that you have entered appears to be in the past'
);
});
it('must see warning if price has too many digits after decimal place', function () {
// 7002-SORD-059
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId('dealticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
);
});
describe('time in force validations', function () {
const validTIF = TIFlist;
validTIF.forEach((tif) => {
// 7002-SORD-023
// 7002-SORD-024
// 7002-SORD-025
// 7002-SORD-026
// 7002-SORD-027
// 7002-SORD-028
it(`must be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
});
});
it('selections should be remembered', () => {
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'IOC')[0].text
);
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_FOK');
cy.getByTestId(toggleLimit).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'GTT')[0].text
);
cy.getByTestId(toggleMarket).click();
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
TIFlist.filter((item) => item.code === 'FOK')[0].text
);
});
});
});
describe('market order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleMarket).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
it('must not see the price unit', function () {
// 7002-SORD-019
cy.getByTestId(orderPriceField).should('not.exist');
});
describe('time in force validations', function () {
const validTIF = TIFlist.filter((tif) => ['FOK', 'IOC'].includes(tif.code));
const invalidTIF = TIFlist.filter(
(tif) => !['FOK', 'IOC'].includes(tif.code)
);
validTIF.forEach((tif) => {
// 7002-SORD-025
// 7002-SORD-026
it(`must be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
});
});
invalidTIF.forEach((tif) => {
// 7002-SORD-023
// 7002-SORD-024
// 7002-SORD-027
// 7002-SORD-028
it(`must not be able to select ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).should('not.contain', tif.text);
});
});
});
});
describe('post and reduce order validations', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId(toggleMarket).click();
});
beforeEach(() => {
cy.setVegaWallet();
});
const validTIF = TIFlist.filter((tif) => ['FOK', 'IOC'].includes(tif.code));
validTIF.forEach((tif) => {
// 7002-SORD-025
// 7002-SORD-026
it(`post and reduce order market for ${tif.code}`, function () {
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId('post-only').should('be.disabled');
cy.getByTestId('reduce-only').should('be.enabled');
});
});
validTIF.forEach((tif) => {
it(`post and reduce order limit for ${tif.code}`, function () {
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId('post-only').should('be.disabled');
cy.getByTestId('reduce-only').should('be.enabled');
});
});
const validTIFLimit = TIFlist.filter((tif) =>
['GFA', 'GFN', 'GTC', 'GTT'].includes(tif.code)
);
validTIFLimit.forEach((tif) => {
it(`post and reduce order for limit ${tif.code}`, function () {
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select(tif.value);
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
'have.text',
tif.text
);
cy.getByTestId('post-only').should('be.enabled');
cy.getByTestId('reduce-only').should('be.disabled');
});
});
});
@@ -117,6 +117,7 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
// 0002-WCON-002
// 0002-WCON-005
// 0002-WCON-007
// 0002-WCON-009
mockConnectWallet();
cy.getByTestId(connectVegaBtn).click();
@@ -124,6 +125,10 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.wait('@walletReq');
cy.getByTestId(dialogContent).should(
'contain.text',
'Approve the connection from your Vega wallet app.'
);
cy.getByTestId(dialogContent).should('not.exist');
cy.getByTestId(manageVegaBtn).should('exist');
});
@@ -132,6 +137,7 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
// 0002-WCON-002
// 0002-WCON-005
// 0002-WCON-007
// 0002-WCON-015
mockConnectWalletWithUserError();
cy.getByTestId(connectVegaBtn).click();
@@ -8,6 +8,7 @@ import {
import { tooltipMapping } from '@vegaprotocol/market-info';
import {
addDecimalsFormatNumber,
createDocsLinks,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
@@ -23,6 +24,7 @@ import {
Tabs,
Link as UiToolkitLink,
Indicator,
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
@@ -37,6 +39,7 @@ import { Link, useParams } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
import { useEnvironment } from '@vegaprotocol/environment';
const enum LiquidityTabs {
Active = 'active',
@@ -144,6 +147,7 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
market?.tradableInstrument.instrument.product.settlementAsset.decimals || 0;
const symbol =
market?.tradableInstrument.instrument.product.settlementAsset.symbol;
const { VEGA_DOCS_URL } = useEnvironment();
const { params } = useNetworkParams([
NetworkParams.market_liquidity_stakeToCcyVolume,
@@ -211,6 +215,13 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
<HeaderStat heading={t('Market ID')}>
<div className="break-word">{marketId}</div>
</HeaderStat>
<HeaderStat heading={t('Learn more')}>
{VEGA_DOCS_URL && (
<ExternalLink href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}>
{t('Providing liquidity')}
</ExternalLink>
)}
</HeaderStat>
</Header>
);
});
@@ -29,7 +29,10 @@ import { LiquidityContainer } from '../liquidity/liquidity';
import { useNavigate } from 'react-router-dom';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import {
useMarketClickHandler,
useMarketLiquidityClickHandler,
} from '../../lib/hooks/use-market-click-handler';
type MarketDependantView =
| typeof CandlesChartContainer
@@ -79,6 +82,7 @@ const MarketBottomPanel = memo(
({ marketId, pinnedAsset }: BottomPanelProps) => {
const { screenSize } = useScreenDimensions();
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
return 'xxxl' === screenSize ? (
<ResizableGrid proportionalLayout minSize={200}>
@@ -94,6 +98,7 @@ const MarketBottomPanel = memo(
<TradingViews.Orders
marketId={marketId}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
enforceBottomPlaceholder
/>
</VegaWalletContainer>
@@ -150,6 +155,7 @@ const MarketBottomPanel = memo(
<TradingViews.Orders
marketId={marketId}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
enforceBottomPlaceholder
/>
</VegaWalletContainer>
@@ -293,6 +299,7 @@ interface TradePanelsProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}
@@ -303,12 +310,16 @@ export const TradePanels = ({
onClickCollateral,
pinnedAsset,
}: TradePanelsProps) => {
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [view, setView] = useState<TradingView>('Candles');
const renderView = () => {
const Component = memo<{
marketId: string;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}>(TradingViews[view]);
@@ -325,6 +336,8 @@ export const TradePanels = ({
onSelect={onSelect}
onClickCollateral={onClickCollateral}
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
/>
);
};
@@ -301,7 +301,7 @@ export const AccountHistoryChart = ({
asset: AssetFieldsFragment;
}) => {
const { theme } = useThemeSwitcher();
const values: { cols: string[]; rows: [Date, ...number[]][] } | null =
const values: { cols: [string, string]; rows: [Date, number][] } | null =
useMemo(() => {
if (!data?.balanceChanges.edges.length) {
return null;
@@ -19,7 +19,10 @@ import { usePageTitleStore } from '../../stores';
import { LedgerContainer } from '@vegaprotocol/ledger';
import { AccountsContainer } from '../../components/accounts-container';
import { AccountHistoryContainer } from './account-history-container';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import {
useMarketClickHandler,
useMarketLiquidityClickHandler,
} from '../../lib/hooks/use-market-click-handler';
export const Portfolio = () => {
const { updateTitle } = usePageTitleStore((store) => ({
@@ -31,6 +34,7 @@ export const Portfolio = () => {
}, [updateTitle]);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const wrapperClasses = 'h-full max-h-full flex flex-col';
return (
@@ -54,7 +58,10 @@ export const Portfolio = () => {
</Tab>
<Tab id="orders" name={t('Orders')}>
<VegaWalletContainer>
<OrderListContainer onMarketClick={onMarketClick} />
<OrderListContainer
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="fills" name={t('Fills')}>
@@ -8,6 +8,7 @@ import type { MarketData } from '@vegaprotocol/market-list';
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
import { HeaderStat } from '../header';
import {
ExternalLink,
Indicator,
KeyValueTable,
KeyValueTableRow,
@@ -18,9 +19,11 @@ import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
import { AuctionTrigger, MarketTradingMode } from '@vegaprotocol/types';
import {
addDecimalsFormatNumber,
createDocsLinks,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useEnvironment } from '@vegaprotocol/environment';
interface Props {
marketId?: string;
@@ -44,6 +47,8 @@ export const MarketLiquiditySupplied = ({
params.market_liquidity_targetstake_triggering_ratio
);
const { VEGA_DOCS_URL } = useEnvironment();
const variables = useMemo(
() => ({
marketId: marketId || '',
@@ -126,6 +131,14 @@ export const MarketLiquiditySupplied = ({
<Link href={`/#/liquidity/${marketId}`} data-testid="view-liquidity-link">
{t('View liquidity provision table')}
</Link>
{VEGA_DOCS_URL && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}
className="mt-2"
>
{t('Learn about providing liquidity')}
</ExternalLink>
)}
{showMessage && (
<p className="mt-4">
{t(
+5 -1
View File
@@ -23,6 +23,7 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { Links, Routes } from '../../pages/client-router';
import { createDocsLinks } from '@vegaprotocol/utils';
export const Navbar = ({
theme = 'system',
@@ -37,6 +38,7 @@ export const Navbar = ({
const tradingPath = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.MARKET]();
return (
<Navigation
appName="Console"
@@ -89,7 +91,9 @@ export const Navbar = ({
<NavigationContent>
<NavigationList>
<NavigationItem>
<NavExternalLink href={VEGA_DOCS_URL}>
<NavExternalLink
href={createDocsLinks(VEGA_DOCS_URL).NEW_TO_VEGA}
>
{t('Docs')}
</NavExternalLink>
</NavigationItem>
@@ -185,7 +185,7 @@ export const columns = (
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onSelect(market.id, e.metaKey);
onSelect(market.id, e.metaKey || e.ctrlKey);
}}
>
<UILink>{market.tradableInstrument.instrument.code}</UILink>
@@ -366,7 +366,7 @@ export const columnsPositionMarkets = (
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onSelect(market.id, e.metaKey);
onSelect(market.id, e.metaKey || e.ctrlKey);
}}
>
<UILink>{market.tradableInstrument.instrument.code}</UILink>
@@ -44,7 +44,7 @@ export const SelectMarketTableRow = ({
<tr
className={`hover:bg-neutral-200 dark:hover:bg-neutral-700 cursor-pointer relative h-[34px]`}
onClick={(ev) => {
onSelect(marketId, ev.metaKey);
onSelect(marketId, ev.metaKey || ev.ctrlKey);
}}
data-testid={`market-link-${marketId}`}
>
@@ -8,6 +8,10 @@ import type {
MarketData,
} from '@vegaprotocol/market-list';
import { SelectMarketLandingTable } from './welcome-landing-dialog';
const mockMarketClickHandler = jest.fn();
jest.mock('../../lib/hooks/use-market-click-handler', () => ({
useMarketClickHandler: () => mockMarketClickHandler,
}));
type Market = MarketMaybeWithCandles & MarketMaybeWithData;
type PartialMarket = Partial<
@@ -174,4 +178,25 @@ describe('WelcomeLandingDialog', () => {
fireEvent.click(screen.getAllByTestId(`market-link-2`)[0]);
expect(onClose).toHaveBeenCalled();
});
it('should not call onClose when metaKey is held', () => {
const onClose = jest.fn();
render(
<MemoryRouter>
<SelectMarketLandingTable
markets={[MARKET_A as Market, MARKET_B as Market]}
onClose={onClose}
/>
</MemoryRouter>,
{ wrapper: MockedProvider }
);
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0], {
metaKey: true,
});
expect(mockMarketClickHandler).toHaveBeenCalled();
expect(onClose).not.toHaveBeenCalled();
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
expect(onClose).toHaveBeenCalled();
});
});
@@ -12,9 +12,10 @@ import {
SelectMarketTableRow,
} from '../select-market';
import { WelcomeDialogHeader } from './welcome-dialog-header';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { ProposedMarkets } from './proposed-markets';
import { Links, Routes } from '../../pages/client-router';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
export const SelectMarketLandingTable = ({
markets,
@@ -23,23 +24,13 @@ export const SelectMarketLandingTable = ({
markets: MarketMaybeWithDataAndCandles[] | null;
onClose: () => void;
}) => {
const params = useParams();
const navigate = useNavigate();
const marketId = params.marketId;
const onSelect = useCallback(
(id: string) => {
if (id && id !== marketId) {
navigate(Links[Routes.MARKET](id));
}
},
[marketId, navigate]
);
const onSelect = useMarketClickHandler();
const onSelectMarket = useCallback(
(id: string) => {
onSelect(id);
onClose();
(id: string, metaKey?: boolean) => {
onSelect(id, metaKey);
if (!metaKey) {
onClose();
}
},
[onSelect, onClose]
);
@@ -73,7 +64,7 @@ export const SelectMarketLandingTable = ({
key={i}
detailed={false}
onSelect={onSelectMarket}
columns={columns(market, onSelect, onCellClick)}
columns={columns(market, onSelectMarket, onCellClick)}
/>
))}
</tbody>
@@ -19,3 +19,21 @@ export const useMarketClickHandler = (replace = false) => {
[navigate, marketId, replace, isMarketPage]
);
};
export const useMarketLiquidityClickHandler = (replace = false) => {
const navigate = useNavigate();
const { marketId } = useParams();
const { pathname } = useLocation();
const isLiquidityPage = pathname.match(/^\/liquidity\/(.+)/);
return useCallback(
(selectedId: string, metaKey?: boolean) => {
const link = Links[Routes.LIQUIDITY](selectedId);
if (metaKey) {
window.open(`/#${link}`, '_blank');
} else if (selectedId !== marketId || !isLiquidityPage) {
navigate(link, { replace });
}
},
[navigate, marketId, replace, isLiquidityPage]
);
};
@@ -7,7 +7,11 @@ import {
} from '@vegaprotocol/types';
import type { VegaStoredTxState } from '@vegaprotocol/wallet';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import { VegaTransactionDetails } from './use-vega-transaction-toasts';
import {
VegaTransactionDetails,
getVegaTransactionContentIntent,
} from './use-vega-transaction-toasts';
import { Intent } from '@vegaprotocol/ui-toolkit';
jest.mock('@vegaprotocol/assets', () => {
const A1 = {
@@ -278,3 +282,27 @@ describe('VegaTransactionDetails', () => {
expect(queryByTestId('toast-panel')?.textContent).toEqual(details);
});
});
describe('getVegaTransactionContentIntent', () => {
it('returns the correct intent for a transaction', () => {
expect(getVegaTransactionContentIntent(withdraw).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(submitOrder).intent).toBe(
Intent.Success
);
expect(getVegaTransactionContentIntent(editOrder).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(cancelOrder).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(cancelAll).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(closePosition).intent).toBe(
Intent.Primary
);
expect(getVegaTransactionContentIntent(batch).intent).toBe(Intent.Primary);
});
});
@@ -39,10 +39,14 @@ import { t } from '@vegaprotocol/i18n';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
import { getRejectionReason, useOrderByIdQuery } from '@vegaprotocol/orders';
import {
getOrderToastIntent,
getOrderToastTitle,
getRejectionReason,
useOrderByIdQuery,
} from '@vegaprotocol/orders';
import { useMarketList } from '@vegaprotocol/market-list';
import type { Side } from '@vegaprotocol/types';
import { OrderStatus } from '@vegaprotocol/types';
import { OrderStatusMapping } from '@vegaprotocol/types';
import { Size } from '@vegaprotocol/react-helpers';
@@ -478,12 +482,10 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
getRejectionReason(tx.order) || tx.order.rejectionReason || '';
return (
<>
<ToastHeading>{t('Order rejected')}</ToastHeading>
{rejectionReason || tx.order.rejectionReason ? (
<ToastHeading>{getOrderToastTitle(tx.order.status)}</ToastHeading>
{rejectionReason ? (
<p>
{t('Your order has been rejected because: %s', [
rejectionReason || tx.order.rejectionReason,
])}
{t('Your order has been rejected because: %s', [rejectionReason])}
</p>
) : (
<p>{t('Your order has been rejected.')}</p>
@@ -506,7 +508,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
if (isOrderSubmissionTransaction(tx.body) && tx.order?.rejectionReason) {
return (
<div>
<h3 className="font-bold">{t('Order rejected')}</h3>
<h3 className="font-bold">{getOrderToastTitle(tx.order.status)}</h3>
<p>{t('Your order was rejected.')}</p>
{tx.txHash && (
<p className="break-all">
@@ -545,7 +547,11 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
return (
<>
<ToastHeading>{t('Confirmed')}</ToastHeading>
<ToastHeading>
{tx.order?.status
? getOrderToastTitle(tx.order.status)
: t('Confirmed')}
</ToastHeading>
<p>{t('Your transaction has been confirmed ')}</p>
{tx.txHash && (
<p className="break-all">
@@ -580,7 +586,7 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
tx.error instanceof WalletError &&
walletNoConnectionCodes.includes(tx.error.code);
if (orderRejection) {
label = t('Order rejected');
label = getOrderToastTitle(tx.order?.status) || t('Order rejected');
errorMessage = t('Your order has been rejected because: %s', [
orderRejection || tx.order?.rejectionReason || ' ',
]);
@@ -632,30 +638,8 @@ export const useVegaTransactionToasts = () => {
);
const fromVegaTransaction = (tx: VegaStoredTxState): Toast => {
let content: ToastContent;
const closeAfter = isFinal(tx) ? CLOSE_AFTER : undefined;
if (tx.status === VegaTxStatus.Requested) {
content = <VegaTxRequestedToastContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Pending) {
content = <VegaTxPendingToastContentProps tx={tx} />;
}
if (tx.status === VegaTxStatus.Complete) {
content = <VegaTxCompleteToastsContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Error) {
content = <VegaTxErrorToastContent tx={tx} />;
}
// Transaction can be successful but the order can be rejected by the network
const intent =
(tx.order &&
[OrderStatus.STATUS_REJECTED, OrderStatus.STATUS_STOPPED].includes(
tx.order.status
)) ||
tx.order?.rejectionReason
? Intent.Danger
: intentMap[tx.status];
const { intent, content } = getVegaTransactionContentIntent(tx);
return {
id: `vega-${tx.id}`,
@@ -679,3 +663,27 @@ export const useVegaTransactionToasts = () => {
}
);
};
export const getVegaTransactionContentIntent = (tx: VegaStoredTxState) => {
let content: ToastContent;
if (tx.status === VegaTxStatus.Requested) {
content = <VegaTxRequestedToastContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Pending) {
content = <VegaTxPendingToastContentProps tx={tx} />;
}
if (tx.status === VegaTxStatus.Complete) {
content = <VegaTxCompleteToastsContent tx={tx} />;
}
if (tx.status === VegaTxStatus.Error) {
content = <VegaTxErrorToastContent tx={tx} />;
}
// Transaction can be successful but the order can be rejected by the network
const intent =
(tx.order &&
!isOrderAmendmentTransaction(tx.body) &&
getOrderToastIntent(tx.order.status)) ||
intentMap[tx.status];
return { intent, content };
};
+15
View File
@@ -33,6 +33,8 @@ import { ViewingBanner } from '../components/viewing-banner';
import { Banner } from '../components/banner';
import { AppLoader, DynamicLoader } from '../components/app-loader';
import { Navbar } from '../components/navbar';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { activeOrdersProvider } from '@vegaprotocol/orders';
const DEFAULT_TITLE = t('Welcome to Vega trading!');
@@ -95,6 +97,7 @@ function AppBody({ Component }: AppProps) {
<ToastsManager />
<InitializeHandlers />
<MaybeConnectEagerly />
<PartyData />
</div>
);
}
@@ -127,6 +130,18 @@ function VegaTradingApp(props: AppProps) {
export default VegaTradingApp;
const PartyData = () => {
const { pubKey } = useVegaWallet();
const variables = { partyId: pubKey || '' };
const skip = !pubKey;
useDataProvider({
dataProvider: activeOrdersProvider,
variables,
skip,
});
return null;
};
const MaybeConnectEagerly = () => {
useVegaEagerConnect(Connectors);
useEthereumEagerConnect();
+15 -3
View File
@@ -31,7 +31,7 @@ export enum Routes {
MARKET = '/markets',
MARKETS = '/markets/all',
PORTFOLIO = '/portfolio',
LIQUIDITY = 'liquidity/:marketId',
LIQUIDITY = '/liquidity',
}
type ConsoleLinks = { [r in Routes]: (...args: string[]) => string };
@@ -41,8 +41,10 @@ export const Links: ConsoleLinks = {
marketId ? trimEnd(`${Routes.MARKET}/${marketId}`, '/') : Routes.MARKET,
[Routes.MARKETS]: () => Routes.MARKETS,
[Routes.PORTFOLIO]: () => Routes.PORTFOLIO,
[Routes.LIQUIDITY]: (marketId: string) =>
Routes.LIQUIDITY.replace(':marketId', marketId),
[Routes.LIQUIDITY]: (marketId: string | null | undefined) =>
marketId
? trimEnd(`${Routes.LIQUIDITY}/${marketId}`, '/')
: Routes.LIQUIDITY,
};
const routerConfig: RouteObject[] = [
@@ -70,6 +72,16 @@ const routerConfig: RouteObject[] = [
{
path: Routes.LIQUIDITY,
element: <LazyLiquidity />,
children: [
{
index: true,
element: <LazyLiquidity />,
},
{
path: ':marketId',
element: <LazyLiquidity />,
},
],
},
{
path: Routes.PORTFOLIO,
+15
View File
@@ -34,6 +34,18 @@ html [data-theme='dark'] {
/* sell candles only use stroke as the candle is solid (without border) */
--pennant-color-sell-stroke: theme('colors.vega.pink.500');
/* studies */
--pennant-color-eldar-ray-bear-power: theme('colors.vega.pink.500');
--pennant-color-eldar-ray-bull-power: theme('colors.vega.green.650');
--pennant-color-macd-divergence-buy: theme('colors.vega.green.650');
--pennant-color-macd-divergence-sell: theme('colors.vega.pink.500');
--pennant-color-macd-signal: theme('colors.vega.blue.500');
--pennant-color-macd-macd: theme('colors.vega.yellow.500');
--pennant-color-volume-buy: theme('colors.vega.green.650');
--pennant-color-volume-sell: theme('colors.vega.pink.500');
/* depth chart */
--pennant-color-depth-buy-fill: theme('colors.vega.green.650');
--pennant-color-depth-buy-stroke: theme('colors.vega.green.500');
@@ -50,6 +62,9 @@ html [data-theme='light'] {
/* sell candles only use stroke as the candle is solid (without border) */
--pennant-color-sell-stroke: theme('colors.vega.pink.400');
--pennant-color-volume-buy: theme('colors.vega.green.400');
--pennant-color-volume-sell: theme('colors.vega.pink.400');
/* depth chart */
--pennant-color-depth-buy-fill: theme('colors.vega.green.400');
--pennant-color-depth-buy-stroke: theme('colors.vega.green.550');
+6
View File
@@ -0,0 +1,6 @@
FROM --platform=amd64 nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
EXPOSE 80
WORKDIR /usr/share/nginx/html
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -rf /usr/share/nginx/html/*
COPY ./dist-result/ /usr/share/nginx/html/
+3 -5
View File
@@ -5,7 +5,9 @@ export PATH="/app/node_modules/.bin:$PATH"
flags="--network-timeout 100000 --pure-lockfile"
if [[ ! -z "${ENV_NAME}" ]]; then
flags="--env=${ENV_NAME} $flags"
if [[ "${ENV_NAME}" != "ops-vega" ]]; then
flags="--env=${ENV_NAME} $flags"
fi
fi
if [ "${APP}" = "trading" ]; then
@@ -16,7 +18,3 @@ if [ "${APP}" = "trading" ]; then
else
yarn nx build ${APP} $flags
fi
env_vars_file="/app/dist/apps/${APP}/.env"
# make sure there are no exposed .env files
rm $env_vars_file || echo "No env vars file"
+5 -4
View File
@@ -13,7 +13,7 @@ RUN apk add --update --no-cache \
COPY . ./
RUN yarn --network-timeout 100000 --pure-lockfile
# work around for different build process in trading
RUN sh ./docker-build.sh
RUN sh docker/docker-build.sh
# Server environment
# if this fails you need to docker pull nginx:1.23-alpine and pin new SHA
@@ -23,6 +23,7 @@ FROM --platform=amd64 nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a
EXPOSE 80
# Copy dist
WORKDIR /usr/share/nginx/html
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/apps/${APP} /usr/share/nginx/html
RUN apk add --no-cache go-ipfs; ipfs init && echo "$(ipfs add -rQ .)" > ipfs-hash; apk del go-ipfs
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -rf /usr/share/nginx/html/*
COPY --from=build /app/dist/apps/${APP}/* /usr/share/nginx/html
RUN apk add --no-cache go-ipfs; ipfs init && echo "$(ipfs add -rQ .)" > /ipfs-hash; apk del go-ipfs
+221 -72
View File
@@ -1,4 +1,10 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import BigNumber from 'bignumber.js';
import { AddressField, TransferFee, TransferForm } from './transfer-form';
import { AccountType } from '@vegaprotocol/types';
@@ -27,6 +33,34 @@ describe('TransferForm', () => {
submitTransfer: jest.fn(),
};
it('validates a manually entered address', async () => {
render(<TransferForm {...props} />);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
const toggle = screen.getByText('Enter manually');
fireEvent.click(toggle);
// has switched to input
expect(toggle).toHaveTextContent('Select from wallet');
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: 'invalid-address' },
});
await waitFor(() => {
const errors = screen.getAllByTestId('input-error-text');
expect(errors[0]).toHaveTextContent('Invalid Vega key');
});
// same pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: pubKey },
});
await waitFor(() => {
const errors = screen.getAllByTestId('input-error-text');
expect(errors[0]).toHaveTextContent('Vega key is the same');
});
});
it('validates fields and submits', async () => {
render(<TransferForm {...props} />);
@@ -62,15 +96,17 @@ describe('TransferForm', () => {
formatNumber(asset.balance, asset.decimals)
);
const amountInput = screen.getByLabelText('Amount');
// Test amount validation
fireEvent.change(screen.getByLabelText('Amount'), {
fireEvent.change(amountInput, {
target: { value: '0.00000001' },
});
expect(
await screen.findByText('Value is below minimum')
).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('Amount'), {
fireEvent.change(amountInput, {
target: { value: '9999999' },
});
expect(
@@ -78,7 +114,7 @@ describe('TransferForm', () => {
).toBeInTheDocument();
// set valid amount
fireEvent.change(screen.getByLabelText('Amount'), {
fireEvent.change(amountInput, {
target: { value: amount },
});
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
@@ -100,78 +136,191 @@ describe('TransferForm', () => {
});
});
it('validates a manually entered address', async () => {
render(<TransferForm {...props} />);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
const toggle = screen.getByText('Enter manually');
fireEvent.click(toggle);
// has switched to input
expect(toggle).toHaveTextContent('Select from wallet');
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: 'invalid-address' },
});
await waitFor(() => {
const errors = screen.getAllByTestId('input-error-text');
expect(errors[0]).toHaveTextContent('Invalid Vega key');
describe('IncludeFeesCheckbox', () => {
it('validates fields and submits when checkbox is checked', async () => {
render(<TransferForm {...props} />);
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
expect(keySelect.children).toHaveLength(2);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
'',
props.pubKeys[1],
]);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
// Select a pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: props.pubKeys[1] },
});
// Select asset
fireEvent.change(
// Bypass RichSelect and target hidden native select
// eslint-disable-next-line
document.querySelector('select[name="asset"]')!,
{ target: { value: asset.id } }
);
// assert rich select as updated
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
asset.name
);
expect(screen.getByTestId('asset-balance')).toHaveTextContent(
formatNumber(asset.balance, asset.decimals)
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
act(() => {
/* fire events that update state */
// set valid amount
fireEvent.change(amountInput, {
target: { value: amount },
});
// check include fees checkbox
fireEvent.click(checkbox);
});
expect(checkbox).toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
expectedAmount
);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
amount
);
submit();
await waitFor(() => {
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
expect(props.submitTransfer).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKeys[1],
asset: asset.id,
amount: removeDecimal(amount, asset.decimals),
oneOff: {},
});
});
});
// same pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: pubKey },
it('validates fields when checkbox is not checked', async () => {
render(<TransferForm {...props} />);
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
expect(keySelect.children).toHaveLength(2);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
'',
props.pubKeys[1],
]);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
// Select a pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: props.pubKeys[1] },
});
// Select asset
fireEvent.change(
// Bypass RichSelect and target hidden native select
// eslint-disable-next-line
document.querySelector('select[name="asset"]')!,
{ target: { value: asset.id } }
);
// assert rich select as updated
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
asset.name
);
expect(screen.getByTestId('asset-balance')).toHaveTextContent(
formatNumber(asset.balance, asset.decimals)
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
act(() => {
/* fire events that update state */
// set valid amount
fireEvent.change(amountInput, {
target: { value: amount },
});
});
expect(checkbox).not.toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const total = new BigNumber(amount).plus(expectedFee).toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
});
});
describe('AddressField', () => {
const props = {
pubKeys: ['pubkey-1', 'pubkey-2'],
select: <div>select</div>,
input: <div>input</div>,
onChange: jest.fn(),
};
it('toggles content and calls onChange', async () => {
const mockOnChange = jest.fn();
render(<AddressField {...props} onChange={mockOnChange} />);
// select should be shown as multiple pubkeys provided
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
fireEvent.click(screen.getByText('Enter manually'));
expect(screen.queryByText('select')).not.toBeInTheDocument();
expect(screen.getByText('input')).toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByText('Select from wallet'));
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(2);
});
await waitFor(() => {
const errors = screen.getAllByTestId('input-error-text');
expect(errors[0]).toHaveTextContent('Vega key is the same');
it('Does not provide select option if there is only a single key', () => {
render(<AddressField {...props} pubKeys={['single-pubKey']} />);
expect(screen.getByText('input')).toBeInTheDocument();
expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
});
});
describe('TransferFee', () => {
const props = {
amount: '200',
feeFactor: '0.001',
fee: '0.2',
transferAmount: '200',
decimals: 8,
};
it('calculates and renders the transfer fee', () => {
render(<TransferFee {...props} />);
const expected = new BigNumber(props.amount)
.times(props.feeFactor)
.toFixed();
const total = new BigNumber(props.amount).plus(expected).toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
props.amount
);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
});
});
});
describe('AddressField', () => {
const props = {
pubKeys: ['pubkey-1', 'pubkey-2'],
select: <div>select</div>,
input: <div>input</div>,
onChange: jest.fn(),
};
it('toggles content and calls onChange', async () => {
const mockOnChange = jest.fn();
render(<AddressField {...props} onChange={mockOnChange} />);
// select should be shown as multiple pubkeys provided
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
fireEvent.click(screen.getByText('Enter manually'));
expect(screen.queryByText('select')).not.toBeInTheDocument();
expect(screen.getByText('input')).toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByText('Select from wallet'));
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(2);
});
it('Does not provide select option if there is only a single key', () => {
render(<AddressField {...props} pubKeys={['single-pubKey']} />);
expect(screen.getByText('input')).toBeInTheDocument();
expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
});
});
describe('TransferFee', () => {
const props = {
amount: '200',
feeFactor: '0.001',
};
it('calculates and renders the transfer fee', () => {
render(<TransferFee {...props} />);
const expected = new BigNumber(props.amount)
.times(props.feeFactor)
.toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
});
});
+102 -16
View File
@@ -16,6 +16,7 @@ import {
RichSelect,
Select,
Tooltip,
Checkbox,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
@@ -63,6 +64,26 @@ export const TransferForm = ({
const amount = watch('amount');
const assetId = watch('asset');
const [includeFee, setIncludeFee] = useState(false);
const transferAmount = useMemo(() => {
if (!amount) return undefined;
if (includeFee && feeFactor) {
return new BigNumber(1).minus(feeFactor).times(amount).toString();
}
return amount;
}, [amount, includeFee, feeFactor]);
const fee = useMemo(() => {
if (!transferAmount) return undefined;
if (includeFee) {
return new BigNumber(amount).minus(transferAmount).toString();
}
return (
feeFactor && new BigNumber(feeFactor).times(transferAmount).toString()
);
}, [amount, includeFee, transferAmount, feeFactor]);
const asset = useMemo(() => {
return assets.find((a) => a.id === assetId);
}, [assets, assetId]);
@@ -72,13 +93,16 @@ export const TransferForm = ({
if (!asset) {
throw new Error('Submitted transfer with no asset selected');
}
const transfer = normalizeTransfer(fields.toAddress, fields.amount, {
if (!transferAmount) {
throw new Error('Submitted transfer with no amount selected');
}
const transfer = normalizeTransfer(fields.toAddress, transferAmount, {
id: asset.id,
decimals: asset.decimals,
});
submitTransfer(transfer);
},
[asset, submitTransfer]
[asset, submitTransfer, transferAmount]
);
const min = useMemo(() => {
@@ -213,7 +237,32 @@ export const TransferForm = ({
<InputError forInput="amount">{errors.amount.message}</InputError>
)}
</FormGroup>
<TransferFee amount={amount} feeFactor={feeFactor} />
<div className="mb-4">
<Checkbox
name="include-transfer-fee"
disabled={!transferAmount}
label={
<Tooltip
description={t(
`The fee will be taken from the amount you are transferring.`
)}
>
<div>{t('Include transfer fee')}</div>
</Tooltip>
}
checked={includeFee}
onCheckedChange={() => setIncludeFee(!includeFee)}
/>
</div>
{transferAmount && fee && (
<TransferFee
amount={transferAmount}
transferAmount={transferAmount}
feeFactor={feeFactor}
fee={fee}
decimals={asset?.decimals}
/>
)}
<Button type="submit" variant="primary" fill={true}>
{t('Confirm transfer')}
</Button>
@@ -223,34 +272,71 @@ export const TransferForm = ({
export const TransferFee = ({
amount,
transferAmount,
feeFactor,
fee,
decimals,
}: {
amount: string;
transferAmount: string;
feeFactor: string | null;
fee?: string;
decimals?: number;
}) => {
if (!feeFactor || !amount) return null;
if (!feeFactor || !amount || !transferAmount || !fee) return null;
// using toFixed without an argument will always return a
// number in normal notation without rounding, formatting functions
// arent working in a way which won't round the decimal places
const value = new BigNumber(amount).times(feeFactor).toFixed();
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
return (
<div className="mb-4 flex justify-between items-center gap-4 flex-wrap">
<div>
<div className="mb-4 flex flex-col gap-2 text-xs">
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to ${feeFactor}`
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to %s`,
[feeFactor]
)}
>
<div>{t('Transfer fee')}</div>
</Tooltip>
<div
data-testid="transfer-fee"
className="text-neutral-500 dark:text-neutral-300"
>
{formatNumber(fee, decimals)}
</div>
</div>
<div
data-testid="transfer-fee"
className="text-neutral-500 dark:text-neutral-300"
>
{value}
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The total amount to be transferred (without the fee)`
)}
>
<div>{t('Amount to be transferred')}</div>
</Tooltip>
<div
data-testid="transfer-amount"
className="text-neutral-500 dark:text-neutral-300"
>
{formatNumber(amount, decimals)}
</div>
</div>
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The total amount taken from your account. The amount to be transferred plus the fee.`
)}
>
<div>{t('Total amount (with fee)')}</div>
</Tooltip>
<div
data-testid="total-transfer-fee"
className="text-neutral-500 dark:text-neutral-300"
>
{formatNumber(totalValue, decimals)}
</div>
</div>
</div>
);
+5
View File
@@ -1 +1,6 @@
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
import { defaultFallbackInView } from 'react-intersection-observer';
defaultFallbackInView(true);
global.ResizeObserver = ResizeObserver;
+18 -1
View File
@@ -1,8 +1,25 @@
fragment AssetListFields on Asset {
id
name
symbol
decimals
quantum
source {
__typename
... on ERC20 {
contractAddress
lifetimeLimit
withdrawThreshold
}
}
status
}
query Assets {
assetsConnection {
edges {
node {
...AssetFields
...AssetListFields
}
}
}
+23 -5
View File
@@ -1,26 +1,44 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import { AssetFieldsFragmentDoc } from './Asset';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type AssetListFieldsFragment = { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string } };
export type AssetsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type AssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string }, infrastructureFeeAccount?: { __typename?: 'AccountBalance', balance: string } | null, globalRewardPoolAccount?: { __typename?: 'AccountBalance', balance: string } | null, takerFeeRewardAccount?: { __typename?: 'AccountBalance', balance: string } | null, makerFeeRewardAccount?: { __typename?: 'AccountBalance', balance: string } | null, lpFeeRewardAccount?: { __typename?: 'AccountBalance', balance: string } | null, marketProposerRewardAccount?: { __typename?: 'AccountBalance', balance: string } | null } } | null> | null } | null };
export type AssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string } } } | null> | null } | null };
export const AssetListFieldsFragmentDoc = gql`
fragment AssetListFields on Asset {
id
name
symbol
decimals
quantum
source {
__typename
... on ERC20 {
contractAddress
lifetimeLimit
withdrawThreshold
}
}
status
}
`;
export const AssetsDocument = gql`
query Assets {
assetsConnection {
edges {
node {
...AssetFields
...AssetListFields
}
}
}
}
${AssetFieldsFragmentDoc}`;
${AssetListFieldsFragmentDoc}`;
/**
* __useAssetsQuery__
@@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react';
import * as Schema from '@vegaprotocol/types';
import { AssetDetailsDialog } from './asset-details-dialog';
import { AssetDetail, testId } from './asset-details-table';
import { AssetsDocument } from './__generated__/Assets';
import { AssetDocument } from './__generated__/Asset';
import { generateBuiltinAsset, generateERC20Asset } from './test-helpers';
const mockedData = {
@@ -39,15 +39,17 @@ const mockedData = {
},
};
const mocks = [
{
request: {
query: AssetsDocument,
variables: {},
},
result: mockedData,
const mocks = mockedData.data.assetsConnection.edges.map((mock) => ({
request: {
query: AssetDocument,
variables: { assetId: mock.node.id },
},
];
result: {
data: {
assetsConnection: { edges: [mock] },
},
},
}));
const WrappedAssetDetailsDialog = ({ assetId }: { assetId: string }) => (
<MockedProvider mocks={mocks}>
+2 -3
View File
@@ -1,5 +1,4 @@
import { t } from '@vegaprotocol/i18n';
import { useAssetsDataProvider } from './assets-data-provider';
import {
Button,
Dialog,
@@ -10,6 +9,7 @@ import {
import { create } from 'zustand';
import { AssetDetailsTable } from './asset-details-table';
import { AssetProposalNotification } from '@vegaprotocol/proposals';
import { useAssetDataProvider } from './asset-data-provider';
export type AssetDetailsDialogStore = {
isOpen: boolean;
@@ -55,9 +55,8 @@ export const AssetDetailsDialog = ({
onChange,
asJson = false,
}: AssetDetailsDialogProps) => {
const { data } = useAssetsDataProvider();
const { data: asset } = useAssetDataProvider(assetId);
const asset = data?.find((a) => a.id === assetId);
const assetSymbol = asset?.symbol || '';
const content = asset ? (
+2 -3
View File
@@ -11,6 +11,7 @@ import {
} from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import type { Asset } from './asset-data-provider';
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from './constants';
type Rows = {
key: AssetDetail;
@@ -121,9 +122,7 @@ export const rows: Rows = [
{
key: AssetDetail.WITHDRAWAL_THRESHOLD,
label: t('Withdrawal threshold'),
tooltip: t(
'The maximum you can withdraw instantly. Theres no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them'
),
tooltip: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
value: (asset) =>
num(asset, (asset.source as Schema.ERC20).withdrawThreshold),
},
+5 -119
View File
@@ -1,8 +1,10 @@
import merge from 'lodash/merge';
import type { AssetsQuery } from './__generated__/Assets';
import type {
AssetsQuery,
AssetListFieldsFragment,
} from './__generated__/Assets';
import * as Types from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import type { AssetFieldsFragment } from './__generated__/Asset';
export const assetsQuery = (
override?: PartialDeep<AssetsQuery>
@@ -18,7 +20,7 @@ export const assetsQuery = (
return merge(defaultAssets, override);
};
const assetFields: AssetFieldsFragment[] = [
const assetFields: AssetListFieldsFragment[] = [
{
__typename: 'Asset',
id: 'asset-id',
@@ -33,30 +35,6 @@ const assetFields: AssetFieldsFragment[] = [
},
quantum: '1',
status: Types.AssetStatus.STATUS_ENABLED,
infrastructureFeeAccount: {
balance: '1',
__typename: 'AccountBalance',
},
globalRewardPoolAccount: {
balance: '2',
__typename: 'AccountBalance',
},
takerFeeRewardAccount: {
balance: '3',
__typename: 'AccountBalance',
},
makerFeeRewardAccount: {
balance: '4',
__typename: 'AccountBalance',
},
lpFeeRewardAccount: {
balance: '5',
__typename: 'AccountBalance',
},
marketProposerRewardAccount: {
balance: '6',
__typename: 'AccountBalance',
},
},
{
__typename: 'Asset',
@@ -72,30 +50,6 @@ const assetFields: AssetFieldsFragment[] = [
},
quantum: '1',
status: Types.AssetStatus.STATUS_ENABLED,
infrastructureFeeAccount: {
balance: '1',
__typename: 'AccountBalance',
},
globalRewardPoolAccount: {
balance: '2',
__typename: 'AccountBalance',
},
takerFeeRewardAccount: {
balance: '3',
__typename: 'AccountBalance',
},
makerFeeRewardAccount: {
balance: '4',
__typename: 'AccountBalance',
},
lpFeeRewardAccount: {
balance: '5',
__typename: 'AccountBalance',
},
marketProposerRewardAccount: {
balance: '6',
__typename: 'AccountBalance',
},
},
{
__typename: 'Asset',
@@ -104,20 +58,10 @@ const assetFields: AssetFieldsFragment[] = [
decimals: 5,
name: 'Asto',
source: {
maxFaucetAmountMint: '5000000000',
__typename: 'BuiltinAsset',
},
quantum: '1',
status: Types.AssetStatus.STATUS_ENABLED,
infrastructureFeeAccount: {
balance: '0',
__typename: 'AccountBalance',
},
globalRewardPoolAccount: null,
takerFeeRewardAccount: null,
makerFeeRewardAccount: null,
lpFeeRewardAccount: null,
marketProposerRewardAccount: null,
},
{
__typename: 'Asset',
@@ -126,20 +70,10 @@ const assetFields: AssetFieldsFragment[] = [
decimals: 5,
name: 'tBTC TEST',
source: {
maxFaucetAmountMint: '5000000000',
__typename: 'BuiltinAsset',
},
quantum: '1',
status: Types.AssetStatus.STATUS_ENABLED,
infrastructureFeeAccount: {
balance: '0',
__typename: 'AccountBalance',
},
globalRewardPoolAccount: null,
takerFeeRewardAccount: null,
makerFeeRewardAccount: null,
lpFeeRewardAccount: null,
marketProposerRewardAccount: null,
},
// NOTE: These assets ids and contract addresses are real assets on Sepolia, this is needed
// because we don't currently mock our seplia infura provider. If we change network these will
@@ -158,30 +92,6 @@ const assetFields: AssetFieldsFragment[] = [
__typename: 'ERC20',
},
quantum: '1',
infrastructureFeeAccount: {
balance: '1',
__typename: 'AccountBalance',
},
globalRewardPoolAccount: {
balance: '2',
__typename: 'AccountBalance',
},
takerFeeRewardAccount: {
balance: '3',
__typename: 'AccountBalance',
},
makerFeeRewardAccount: {
balance: '4',
__typename: 'AccountBalance',
},
lpFeeRewardAccount: {
balance: '5',
__typename: 'AccountBalance',
},
marketProposerRewardAccount: {
balance: '6',
__typename: 'AccountBalance',
},
},
{
__typename: 'Asset',
@@ -197,29 +107,5 @@ const assetFields: AssetFieldsFragment[] = [
__typename: 'ERC20',
},
quantum: '1',
infrastructureFeeAccount: {
balance: '1',
__typename: 'AccountBalance',
},
globalRewardPoolAccount: {
balance: '2',
__typename: 'AccountBalance',
},
takerFeeRewardAccount: {
balance: '3',
__typename: 'AccountBalance',
},
makerFeeRewardAccount: {
balance: '4',
__typename: 'AccountBalance',
},
lpFeeRewardAccount: {
balance: '5',
__typename: 'AccountBalance',
},
marketProposerRewardAccount: {
balance: '6',
__typename: 'AccountBalance',
},
},
];
+4
View File
@@ -0,0 +1,4 @@
import { t } from '@vegaprotocol/i18n';
export const WITHDRAW_THRESHOLD_TOOLTIP_TEXT = t(
"The maximum you can withdraw instantly. There's no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them"
);
+1
View File
@@ -5,3 +5,4 @@ export * from './assets-data-provider';
export * from './asset-details-dialog';
export * from './asset-details-table';
export * from './asset-option';
export * from './constants';
+2 -2
View File
@@ -1,6 +1,6 @@
import 'pennant/dist/style.css';
import {
Chart,
CandlestickChart,
ChartType,
Interval,
Overlay,
@@ -234,7 +234,7 @@ export const CandlesChartContainer = ({
</DropdownMenu>
</div>
<div className="flex-1">
<Chart
<CandlestickChart
dataSource={dataSource}
options={{
chartType: chartType,
+3
View File
@@ -52,6 +52,9 @@ export const checkSorting = (
cy.get(`[col-id="${column}"]`).click();
});
checkSortChange(orderTabDesc, column);
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="${column}"]`).click();
});
};
const checkSortChange = (tabsArr: string[], column: string) => {
+1
View File
@@ -10,6 +10,7 @@ export * from './lib/cells/price-flash-cell';
export * from './lib/cells/vol-cell';
export * from './lib/cells/centered-grid-cell';
export * from './lib/cells/market-name-cell';
export * from './lib/cells/order-type-cell';
export * from './lib/filters/date-range-filter';
export * from './lib/filters/set-filter';
@@ -1,6 +1,6 @@
import { memo } from 'react';
import { BID_COLOR, ASK_COLOR } from './vol-cell';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
import { NumericCell } from './numeric-cell';
export interface CumulativeVolProps {
@@ -55,7 +55,7 @@ export const CumulativeVol = memo(
(
<NumericCell
value={Number(indicativeVolume)}
valueFormatted={addDecimalsFormatNumber(
valueFormatted={addDecimalsFixedFormatNumber(
indicativeVolume,
positionDecimalPlaces ?? 0
)}
@@ -67,7 +67,7 @@ export const CumulativeVol = memo(
{ask ? (
<NumericCell
value={ask}
valueFormatted={addDecimalsFormatNumber(
valueFormatted={addDecimalsFixedFormatNumber(
ask,
positionDecimalPlaces ?? 0
)}
@@ -77,7 +77,7 @@ export const CumulativeVol = memo(
{bid ? (
<NumericCell
value={ask}
valueFormatted={addDecimalsFormatNumber(
valueFormatted={addDecimalsFixedFormatNumber(
bid,
positionDecimalPlaces ?? 0
)}
@@ -21,7 +21,7 @@ export const MarketNameCell = ({
ev.preventDefault();
ev.stopPropagation();
if (onMarketClick) {
onMarketClick(id, ev.metaKey);
onMarketClick(id, ev.metaKey || ev.ctrlKey);
}
},
[id, onMarketClick]
@@ -0,0 +1,52 @@
import type { MouseEvent } from 'react';
import { useMemo } from 'react';
import { useCallback } from 'react';
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
interface OrderTypeCellProps {
value?: Schema.OrderType;
data?: Schema.Order;
onClick?: (marketId: string, metaKey?: boolean) => void;
}
export const OrderTypeCell = ({
value,
data: order,
onClick,
}: OrderTypeCellProps) => {
const id = order ? order.market.id : '';
const label = useMemo(() => {
if (!order) {
return undefined;
}
if (!value) return '-';
if (order?.peggedOrder) {
return t('Pegged');
}
if (order?.liquidityProvision) {
return t('Liquidity provision');
}
return Schema.OrderTypeMapping[value];
}, [order, value]);
const handleOnClick = useCallback(
(ev: MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
ev.stopPropagation();
if (onClick) {
onClick(id, ev.metaKey || ev.ctrlKey);
}
},
[id, onClick]
);
if (!order) return null;
return order?.liquidityProvision ? (
<button onClick={handleOnClick} tabIndex={0} className="underline">
{label}
</button>
) : (
<span>{label}</span>
);
};
@@ -8,7 +8,7 @@ const commonProps = {
describe('DateRangeFilter', () => {
it('should be properly rendered', async () => {
const defaultRangeFilter = {
const defaultValue = {
start: '2023-02-14T13:53:01+01:00',
end: '2023-02-21T13:53:01+01:00',
};
@@ -17,7 +17,7 @@ describe('DateRangeFilter', () => {
render(
<DateRangeFilter
{...(commonProps as unknown as DateRangeFilterProps)}
defaultRangeFilter={defaultRangeFilter}
defaultValue={defaultValue}
/>
);
@@ -1,5 +1,5 @@
import type { ChangeEvent } from 'react';
import { useEffect, useMemo } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import type * as Schema from '@vegaprotocol/types';
import { forwardRef, useImperativeHandle, useState } from 'react';
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
@@ -17,9 +17,9 @@ import { formatForInput } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { InputError } from '@vegaprotocol/ui-toolkit';
const defaultFilterValue: Schema.DateRange = {};
const defaultValue: Schema.DateRange = {};
export interface DateRangeFilterProps extends IFilterParams {
defaultRangeFilter?: Schema.DateRange;
defaultValue?: Schema.DateRange;
maxSubDays?: number;
maxNextDays?: number;
maxDaysRange?: number;
@@ -27,8 +27,9 @@ export interface DateRangeFilterProps extends IFilterParams {
export const DateRangeFilter = forwardRef(
(props: DateRangeFilterProps, ref) => {
const defaultDates = props?.defaultRangeFilter || defaultFilterValue;
const defaultDates = props?.defaultValue || defaultValue;
const [value, setValue] = useState<Schema.DateRange>(defaultDates);
const valueRef = useRef<Schema.DateRange>(value);
const [error, setError] = useState<string>('');
const [minStartDate, maxStartDate, minEndDate, maxEndDate] = useMemo(() => {
const minStartDate =
@@ -93,7 +94,7 @@ export const DateRangeFilter = forwardRef(
},
isFilterActive() {
return value.start || value.end;
return valueRef.current.start || valueRef.current.end;
},
getModel() {
@@ -101,13 +102,13 @@ export const DateRangeFilter = forwardRef(
return null;
}
return { value };
return { value: valueRef.current };
},
setModel(model?: { value: Schema.DateRange } | null) {
setValue(
model?.value || props?.defaultRangeFilter || defaultFilterValue
);
valueRef.current =
model?.value || props?.defaultValue || defaultValue;
setValue(valueRef.current);
},
};
});
@@ -185,10 +186,8 @@ export const DateRangeFilter = forwardRef(
update = { ...update, end: checkForEndDate(endDate, startDate) };
if (validate(name, date, update)) {
setValue((curr) => ({
...curr,
...update,
}));
valueRef.current = { ...valueRef.current, ...update };
setValue(valueRef.current);
}
};
useEffect(() => {
@@ -241,7 +240,8 @@ export const DateRangeFilter = forwardRef(
className="ag-standard-button ag-filter-apply-panel-button"
onClick={() => {
setError('');
setValue(defaultDates);
valueRef.current = defaultDates;
setValue(valueRef.current);
}}
>
{t('Reset')}
+17 -11
View File
@@ -1,10 +1,17 @@
import type { ChangeEvent } from 'react';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import {
forwardRef,
useEffect,
useImperativeHandle,
useState,
useRef,
} from 'react';
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n';
export const SetFilter = forwardRef((props: IFilterParams, ref) => {
const [value, setValue] = useState<string[]>([]);
const valueRef = useRef(value);
// expose AG Grid Filter Lifecycle callbacks
useImperativeHandle(ref, () => {
@@ -28,29 +35,28 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
},
isFilterActive() {
return value.length !== 0;
return valueRef.current.length !== 0;
},
getModel() {
if (!this.isFilterActive()) {
return null;
}
return { value };
return { value: valueRef.current };
},
setModel(model?: { value: string[] } | null) {
setValue(!model ? [] : model.value);
valueRef.current = !model ? [] : model.value;
setValue(valueRef.current);
},
};
});
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(
event.target.checked
? [...value, event.target.value]
: value.filter((v) => v !== event.target.value)
);
valueRef.current = event.target.checked
? [...value, event.target.value]
: value.filter((v) => v !== event.target.value);
setValue(valueRef.current);
};
useEffect(() => {
@@ -77,7 +83,7 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
<button
type="button"
className="ag-standard-button ag-filter-apply-panel-button"
onClick={() => setValue([])}
onClick={() => setValue((valueRef.current = []))}
>
{t('Reset')}
</button>
@@ -78,7 +78,9 @@ export const compileGridData = (
label: (
<Link
to={`/liquidity/${market.id}`}
onClick={(ev) => onSelect && onSelect(market.id, ev.metaKey)}
onClick={(ev) =>
onSelect && onSelect(market.id, ev.metaKey || ev.ctrlKey)
}
>
<UILink>{t('Current liquidity')}</UILink>
</Link>
-4
View File
@@ -1,6 +1,2 @@
export * from './__generated__/EstimateOrder';
export * from './use-calculate-slippage';
export * from './use-fee-deal-ticket-details';
export * from './use-market-positions';
export * from './use-maximum-position-size';
export * from './use-order-closeout';
@@ -1,144 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import * as Schema from '@vegaprotocol/types';
import type { Market } from '@vegaprotocol/market-list';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useCalculateSlippage } from './use-calculate-slippage';
const mockData = {
decimalPlaces: 0,
positionDecimalPlaces: 0,
depth: {
buy: [
{
price: '5',
volume: '2',
},
{
price: '4',
volume: '3',
},
{
price: '3',
volume: '2',
},
{
price: '2',
volume: '1',
},
{
price: '1',
volume: '1',
},
],
sell: [
{
price: '6',
volume: '1',
},
{
price: '7',
volume: '3',
},
{
price: '8',
volume: '2',
},
{
price: '9',
volume: '1',
},
{
price: '10',
volume: '2',
},
],
},
};
let mockOrderBookData = {
data: mockData,
};
jest.mock('@vegaprotocol/react-helpers', () => ({
...jest.requireActual('@vegaprotocol/react-helpers'),
useDataProvider: jest.fn(() => ({
data: {
marketsConnection: [],
},
})),
useThrottledDataProvider: jest.fn(() => mockOrderBookData),
}));
describe('useCalculateSlippage Hook', () => {
describe('calculate proper result', () => {
afterEach(() => {
jest.clearAllMocks();
});
const market = {
id: 'marketId',
decimalPlaces: 0,
positionDecimalPlaces: 0,
} as Market;
it('long order', () => {
const { result } = renderHook(
() =>
useCalculateSlippage({
market,
order: {
size: '10',
side: Schema.Side.SIDE_BUY,
} as OrderSubmissionBody['orderSubmission'],
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toEqual('33.33');
});
it('short order', () => {
const { result } = renderHook(
() =>
useCalculateSlippage({
market,
order: {
size: '10',
side: Schema.Side.SIDE_SELL,
} as OrderSubmissionBody['orderSubmission'],
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toEqual('31.11');
});
it('when no order book result should be null', () => {
mockOrderBookData = {
data: {
...mockData,
depth: {
...mockData.depth,
buy: [],
},
},
};
const { result } = renderHook(
() =>
useCalculateSlippage({
market,
order: {
size: '10',
side: Schema.Side.SIDE_SELL,
} as OrderSubmissionBody['orderSubmission'],
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toBeNull();
});
});
});
@@ -1,63 +0,0 @@
import { marketDepthProvider } from '@vegaprotocol/market-depth';
import * as Schema from '@vegaprotocol/types';
import type { Market } from '@vegaprotocol/market-list';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { BigNumber } from 'bignumber.js';
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
import { useThrottledDataProvider } from '@vegaprotocol/react-helpers';
interface Props {
market: Market;
order: OrderSubmissionBody['orderSubmission'];
}
export const useCalculateSlippage = ({ market, order }: Props) => {
const { data } = useThrottledDataProvider(
{
dataProvider: marketDepthProvider,
variables: { marketId: market.id },
},
1000
);
const volPriceArr =
data?.depth[order.side === Schema.Side.SIDE_BUY ? 'sell' : 'buy'] || [];
if (volPriceArr.length && market) {
const decimals = market.decimalPlaces ?? 0;
const positionDecimals = market.positionDecimalPlaces ?? 0;
const bestPrice = toBigNum(volPriceArr[0].price, decimals);
const { size } = order;
let descSize = new BigNumber(size);
let i = 0;
const volPricePairs: Array<[BigNumber, BigNumber]> = [];
while (!descSize.isZero() && i < volPriceArr.length) {
const price = toBigNum(volPriceArr[i].price, decimals);
const amount = BigNumber.min(
descSize,
toBigNum(volPriceArr[i].volume, positionDecimals)
);
volPricePairs.push([price, amount]);
descSize = BigNumber.max(0, descSize.minus(amount));
i++;
}
if (volPricePairs.length) {
const volWeightAvPricePair = volPricePairs.reduce(
(agg, item) => {
agg[0] = agg[0].plus(item[0].multipliedBy(item[1]));
agg[1] = agg[1].plus(item[1]);
return agg;
},
[new BigNumber(0), new BigNumber(0)]
);
const volWeightAvPrice = volWeightAvPricePair[0].dividedBy(
volWeightAvPricePair[1]
);
const slippage = volWeightAvPrice
.minus(bestPrice)
.absoluteValue()
.dividedBy(bestPrice)
.multipliedBy(100);
return formatNumber(slippage, 2);
}
}
return null;
};
@@ -18,7 +18,6 @@ import {
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
TOTAL_MARGIN_AVAILABLE,
} from '../constants';
import { useOrderCloseOut } from './use-order-closeout';
import { useMarketAccountBalance } from '@vegaprotocol/accounts';
import { getDerivedPrice } from '../utils/get-price';
import { useEstimateOrderQuery } from './__generated__/EstimateOrder';
@@ -49,12 +48,6 @@ export const useFeeDealTicketDetails = (
skip: !pubKey || !market || !order.size || !price,
});
const estCloseOut = useOrderCloseOut({
order,
market,
marketData,
});
const notionalSize = useMemo(() => {
if (price && order.size) {
return toBigNum(order.size, market.positionDecimalPlaces)
@@ -74,16 +67,8 @@ export const useFeeDealTicketDetails = (
notionalSize,
accountBalance,
estimateOrder: estMargin?.estimateOrder,
estCloseOut,
};
}, [
market,
assetSymbol,
notionalSize,
accountBalance,
estMargin,
estCloseOut,
]);
}, [market, assetSymbol, notionalSize, accountBalance, estMargin]);
};
export interface FeeDetails {
@@ -92,7 +77,6 @@ export interface FeeDetails {
market: Market;
assetSymbol: string;
notionalSize: string | null;
estCloseOut: string | null;
estimateOrder: EstimateOrderQuery['estimateOrder'] | undefined;
estimatedInitialMargin: string;
estimatedTotalInitialMargin: string;
@@ -15,8 +15,8 @@ export const useInitialMargin = (
marketId: OrderSubmissionBody['orderSubmission']['marketId'],
order?: OrderSubmissionBody['orderSubmission']
) => {
const { pubKey: partyId } = useVegaWallet();
const commonVariables = { marketId, partyId: partyId || '' };
const { pubKey } = useVegaWallet();
const commonVariables = { marketId, partyId: pubKey || '' };
const { data: marketData } = useDataProvider({
dataProvider: marketDataProvider,
variables: { marketId },
@@ -24,7 +24,7 @@ export const useInitialMargin = (
const { data: activeVolumeAndMargin } = useDataProvider({
dataProvider: volumeAndMarginProvider,
variables: commonVariables,
skip: !partyId,
skip: !pubKey,
});
const { data: marketInfo } = useDataProvider({
dataProvider: marketInfoProvider,
@@ -1,53 +0,0 @@
import { renderHook } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { useMarketPositions } from './use-market-positions';
jest.mock('@vegaprotocol/wallet', () => ({
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
}));
let mockMarketAccountBalance: {
accountBalance: string;
accountDecimals: number | null;
} = { accountBalance: '50001000000', accountDecimals: 5 };
jest.mock('@vegaprotocol/accounts', () => ({
...jest.requireActual('@vegaprotocol/accounts'),
useMarketAccountBalance: jest.fn(() => mockMarketAccountBalance),
}));
jest.mock('@vegaprotocol/positions', () => ({
...jest.requireActual('@vegaprotocol/positions'),
useMarketPositionOpenVolume: jest.fn(() => '100002'),
}));
describe('useOrderPosition Hook', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should return proper positive value', () => {
const { result } = renderHook(
() => useMarketPositions({ marketId: 'marketId' }),
{ wrapper: MockedProvider }
);
expect(result.current?.openVolume).toEqual('100002');
expect(result.current?.balance).toEqual('50001000000');
});
it('if balance equal 0 return null', () => {
mockMarketAccountBalance = { accountBalance: '0', accountDecimals: 5 };
const { result } = renderHook(
() => useMarketPositions({ marketId: 'marketId' }),
{ wrapper: MockedProvider }
);
expect(result.current).toBeNull();
});
it('if no markets return null', () => {
mockMarketAccountBalance = { accountBalance: '', accountDecimals: null };
const { result } = renderHook(
() => useMarketPositions({ marketId: 'marketId' }),
{ wrapper: MockedProvider }
);
expect(result.current).toBeNull();
});
});
@@ -1,34 +0,0 @@
import { useMemo } from 'react';
import { BigNumber } from 'bignumber.js';
import { useMarketAccountBalance } from '@vegaprotocol/accounts';
import { useMarketPositionOpenVolume } from '@vegaprotocol/positions';
interface Props {
marketId: string;
}
export type PositionMargin = {
openVolume: string;
balance: string;
balanceDecimals?: number;
} | null;
export const useMarketPositions = ({ marketId }: Props): PositionMargin => {
const { accountBalance, accountDecimals } = useMarketAccountBalance(marketId);
const openVolume = useMarketPositionOpenVolume(marketId);
return useMemo(() => {
if (accountBalance && accountDecimals) {
const balance = new BigNumber(accountBalance);
const volume = new BigNumber(openVolume);
if (!balance.isZero() && !volume.isZero()) {
return {
balance: accountBalance,
balanceDecimals: accountDecimals,
openVolume,
};
}
}
return null;
}, [accountBalance, accountDecimals, openVolume]);
};
@@ -1,120 +0,0 @@
import { renderHook } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import * as Schema from '@vegaprotocol/types';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import type { PositionMargin } from './use-market-positions';
import { useMaximumPositionSize } from './use-maximum-position-size';
jest.mock('@vegaprotocol/wallet', () => ({
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
}));
let mockAccountBalance: {
accountBalance: string;
accountDecimals: number | null;
} = { accountBalance: '200000', accountDecimals: 5 };
jest.mock('@vegaprotocol/accounts', () => ({
...jest.requireActual('@vegaprotocol/accounts'),
useAccountBalance: jest.fn(() => mockAccountBalance),
}));
const defaultMockMarketPositions = {
openVolume: '1',
balance: '100000',
};
let mockMarketPositions: PositionMargin | null = defaultMockMarketPositions;
const mockOrder: OrderSubmissionBody['orderSubmission'] = {
type: Schema.OrderType.TYPE_MARKET,
size: '1',
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
marketId: 'market-id',
};
jest.mock('./use-market-positions', () => ({
useMarketPositions: ({
marketId,
partyId,
}: {
marketId: string;
partyId: string;
}) => mockMarketPositions,
}));
describe('useMaximumPositionSize', () => {
it('should return correct size when no open positions', () => {
mockMarketPositions = null;
const price = '50';
const expected = 4000;
const { result } = renderHook(
() =>
useMaximumPositionSize({
marketId: '',
price,
settlementAssetId: '',
order: mockOrder,
}),
{ wrapper: MockedProvider }
);
expect(result.current).toBe(expected);
});
it('should return correct size when open positions and same side', () => {
const price = '50';
mockMarketPositions = defaultMockMarketPositions;
const expected = 3999;
const { result } = renderHook(
() =>
useMaximumPositionSize({
marketId: '',
price,
settlementAssetId: '',
order: mockOrder,
}),
{ wrapper: MockedProvider }
);
expect(result.current).toBe(expected);
});
it('should return correct size when open positions and opposite side', () => {
const price = '50';
mockOrder.side = Schema.Side.SIDE_SELL;
mockMarketPositions = defaultMockMarketPositions;
const expected = 4001;
const { result } = renderHook(
() =>
useMaximumPositionSize({
marketId: '',
price,
settlementAssetId: '',
order: mockOrder,
}),
{ wrapper: MockedProvider }
);
expect(result.current).toBe(expected);
});
it('should return zero if no account balance', () => {
mockAccountBalance = {
accountBalance: '0',
accountDecimals: 5,
};
const price = '50';
mockMarketPositions = defaultMockMarketPositions;
const expected = 0;
const { result } = renderHook(
() =>
useMaximumPositionSize({
marketId: '',
price,
settlementAssetId: '',
order: mockOrder,
}),
{ wrapper: MockedProvider }
);
expect(result.current).toBe(expected);
});
});
@@ -1,46 +0,0 @@
import * as Schema from '@vegaprotocol/types';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useAccountBalance } from '@vegaprotocol/accounts';
import { BigNumber } from 'bignumber.js';
import { useMarketPositions } from './use-market-positions';
interface Props {
marketId: string;
price?: string;
settlementAssetId: string;
order: OrderSubmissionBody['orderSubmission'];
}
const getSize = (balance: string, price: string) =>
new BigNumber(balance).dividedBy(new BigNumber(price));
export const useMaximumPositionSize = ({
marketId,
price,
settlementAssetId,
order,
}: Props): number => {
const { accountBalance } = useAccountBalance(settlementAssetId) || {};
const marketPositions = useMarketPositions({ marketId: marketId });
if (!accountBalance || new BigNumber(accountBalance || 0).isZero()) {
return 0;
}
const size = getSize(accountBalance, price || '');
if (!marketPositions) {
return size.toNumber() || 0;
}
const isSameSide =
(new BigNumber(marketPositions.openVolume).isPositive() &&
order.side === Schema.Side.SIDE_BUY) ||
(new BigNumber(marketPositions.openVolume).isNegative() &&
order.side === Schema.Side.SIDE_SELL);
const adjustedForVolume = new BigNumber(size)[isSameSide ? 'minus' : 'plus'](
marketPositions.openVolume
);
return adjustedForVolume.isNegative() ? 0 : adjustedForVolume.toNumber();
};
@@ -1,117 +0,0 @@
import { renderHook } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import type { Market, MarketData } from '@vegaprotocol/market-list';
import { useOrderCloseOut } from './use-order-closeout';
jest.mock('@vegaprotocol/wallet', () => ({
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
}));
let mockMarketMargin: string | undefined = undefined;
jest.mock('@vegaprotocol/positions', () => ({
...jest.requireActual('@vegaprotocol/positions'),
useMarketMargin: () => mockMarketMargin,
}));
describe('useOrderCloseOut', () => {
const order = { size: '2', side: 'SIDE_BUY' };
const market = {
decimalPlaces: 5,
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'assetId',
},
},
},
},
} as unknown as Market;
const marketData = {
markPrice: 100000,
} as unknown as MarketData;
beforeEach(() => {
jest.clearAllMocks();
});
it('should return proper null value', () => {
mockMarketMargin = '-1';
const { result } = renderHook(
() =>
useOrderCloseOut({
order: order as OrderSubmissionBody['orderSubmission'],
market,
marketData: {
markPrice: '0',
} as MarketData,
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toEqual(null);
});
it('should return proper sell value', () => {
mockMarketMargin = '0';
const { result } = renderHook(
() =>
useOrderCloseOut({
order: {
...order,
side: 'SIDE_SELL',
} as OrderSubmissionBody['orderSubmission'],
market,
marketData,
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toEqual('1');
});
it('should return proper sell value on limit order', () => {
mockMarketMargin = '0';
const { result } = renderHook(
() =>
useOrderCloseOut({
order: {
...order,
price: '1000000',
type: 'TYPE_LIMIT',
side: 'SIDE_SELL',
} as OrderSubmissionBody['orderSubmission'],
market,
marketData,
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toEqual('1000000');
});
it('should return proper empty value', () => {
const { result } = renderHook(
() =>
useOrderCloseOut({
order: {
...order,
side: 'SIDE_SELL',
} as OrderSubmissionBody['orderSubmission'],
market,
marketData: {
markPrice: '0',
} as MarketData,
}),
{
wrapper: MockedProvider,
}
);
expect(result.current).toEqual('0');
});
});
@@ -1,61 +0,0 @@
import { BigNumber } from 'bignumber.js';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { addDecimal } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import type { Market, MarketData } from '@vegaprotocol/market-list';
import {
useAccountBalance,
useMarketAccountBalance,
} from '@vegaprotocol/accounts';
import { useMarketMargin } from '@vegaprotocol/positions';
import { useMarketPositions } from './use-market-positions';
interface Props {
order: OrderSubmissionBody['orderSubmission'];
market: Market;
marketData: MarketData;
}
export const useOrderCloseOut = ({
order,
market,
marketData,
}: Props): string | null => {
const { accountBalance, accountDecimals } = useAccountBalance(
market.tradableInstrument.instrument.product.settlementAsset.id
);
const { accountBalance: positionBalance, accountDecimals: positionDecimals } =
useMarketAccountBalance(market.id);
const maintenanceLevel = useMarketMargin(market.id);
const marginMaintenanceLevel = new BigNumber(
addDecimal(maintenanceLevel || 0, market.decimalPlaces)
);
const positionAccountBalance = new BigNumber(
addDecimal(positionBalance || 0, positionDecimals || 0)
);
const generalAccountBalance = new BigNumber(
addDecimal(accountBalance || 0, accountDecimals || 0)
);
const { openVolume } =
useMarketPositions({
marketId: market.id,
}) || {};
const volume = new BigNumber(
addDecimal(openVolume || '0', market.positionDecimalPlaces)
)[order.side === Schema.Side.SIDE_BUY ? 'plus' : 'minus'](order.size);
const price =
order.type === Schema.OrderType.TYPE_LIMIT && order.price
? new BigNumber(order.price)
: new BigNumber(addDecimal(marketData.markPrice, market.decimalPlaces));
// regarding formula (marginMaintenanceLevel - positionAccountBalance - generalAccountBalance) / volume + markPrice
const marginDifference = marginMaintenanceLevel
.minus(positionAccountBalance)
.minus(generalAccountBalance);
const closeOut = marginDifference.div(volume).plus(price);
if (closeOut.isPositive()) {
return closeOut.toString();
}
return null;
};
+14 -15
View File
@@ -51,11 +51,12 @@ export const ApproveNotification = ({
intent={intent}
testId="approve-default"
message={t(
`Before you can make a deposit of your chosen asset, ${selectedAsset?.symbol}, you need to approve its use in your Ethereum wallet`
'Before you can make a deposit of your chosen asset, %s, you need to approve its use in your Ethereum wallet',
selectedAsset?.symbol
)}
buttonProps={{
size: 'sm',
text: `Approve ${selectedAsset?.symbol}`,
text: t('Approve %s', selectedAsset?.symbol),
action: onApprove,
dataTestId: 'approve-submit',
}}
@@ -68,13 +69,12 @@ export const ApproveNotification = ({
intent={intent}
testId="reapprove-default"
message={t(
`Approve again to deposit more than ${formatNumber(
balances.allowance.toString()
)}`
'Approve again to deposit more than %s',
formatNumber(balances.allowance.toString())
)}
buttonProps={{
size: 'sm',
text: `Approve ${selectedAsset?.symbol}`,
text: t('Approve %s', selectedAsset?.symbol),
action: onApprove,
dataTestId: 'reapprove-submit',
}}
@@ -157,7 +157,8 @@ const ApprovalTxFeedback = ({
intent={Intent.Warning}
testId="approve-requested"
message={t(
`Go to your Ethereum wallet and approve the transaction to enable the use of ${selectedAsset?.symbol}`
'Go to your Ethereum wallet and approve the transaction to enable the use of %s',
selectedAsset?.symbol
)}
/>
</div>
@@ -174,7 +175,8 @@ const ApprovalTxFeedback = ({
<>
<p>
{t(
`Your ${selectedAsset?.symbol} approval is being confirmed by the Ethereum network. When this is complete, you can continue your deposit`
'Your %s approval is being confirmed by the Ethereum network. When this is complete, you can continue your deposit',
selectedAsset?.symbol
)}{' '}
</p>
{txLink && <p>{txLink}</p>}
@@ -194,13 +196,10 @@ const ApprovalTxFeedback = ({
message={
<>
<p>
{t(
`You can now make deposits in ${
selectedAsset?.symbol
}, up to a maximum of ${formatNumber(
allowance?.toString() || 0
)}`
)}
{t('You approved deposits of up to %s %s.', [
selectedAsset?.symbol,
formatNumber(allowance?.toString() || 0),
])}
</p>
{txLink && <p>{txLink}</p>}
</>
+47 -7
View File
@@ -1,4 +1,11 @@
import { waitFor, fireEvent, render, screen } from '@testing-library/react';
import {
waitFor,
fireEvent,
render,
screen,
act,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BigNumber from 'bignumber.js';
import type { DepositFormProps } from './deposit-form';
import { DepositForm } from './deposit-form';
@@ -61,6 +68,7 @@ beforeEach(() => {
submitDeposit: jest.fn(),
submitFaucet: jest.fn(),
onDisconnect: jest.fn(),
handleAmountChange: jest.fn(),
approveTxId: null,
faucetTxId: null,
isFaucetable: true,
@@ -140,12 +148,14 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Insufficient amount in Ethereum wallet')
await screen.findByText(
"You can't deposit more than you have in your Ethereum wallet, 5"
)
).toBeInTheDocument();
});
it('fails when submitted amount is more than the maximum limit', async () => {
render(<DepositForm {...props} />);
render(<DepositForm {...props} selectedAsset={asset} />);
const amountMoreThanLimit = '21';
fireEvent.change(screen.getByLabelText('Amount'), {
@@ -154,7 +164,9 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Amount is above lifetime deposit limit')
await screen.findByText(
"You can't deposit more than your remaining deposit allowance, 10 asset-symbol"
)
).toBeInTheDocument();
});
@@ -178,7 +190,9 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Amount is above approved amount')
await screen.findByText(
"You can't deposit more than your approved deposit amount, 30"
)
).toBeInTheDocument();
});
@@ -282,8 +296,6 @@ describe('Deposit form', () => {
expect(screen.getByTestId('BALANCE_AVAILABLE_value')).toHaveTextContent(
'50'
);
expect(screen.getByTestId('MAX_LIMIT_value')).toHaveTextContent('20');
expect(screen.getByTestId('DEPOSITED_value')).toHaveTextContent('10');
expect(screen.getByTestId('REMAINING_value')).toHaveTextContent('10');
fireEvent.change(screen.getByLabelText('Amount'), {
@@ -364,4 +376,32 @@ describe('Deposit form', () => {
/this app only works on/i
);
});
it('Remaining deposit allowance tooltip should be rendered', async () => {
render(<DepositForm {...props} selectedAsset={asset} />);
await act(async () => {
await userEvent.hover(screen.getByText('Remaining deposit allowance'));
});
await waitFor(async () => {
await expect(
screen.getByRole('tooltip', {
name: /VEGA has a lifetime deposit limit of 20 asset-symbol per address/,
})
).toBeInTheDocument();
});
});
it('Ethereum deposit cap tooltip should be rendered', async () => {
render(<DepositForm {...props} selectedAsset={asset} />);
await act(async () => {
await userEvent.hover(screen.getByText('Ethereum deposit cap'));
});
await waitFor(async () => {
await expect(
screen.getByRole('tooltip', {
name: /The deposit cap is set when you approve an asset for use with this app/,
})
).toBeInTheDocument();
});
});
});
+81 -60
View File
@@ -8,6 +8,7 @@ import {
maxSafe,
addDecimal,
isAssetTypeERC20,
formatNumber,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
@@ -25,11 +26,9 @@ import {
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
import BigNumber from 'bignumber.js';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useState } from 'react';
import { useMemo } from 'react';
import { useWatch } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import type { ButtonHTMLAttributes, ChangeEvent, ReactNode } from 'react';
import { useMemo, useState } from 'react';
import { useWatch, Controller, useForm } from 'react-hook-form';
import { DepositLimits } from './deposit-limits';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import {
@@ -40,6 +39,7 @@ import {
import type { DepositBalances } from './use-deposit-balances';
import { FaucetNotification } from './faucet-notification';
import { ApproveNotification } from './approve-notification';
import { usePersistentDeposit } from './use-persistent-deposit';
interface FormFields {
asset: string;
@@ -53,6 +53,7 @@ export interface DepositFormProps {
selectedAsset?: Asset;
balances: DepositBalances | null;
onSelectAsset: (assetId: string) => void;
handleAmountChange: (amount: string) => void;
onDisconnect: () => void;
submitApprove: () => void;
approveTxId: number | null;
@@ -71,6 +72,7 @@ export const DepositForm = ({
selectedAsset,
balances,
onSelectAsset,
handleAmountChange,
onDisconnect,
submitApprove,
submitDeposit,
@@ -85,6 +87,8 @@ export const DepositForm = ({
const { pubKey, pubKeys: _pubKeys } = useVegaWallet();
const [approveNotificationIntent, setApproveNotificationIntent] =
useState<Intent>(Intent.Warning);
const [persistedDeposit] = usePersistentDeposit(selectedAsset?.id);
const {
register,
handleSubmit,
@@ -95,7 +99,8 @@ export const DepositForm = ({
} = useForm<FormFields>({
defaultValues: {
to: pubKey ? pubKey : undefined,
asset: selectedAsset?.id || '',
asset: selectedAsset?.id,
amount: persistedDeposit?.amount,
},
});
@@ -129,11 +134,8 @@ export const DepositForm = ({
return _pubKeys ? _pubKeys.map((pk) => pk.publicKey) : [];
}, [_pubKeys]);
const approved = balances
? balances.allowance.isGreaterThan(0)
? true
: false
: false;
const approved =
balances && balances.allowance.isGreaterThan(0) ? true : false;
return (
<form
@@ -190,6 +192,44 @@ export const DepositForm = ({
<InputError intent="danger">{errors.from.message}</InputError>
)}
</FormGroup>
<FormGroup label={t('To (Vega key)')} labelFor="to">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('to', '')}
select={
<Select {...register('to')} id="to" defaultValue="">
<option value="" disabled>
{t('Please select')}
</option>
{pubKeys?.length &&
pubKeys.map((pk) => (
<option key={pk} value={pk}>
{pk}
</option>
))}
</Select>
}
input={
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to"
type="text"
{...register('to', {
validate: {
required,
vegaPublicKey,
},
})}
/>
}
/>
{errors.to?.message && (
<InputError intent="danger" forInput="to">
{errors.to.message}
</InputError>
)}
</FormGroup>
<FormGroup label={t('Asset')} labelFor="asset">
<Controller
control={control}
@@ -246,45 +286,7 @@ export const DepositForm = ({
selectedAsset={selectedAsset}
faucetTxId={faucetTxId}
/>
<FormGroup label={t('To (Vega key)')} labelFor="to">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('to', '')}
select={
<Select {...register('to')} id="to" defaultValue="">
<option value="" disabled={true}>
{t('Please select')}
</option>
{pubKeys?.length &&
pubKeys.map((pk) => (
<option key={pk} value={pk}>
{pk}
</option>
))}
</Select>
}
input={
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to"
type="text"
{...register('to', {
validate: {
required,
vegaPublicKey,
},
})}
/>
}
/>
{errors.to?.message && (
<InputError intent="danger" forInput="to">
{errors.to.message}
</InputError>
)}
</FormGroup>
{selectedAsset && balances && (
{approved && selectedAsset && balances && (
<div className="mb-6">
<DepositLimits {...balances} asset={selectedAsset} />
</div>
@@ -301,8 +303,15 @@ export const DepositForm = ({
minSafe: (value) => minSafe(new BigNumber(min))(value),
approved: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(balances?.allowance || 0)) {
return t('Amount is above approved amount');
const allowance = new BigNumber(balances?.allowance || 0);
if (value.isGreaterThan(allowance)) {
return t(
"You can't deposit more than your approved deposit amount, %s %s",
[
formatNumber(allowance.toString()),
selectedAsset?.symbol || ' ',
]
);
}
return true;
},
@@ -318,14 +327,24 @@ export const DepositForm = ({
}
if (value.isGreaterThan(lifetimeLimit)) {
return t('Amount is above lifetime deposit limit');
return t(
"You can't deposit more than your remaining deposit allowance, %s %s",
[
formatNumber(lifetimeLimit.toString()),
selectedAsset?.symbol || ' ',
]
);
}
return true;
},
balance: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(balances?.balance || 0)) {
return t('Insufficient amount in Ethereum wallet');
const balance = new BigNumber(balances?.balance || 0);
if (value.isGreaterThan(balance)) {
return t(
"You can't deposit more than you have in your Ethereum wallet, %s %s",
[formatNumber(balance), selectedAsset?.symbol || ' ']
);
}
return true;
},
@@ -333,6 +352,9 @@ export const DepositForm = ({
return maxSafe(balances?.balance || new BigNumber(0))(v);
},
},
onChange: (e: ChangeEvent<HTMLInputElement>) => {
handleAmountChange(e.target.value || '');
},
})}
/>
{errors.amount?.message && (
@@ -343,10 +365,9 @@ export const DepositForm = ({
{selectedAsset && balances && (
<UseButton
onClick={() => {
setValue(
'amount',
balances.balance.toFixed(selectedAsset.decimals)
);
const amount = balances.balance.toFixed(selectedAsset.decimals);
setValue('amount', amount);
handleAmountChange(amount);
clearErrors('amount');
}}
>
@@ -399,7 +420,7 @@ const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
type="submit"
data-testid="deposit-submit"
variant={isActive ? 'primary' : 'default'}
fill={true}
fill
disabled={invalidChain}
>
{t('Deposit')}
+47 -15
View File
@@ -1,8 +1,13 @@
import type { Asset } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { CompactNumber } from '@vegaprotocol/react-helpers';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import {
KeyValueTable,
KeyValueTableRow,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import type BigNumber from 'bignumber.js';
import { formatNumber } from '@vegaprotocol/utils';
// Note: all of the values here are with correct asset's decimals
// See `libs/deposits/src/lib/use-deposit-balances.ts`
@@ -33,21 +38,35 @@ export const DepositLimits = ({
'-'
),
},
{
key: 'MAX_LIMIT',
label: t('Lifetime deposit allowance'),
rawValue: max,
value: <CompactNumber number={max} decimals={asset.decimals} />,
},
{
key: 'DEPOSITED',
label: t('Deposited'),
rawValue: deposited,
value: <CompactNumber number={deposited} decimals={asset.decimals} />,
},
{
key: 'REMAINING',
label: t('Remaining'),
label: (
<Tooltip
description={
<>
<p>
{t(
'VEGA has a lifetime deposit limit of %s %s per address. This can be changed through governance',
[formatNumber(max.toString()), asset.symbol]
)}
</p>
<p>
{t(
'To date, %s %s has been deposited from this Ethereum address, so you can deposit up to %s %s more.',
[
formatNumber(deposited.toString()),
asset.symbol,
formatNumber(max.minus(deposited).toString()),
asset.symbol,
]
)}
</p>
</>
}
>
<button type="button">{t('Remaining deposit allowance')}</button>
</Tooltip>
),
rawValue: max.minus(deposited),
value: (
<CompactNumber
@@ -58,7 +77,20 @@ export const DepositLimits = ({
},
{
key: 'ALLOWANCE',
label: t('Approved'),
label: (
<Tooltip
description={
<p>
{t(
'The deposit cap is set when you approve an asset for use with this app. To increase this cap, approve %s again and choose a higher cap. Check the documentation for your Ethereum wallet app for details.',
asset.symbol
)}
</p>
}
>
<button type="button">{t('Ethereum deposit cap')}</button>
</Tooltip>
),
rawValue: allowance,
value: allowance ? (
<CompactNumber number={allowance} decimals={asset.decimals} />
+15 -2
View File
@@ -5,7 +5,7 @@ import { prepend0x } from '@vegaprotocol/smart-contracts';
import sortBy from 'lodash/sortBy';
import { useSubmitApproval } from './use-submit-approval';
import { useSubmitFaucet } from './use-submit-faucet';
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { useDepositBalances } from './use-deposit-balances';
import { useDepositDialog } from './deposit-dialog';
import type { Asset } from '@vegaprotocol/assets';
@@ -14,6 +14,7 @@ import {
useBridgeContract,
useEthereumConfig,
} from '@vegaprotocol/web3';
import { usePersistentDeposit } from './use-persistent-deposit';
interface DepositManagerProps {
assetId?: string;
@@ -28,7 +29,9 @@ export const DepositManager = ({
}: DepositManagerProps) => {
const createEthTransaction = useEthTransactionStore((state) => state.create);
const { config } = useEthereumConfig();
const [assetId, setAssetId] = useState(initialAssetId);
const [persistentDeposit, savePersistentDeposit] =
usePersistentDeposit(initialAssetId);
const [assetId, setAssetId] = useState(persistentDeposit?.assetId);
const asset = assets.find((a) => a.id === assetId);
const bridgeContract = useBridgeContract();
const closeDepositDialog = useDepositDialog((state) => state.close);
@@ -65,17 +68,27 @@ export const DepositManager = ({
closeDepositDialog();
};
const onAmountChange = useCallback(
(amount: string) => {
persistentDeposit &&
savePersistentDeposit({ ...persistentDeposit, amount });
},
[savePersistentDeposit, persistentDeposit]
);
return (
<DepositForm
selectedAsset={asset}
onDisconnect={reset}
onSelectAsset={(id) => {
setAssetId(id);
savePersistentDeposit({ assetId: id });
// When we change asset, also clear the tracked faucet/approve transactions so
// we dont render stale UI
approve.reset();
faucet.reset();
}}
handleAmountChange={onAmountChange}
assets={sortBy(assets, 'name')}
submitApprove={approve.perform}
submitDeposit={submitDeposit}
@@ -0,0 +1,21 @@
import { renderHook, waitFor, act } from '@testing-library/react';
import { usePersistentDeposit } from './use-persistent-deposit';
describe('usePersistenDeposit', () => {
it('should return empty data', () => {
const { result } = renderHook(() => usePersistentDeposit());
expect(result.current).toEqual([undefined, expect.any(Function)]);
});
it('should return empty and properly saved data', async () => {
const aId = 'test';
const retObj = { assetId: 'test', amount: '1.00000' };
const { result } = renderHook(() => usePersistentDeposit(aId));
expect(result.current).toEqual([{ assetId: 'test' }, expect.any(Function)]);
await act(() => {
result.current[1](retObj);
});
await waitFor(() => {
expect(result.current[0]).toEqual(retObj);
});
});
});
@@ -0,0 +1,46 @@
import { useMemo } from 'react';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
const STORAGE_KEY = 'vega_deposit_store';
interface PersistedDeposit {
assetId: string;
amount?: string;
}
type PersistedDepositData = Record<string, PersistedDeposit>;
const usePersistentDepositStore = create<{
deposits: PersistedDepositData;
saveValue: (entry: PersistedDeposit) => void;
lastVisited?: PersistedDeposit;
}>()(
persist(
immer((set) => ({
deposits: {},
saveValue: (entry) =>
set((state) => {
const oldValue = state.deposits[entry.assetId] || null;
state.deposits[entry.assetId] = { ...oldValue, ...entry };
state.lastVisited = { ...oldValue, ...entry };
return state;
}),
})),
{ name: STORAGE_KEY }
)
);
export const usePersistentDeposit = (
assetId?: string
): [PersistedDeposit | undefined, (entry: PersistedDeposit) => void] => {
const { deposits, lastVisited, saveValue } = usePersistentDepositStore();
const discoveredData = useMemo(() => {
return assetId
? deposits[assetId]
? deposits[assetId]
: { assetId }
: lastVisited;
}, [deposits, lastVisited, assetId]);
return [discoveredData, saveValue];
};
+3 -3
View File
@@ -1,4 +1,5 @@
import { isAssetTypeERC20, removeDecimal } from '@vegaprotocol/utils';
import { MaxUint256 } from '@ethersproject/constants';
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
import {
EthTxStatus,
useEthereumConfig,
@@ -37,10 +38,9 @@ export const useSubmitApproval = (
},
perform: () => {
if (!asset || !config) return;
const amount = removeDecimal('1000000', asset.decimals);
const id = createEthTransaction(contract, 'approve', [
config?.collateral_bridge_contract.address,
amount,
MaxUint256.toString(),
]);
setId(id);
},
+3
View File
@@ -1 +1,4 @@
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
global.ResizeObserver = ResizeObserver;
@@ -1,6 +1,6 @@
import type { MockedResponse } from '@apollo/react-testing';
import { MockedProvider } from '@apollo/react-testing';
import { render, screen, waitFor } from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import { RadioGroup } from '@vegaprotocol/ui-toolkit';
import type {
BlockTimeSubscription,
@@ -9,6 +9,7 @@ import type {
import { BlockTimeDocument } from '../../utils/__generated__/Node';
import { StatisticsDocument } from '../../utils/__generated__/Node';
import type { RowDataProps } from './row-data';
import { POLL_INTERVAL } from './row-data';
import { BLOCK_THRESHOLD, RowData } from './row-data';
import type { HeaderEntry } from '@vegaprotocol/apollo-client';
import { useHeaderStore } from '@vegaprotocol/apollo-client';
@@ -25,7 +26,7 @@ const statsQueryMock: MockedResponse<StatisticsQuery> = {
result: {
data: {
statistics: {
blockHeight: '1234',
blockHeight: '1234', // the actual value used in the component is the value from the header store
vegaTime: new Date().toISOString(),
chainId: 'test-chain-id',
},
@@ -251,4 +252,105 @@ describe('RowData', () => {
expect(mockOnBlockHeight).toHaveBeenCalledWith(blockHeight);
});
it('should poll the query unless an errors is returned', async () => {
jest.useFakeTimers();
const createStatsQueryMock = (
blockHeight: string
): MockedResponse<StatisticsQuery> => {
return {
request: {
query: StatisticsDocument,
},
result: {
data: {
statistics: {
blockHeight,
vegaTime: new Date().toISOString(),
chainId: 'test-chain-id',
},
},
},
};
};
const createFailedStatsQueryMock = (): MockedResponse<StatisticsQuery> => {
return {
request: {
query: StatisticsDocument,
},
result: {
data: undefined,
},
error: new Error('failed'),
};
};
mockHeaders(props.url);
const statsQueryMock1 = createStatsQueryMock('1234');
const statsQueryMock2 = createStatsQueryMock('1235');
const statsQueryMock3 = createFailedStatsQueryMock();
const statsQueryMock4 = createStatsQueryMock('1236');
render(
<MockedProvider
mocks={[
statsQueryMock1,
statsQueryMock2,
statsQueryMock3,
statsQueryMock4,
subMock,
]}
>
<RadioGroup>
{/* Radio group required as radio is being render in isolation */}
<RowData {...props} />
</RadioGroup>
</MockedProvider>
);
expect(screen.getByTestId('block-height-cell')).toHaveTextContent(
'Checking'
);
// statsQueryMock1 should be rendered
await waitFor(() => {
const elem = screen.getByTestId('query-block-height');
expect(elem).toHaveAttribute('data-query-block-height', '1234');
});
await act(async () => {
jest.advanceTimersByTime(POLL_INTERVAL);
});
// statsQueryMock2 should be rendered
await waitFor(() => {
const elem = screen.getByTestId('query-block-height');
expect(elem).toHaveAttribute('data-query-block-height', '1235');
});
await act(async () => {
jest.advanceTimersByTime(POLL_INTERVAL);
});
// statsQueryMock3 should FAIL!
await waitFor(() => {
const elem = screen.getByTestId('query-block-height');
expect(elem).toHaveAttribute('data-query-block-height', 'failed');
});
// run the timer again, but statsQueryMock4's result should not be
// rendered even though its successful, because the poll
// should have been stopped
await act(async () => {
jest.advanceTimersByTime(POLL_INTERVAL);
});
// should still render the result of statsQueryMock3
await waitFor(() => {
const elem = screen.getByTestId('query-block-height');
expect(elem).toHaveAttribute('data-query-block-height', 'failed');
});
jest.useRealTimers();
});
});
@@ -11,7 +11,7 @@ import {
} from '../../utils/__generated__/Node';
import { LayoutCell } from './layout-cell';
const POLL_INTERVAL = 1000;
export const POLL_INTERVAL = 1000;
export const BLOCK_THRESHOLD = 3;
export interface RowDataProps {
@@ -60,18 +60,22 @@ export const RowData = ({
// handle polling
useEffect(() => {
const handleStartPoll = () => startPolling(POLL_INTERVAL);
const handleStartPoll = () => {
if (error) return;
startPolling(POLL_INTERVAL);
};
const handleStopPoll = () => stopPolling();
// start polling on mount, but only if there is no error
if (error) {
handleStopPoll();
} else {
handleStartPoll();
}
window.addEventListener('blur', handleStopPoll);
window.addEventListener('focus', handleStartPoll);
handleStartPoll();
if (error) {
stopPolling();
}
return () => {
window.removeEventListener('blur', handleStopPoll);
window.removeEventListener('focus', handleStartPoll);
@@ -81,6 +85,7 @@ export const RowData = ({
// measure response time
useEffect(() => {
if (!isValidUrl(url)) return;
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
// every time we get data measure response speed
const requestUrl = new URL(url);
const requests = window.performance.getEntriesByName(requestUrl.href);
@@ -177,7 +182,14 @@ export const RowData = ({
hasError={getHasError()}
dataTestId="block-height-cell"
>
{getBlockDisplayValue(headers?.blockHeight, error)}
<span
data-testid="query-block-height"
data-query-block-height={
error ? 'failed' : data?.statistics.blockHeight
}
>
{getBlockDisplayValue(headers?.blockHeight, error)}
</span>
</LayoutCell>
<LayoutCell
label={t('Subscription')}
+2 -2
View File
@@ -38,10 +38,10 @@ export const TransferTooltipCellComponent = ({
);
};
const defaultRangeFilter = { start: formatRFC3339(subDays(Date.now(), 7)) };
const defaultValue = { start: formatRFC3339(subDays(Date.now(), 7)) };
const dateRangeFilterParams = {
maxNextDays: 0,
defaultRangeFilter,
defaultValue,
};
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
+15 -20
View File
@@ -1,8 +1,6 @@
import React from 'react';
import throttle from 'lodash/throttle';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { Orderbook } from './orderbook';
import { addDecimal } from '@vegaprotocol/utils';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { marketDepthProvider } from './market-depth-provider';
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
@@ -48,12 +46,12 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
throttle(() => {
dataRef.current = {
...marketDataRef.current,
indicativePrice: marketDataRef.current?.indicativePrice
? getPriceLevel(
marketDataRef.current.indicativePrice,
resolutionRef.current
)
: undefined,
indicativePrice:
marketDataRef.current?.indicativePrice &&
getPriceLevel(
marketDataRef.current.indicativePrice,
resolutionRef.current
),
midPrice: getMidPrice(
rawDataRef.current?.depth.sell,
rawDataRef.current?.depth.buy,
@@ -148,13 +146,12 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
variables,
});
marketDataRef.current = marketData;
if (!marketDataRef.current && marketData) {
marketDataRef.current = marketData;
}
useEffect(() => {
const throttleRunner = updateOrderbookData.current;
if (!marketDataRef.current) {
return;
}
if (!data) {
dataRef.current = { rows: null };
setOrderbookData(dataRef.current);
@@ -162,10 +159,9 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
}
dataRef.current = {
...marketDataRef.current,
indicativePrice: getPriceLevel(
marketDataRef.current.indicativePrice,
resolution
),
indicativePrice:
marketDataRef.current?.indicativePrice &&
getPriceLevel(marketDataRef.current.indicativePrice, resolution),
midPrice: getMidPrice(data.depth.sell, data.depth.buy, resolution),
rows: compactRows(data.depth.sell, data.depth.buy, resolution),
};
@@ -175,7 +171,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
return () => {
throttleRunner.cancel();
};
}, [data, marketData, resolution]);
}, [data, resolution]);
useEffect(() => {
resolutionRef.current = resolution;
@@ -197,10 +193,9 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
positionDecimalPlaces={market?.positionDecimalPlaces ?? 0}
resolution={resolution}
onResolutionChange={(resolution: number) => setResolution(resolution)}
onClick={(price?: string | number) => {
onClick={(price: string) => {
if (price) {
const priceValue = addDecimal(price, market?.decimalPlaces ?? 0);
updateOrder(marketId, { price: priceValue });
updateOrder(marketId, { price });
}
}}
/>
+3 -3
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
import { addDecimal, addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
import { PriceCell, VolCell, CumulativeVol } from '@vegaprotocol/datagrid';
interface OrderbookRowProps {
@@ -15,7 +15,7 @@ interface OrderbookRowProps {
price: string;
relativeAsk?: number;
relativeBid?: number;
onClick?: (price?: string | number) => void;
onClick?: (price: string) => void;
}
export const OrderbookRow = React.memo(
@@ -59,7 +59,7 @@ export const OrderbookRow = React.memo(
<PriceCell
testId={`price-${price}`}
value={BigInt(price)}
onClick={() => onClick && onClick(price)}
onClick={() => onClick && onClick(addDecimal(price, decimalPlaces))}
valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)}
/>
<CumulativeVol
+50 -9
View File
@@ -20,7 +20,7 @@ describe('Orderbook', () => {
const decimalPlaces = 3;
it('should scroll to mid price on init', async () => {
window.innerHeight = 11 * rowHeight;
const result = render(
render(
<Orderbook
decimalPlaces={decimalPlaces}
positionDecimalPlaces={0}
@@ -30,7 +30,7 @@ describe('Orderbook', () => {
/>
);
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
});
it('should keep mid price row in the middle', async () => {
@@ -45,7 +45,7 @@ describe('Orderbook', () => {
/>
);
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
result.rerender(
<Orderbook
decimalPlaces={decimalPlaces}
@@ -121,7 +121,7 @@ describe('Orderbook', () => {
/>
);
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 0.01);
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 0.01);
});
it('should get back to mid price on click', async () => {
@@ -143,7 +143,7 @@ describe('Orderbook', () => {
expect(result.getByTestId('scroll').scrollTop).toBe(1);
const scrollToMidPriceButton = result.getByTestId('scroll-to-midprice');
fireEvent.click(scrollToMidPriceButton);
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 1);
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 1);
});
it('should get back to mid price on resolution change', async () => {
@@ -158,12 +158,12 @@ describe('Orderbook', () => {
/>
);
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
const scrollElement = result.getByTestId('scroll');
const scrollElement = screen.getByTestId('scroll');
expect(scrollElement.scrollTop).toBe(91 * rowHeight);
scrollElement.scrollTop = 1;
fireEvent.scroll(scrollElement);
expect(result.getByTestId('scroll').scrollTop).toBe(1);
const resolutionSelect = result.getByTestId(
expect(screen.getByTestId('scroll').scrollTop).toBe(1);
const resolutionSelect = screen.getByTestId(
'resolution'
) as HTMLSelectElement;
fireEvent.change(resolutionSelect, { target: { value: '10' } });
@@ -181,6 +181,47 @@ describe('Orderbook', () => {
onResolutionChange={onResolutionChange}
/>
);
expect(result.getByTestId('scroll').scrollTop).toBe(6 * rowHeight);
expect(screen.getByTestId('scroll').scrollTop).toBe(6 * rowHeight);
});
it('should format correctly the numbers on resolution change', async () => {
const onClickSpy = jest.fn();
const result = render(
<Orderbook
decimalPlaces={decimalPlaces}
positionDecimalPlaces={0}
onClick={onClickSpy}
fillGaps
{...generateMockData(params)}
onResolutionChange={onResolutionChange}
/>
);
expect(
await screen.findByTestId(`bid-vol-${params.midPrice}`)
).toBeInTheDocument();
// Before resolution change the price is 122.934
await fireEvent.click(await screen.getByTestId('price-122934'));
expect(onClickSpy).toBeCalledWith('122.934');
const resolutionSelect = screen.getByTestId(
'resolution'
) as HTMLSelectElement;
await fireEvent.change(resolutionSelect, { target: { value: '10' } });
await result.rerender(
<Orderbook
decimalPlaces={decimalPlaces}
positionDecimalPlaces={0}
onClick={onClickSpy}
fillGaps
{...generateMockData({
...params,
resolution: 10,
})}
onResolutionChange={onResolutionChange}
/>
);
await fireEvent.click(await screen.getByTestId('price-12299'));
// After resolution change the price is 122.99
expect(onResolutionChange.mock.calls[0][0]).toBe(10);
expect(onClickSpy).toBeCalledWith('122.99');
});
});
+1 -1
View File
@@ -21,7 +21,7 @@ interface OrderbookProps extends OrderbookData {
positionDecimalPlaces: number;
resolution: number;
onResolutionChange: (resolution: number) => void;
onClick?: (price?: string | number) => void;
onClick?: (price: string) => void;
fillGaps?: boolean;
}
@@ -169,7 +169,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
<LiquidityInfoPanel market={market}>
<Link
to={`/liquidity/${market.id}`}
onClick={(ev) => onSelect?.(market.id, ev.metaKey)}
onClick={(ev) => onSelect?.(market.id, ev.metaKey || ev.ctrlKey)}
data-testid="view-liquidity-link"
>
<UILink>{t('View liquidity provision table')}</UILink>
@@ -11,12 +11,13 @@ import {
AgGridDynamic as AgGrid,
PriceFlashCell,
MarketNameCell,
SetFilter,
} from '@vegaprotocol/datagrid';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import * as Schema from '@vegaprotocol/types';
import type { MarketMaybeWithData, MarketFieldsFragment } from '../../';
import type { MarketMaybeWithData } from '../../';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
const { MarketTradingMode, AuctionTrigger } = Schema;
@@ -33,7 +34,6 @@ export const MarketListTable = forwardRef<
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No markets')}
getRowId={getRowId}
ref={ref}
defaultColDef={{
@@ -43,7 +43,7 @@ export const MarketListTable = forwardRef<
filter: true,
filterParams: { buttons: ['reset'] },
}}
suppressCellFocus={true}
suppressCellFocus
components={{ PriceFlashCell, MarketNameCell }}
{...props}
>
@@ -59,11 +59,11 @@ export const MarketListTable = forwardRef<
/>
<AgGridColumn
headerName={t('Trading mode')}
field="data"
field="tradingMode"
minWidth={170}
valueGetter={({
valueFormatter={({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data'>) => {
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
if (!data?.data) return undefined;
const { trigger } = data.data;
const { tradingMode } = data;
@@ -75,14 +75,22 @@ export const MarketListTable = forwardRef<
- ${Schema.AuctionTriggerMapping[trigger]}`
: Schema.MarketTradingModeMapping[tradingMode];
}}
filter={SetFilter}
filterParams={{
set: Schema.MarketTradingModeMapping,
}}
/>
<AgGridColumn
headerName={t('Status')}
field="state"
valueGetter={({
valueFormatter={({
data,
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data?.state] : '-';
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
}}
filter={SetFilter}
filterParams={{
set: Schema.MarketStateMapping,
}}
/>
<AgGridColumn
@@ -1,4 +1,6 @@
import type { MouseEvent } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { t } from '@vegaprotocol/i18n';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { MarketListTable } from './market-list-table';
@@ -12,15 +14,24 @@ interface MarketsContainerProps {
}
export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [dataCount, setDataCount] = useState(0);
const { data, error, loading, reload } = useDataProvider({
dataProvider,
skipUpdates: true,
variables: undefined,
});
useEffect(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, [data]);
const onFilterChanged = useCallback(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, []);
return (
<div className="h-full relative">
<MarketListTable
rowData={error ? [] : data}
ref={gridRef}
rowData={data}
suppressLoadingOverlay
suppressNoRowsOverlay
onCellClicked={(cellEvent: CellClickedEvent) => {
@@ -36,10 +47,12 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
}
onSelect(
(data as MarketMaybeWithData).id,
(event as unknown as MouseEvent)?.metaKey
(event as unknown as MouseEvent)?.metaKey ||
(event as unknown as MouseEvent)?.ctrlKey
);
}}
onMarketClick={onSelect}
onFilterChanged={onFilterChanged}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
@@ -47,6 +60,7 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
error={error}
data={data}
noDataMessage={t('No markets')}
noDataCondition={() => !dataCount}
reload={reload}
/>
</div>
@@ -20,6 +20,7 @@ import type {
OrdersQueryVariables,
} from './__generated__/Orders';
import { OrdersDocument, OrdersUpdateDocument } from './__generated__/Orders';
import type { ApolloClient } from '@apollo/client';
export type Order = Omit<OrderFieldsFragment, 'market'> & {
market?: Market;
@@ -81,8 +82,31 @@ const getData = (
): Edge<OrderFieldsFragment>[] =>
responseData?.party?.ordersConnection?.edges || [];
const getDelta = (subscriptionData: OrdersUpdateSubscription) =>
subscriptionData.orders || [];
const getDelta = (
subscriptionData: OrdersUpdateSubscription,
variables: OrdersQueryVariables,
client: ApolloClient<object>
) => {
if (!subscriptionData.orders) {
return [];
}
subscriptionData.orders.forEach((order) => {
client.cache.modify({
id: client.cache.identify({
__typename: 'Order',
id: order.id,
}),
fields: {
price: () => order.price,
size: () => order.size,
remaining: () => order.remaining,
updatedAt: () => order.updatedAt,
status: () => order.status,
},
});
});
return subscriptionData.orders;
};
const getPageInfo = (responseData: OrdersQuery): PageInfo | null =>
responseData.party?.ordersConnection?.pageInfo || null;
@@ -150,7 +174,7 @@ export const update = (
});
};
export const ordersProvider = makeDataProvider<
const ordersProvider = makeDataProvider<
OrdersQuery,
ReturnType<typeof getData>,
OrdersUpdateSubscription,
@@ -165,11 +189,36 @@ export const ordersProvider = makeDataProvider<
pagination: {
getPageInfo,
append,
first: 100,
first: 1000,
},
additionalContext: { isEnlargedTimeout: true },
});
export const activeOrdersProvider = makeDerivedDataProvider<
ReturnType<typeof getData>,
never,
{ partyId: string; marketId?: string }
>(
[
(callback, client, variables) =>
ordersProvider(callback, client, {
partyId: variables.partyId,
filter: {
status: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
},
}),
],
(partsData, variables, prevData, parts, subscriptions) => {
if (!parts[0].isUpdate && subscriptions && subscriptions[0].load) {
subscriptions[0].load();
}
const orders = partsData[0] as ReturnType<typeof getData>;
return variables.marketId
? orders.filter((edge) => variables.marketId === edge.node.market.id)
: orders;
}
);
export const ordersWithMarketProvider = makeDerivedDataProvider<
(OrderEdge | null)[],
Order[],
@@ -193,63 +242,20 @@ export const ordersWithMarketProvider = makeDerivedDataProvider<
combineInsertionData<Order>
);
const hasActiveOrderProviderInternal = makeDataProvider<
OrdersQuery,
boolean,
OrdersUpdateSubscription,
ReturnType<typeof getDelta>,
OrdersQueryVariables
>({
query: OrdersDocument,
subscriptionQuery: OrdersUpdateDocument,
update: (
data: boolean | null,
delta: ReturnType<typeof getDelta>,
reload: () => void
) => {
const orders = delta?.filter(
(order) => !(order.peggedOrder || order.liquidityProvisionId)
);
if (!orders?.length) {
return data;
}
const hasActiveOrders = orders.some(
(order) => order.status === OrderStatus.STATUS_ACTIVE
);
if (hasActiveOrders) {
return true;
} else if (data && !hasActiveOrders) {
reload();
}
return data;
},
getData: (responseData: OrdersQuery | null) => {
const hasActiveOrder = !!responseData?.party?.ordersConnection?.edges?.some(
(order) => !(order.node.peggedOrder || order.node.liquidityProvision)
);
return hasActiveOrder;
},
getDelta,
});
export const hasActiveOrderProvider = makeDerivedDataProvider<
boolean,
never,
{ partyId: string; marketId?: string }
>(
[
(callback, client, { partyId, marketId }) =>
hasActiveOrderProviderInternal(callback, client, {
marketIds: marketId ? [marketId] : undefined,
filter: {
status: [OrderStatus.STATUS_ACTIVE],
excludeLiquidity: true,
},
pagination: {
first: 1,
},
partyId,
} as OrdersQueryVariables),
],
(parts) => parts[0]
);
>([activeOrdersProvider], (parts) => !!parts[0].length);
export const hasAmendableOrderProvider = makeDerivedDataProvider<
boolean,
never,
{ partyId: string; marketId?: string }
>([activeOrdersProvider], (parts) => {
const activeOrders = parts[0] as ReturnType<typeof getData>;
const hasAmendableOrder = activeOrders.some(
(edge) => !(edge.node.liquidityProvision || edge.node.peggedOrder)
);
return hasAmendableOrder;
});
@@ -6,10 +6,12 @@ import { OrderListManager } from './order-list-manager';
export const OrderListContainer = ({
marketId,
onMarketClick,
onOrderTypeClick,
enforceBottomPlaceholder,
}: {
marketId?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
enforceBottomPlaceholder?: boolean;
}) => {
const { pubKey, isReadOnly } = useVegaWallet();
@@ -23,6 +25,7 @@ export const OrderListContainer = ({
partyId={pubKey}
marketId={marketId}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
isReadOnly={isReadOnly}
enforceBottomPlaceholder={enforceBottomPlaceholder}
/>
@@ -8,7 +8,7 @@ import type { GridReadyEvent } from 'ag-grid-community';
import { OrderListTable } from '../order-list/order-list';
import { useOrderListData } from './use-order-list-data';
import { useHasActiveOrder } from '../../order-hooks/use-has-active-order';
import { useHasAmendableOrder } from '../../order-hooks/use-has-amendable-order';
import type { Filter, Sort } from './use-order-list-data';
import { useBottomPlaceholder } from '@vegaprotocol/react-helpers';
import { OrderStatus } from '@vegaprotocol/types';
@@ -16,6 +16,7 @@ import {
normalizeOrderAmendment,
useVegaTransactionStore,
} from '@vegaprotocol/wallet';
import isEqual from 'lodash/isEqual';
import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order, OrderEdge } from '../order-data-provider';
@@ -24,31 +25,23 @@ export interface OrderListManagerProps {
partyId: string;
marketId?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
isReadOnly: boolean;
enforceBottomPlaceholder?: boolean;
}
const CancelAllOrdersButton = ({
onClick,
marketId,
}: {
onClick: (marketId?: string) => void;
marketId?: string;
}) => {
const hasActiveOrder = useHasActiveOrder(marketId);
return hasActiveOrder ? (
<div className="dark:bg-black/75 bg-white/75 h-auto flex justify-end px-[11px] py-2 absolute bottom-0 right-3 rounded">
<Button
variant="primary"
size="sm"
onClick={() => onClick(marketId)}
data-testid="cancelAll"
>
{t('Cancel all')}
</Button>
</div>
) : null;
};
const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => (
<div className="dark:bg-black/75 bg-white/75 h-auto flex justify-end px-[11px] py-2 absolute bottom-0 right-3 rounded">
<Button
variant="primary"
size="sm"
onClick={onClick}
data-testid="cancelAll"
>
{t('Cancel all')}
</Button>
</div>
);
const initialFilter: Filter = {
status: {
@@ -60,6 +53,7 @@ export const OrderListManager = ({
partyId,
marketId,
onMarketClick,
onOrderTypeClick,
isReadOnly,
enforceBottomPlaceholder,
}: OrderListManagerProps) => {
@@ -68,9 +62,10 @@ export const OrderListManager = ({
const scrolledToTop = useRef(false);
const [sort, setSort] = useState<Sort[] | undefined>();
const [filter, setFilter] = useState<Filter | undefined>(initialFilter);
const filterRef = useRef(initialFilter);
const [editOrder, setEditOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create);
const hasActiveOrder = useHasActiveOrder(marketId);
const hasAmendableOrder = useHasAmendableOrder(marketId);
const { data, error, loading, reload } = useOrderListData({
partyId,
@@ -86,12 +81,16 @@ export const OrderListManager = ({
...bottomPlaceholderProps
} = useBottomPlaceholder<Order>({
gridRef,
disabled: !enforceBottomPlaceholder && !isReadOnly && !hasActiveOrder,
disabled: !enforceBottomPlaceholder && !isReadOnly && !hasAmendableOrder,
});
const onFilterChanged = useCallback(
(event: FilterChangedEvent) => {
const updatedFilter = event.api.getFilterModel();
if (isEqual(updatedFilter, filterRef.current)) {
return;
}
filterRef.current = updatedFilter;
if (Object.keys(updatedFilter).length) {
setFilter(updatedFilter);
} else {
@@ -142,16 +141,13 @@ export const OrderListManager = ({
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, [data]);
const cancelAll = useCallback(
(marketId?: string) => {
create({
orderCancellation: {
marketId,
},
});
},
[create]
);
const cancelAll = useCallback(() => {
create({
orderCancellation: {
marketId,
},
});
}, [create, marketId]);
const extractedData =
data && !loading
? data
@@ -171,6 +167,7 @@ export const OrderListManager = ({
cancel={cancel}
setEditOrder={setEditOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
isReadOnly={isReadOnly}
blockLoadDebounceMillis={100}
suppressLoadingOverlay
@@ -188,8 +185,8 @@ export const OrderListManager = ({
/>
</div>
</div>
{!isReadOnly && (
<CancelAllOrdersButton onClick={cancelAll} marketId={marketId} />
{!isReadOnly && hasAmendableOrder && (
<CancelAllOrdersButton onClick={cancelAll} />
)}
{editOrder && (
<OrderEditDialog
@@ -71,17 +71,27 @@ export const useOrderListData = ({
// define variable as const to get type safety, using generic with useMemo resulted in lost type safety
const allVars: OrdersQueryVariables & OrdersUpdateSubscriptionVariables = {
partyId,
filter: {
dateRange: filter?.updatedAt?.value,
status: filter?.status?.value,
timeInForce: filter?.timeInForce?.value,
types: filter?.type?.value,
},
pagination: {
first: 1000,
},
};
if (
filter?.updatedAt?.value ||
filter?.status?.value.length ||
filter?.timeInForce?.value.length ||
filter?.type?.value.length
) {
allVars.filter = {};
if (filter?.updatedAt?.value) {
allVars.filter.dateRange = filter?.updatedAt?.value;
}
if (filter?.status?.value.length) {
allVars.filter.status = filter?.status?.value;
}
if (filter?.timeInForce?.value.length) {
allVars.filter.timeInForce = filter?.timeInForce?.value;
}
if (filter?.type?.value.length) {
allVars.filter.types = filter?.type?.value;
}
}
return allVars;
}, [partyId, filter]);

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