Compare commits
57
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52481ccaf5 | ||
|
|
483345942d | ||
|
|
d71871566b | ||
|
|
10afa3c1d4 | ||
|
|
ecd362615e | ||
|
|
0d9bd67465 | ||
|
|
b5aa92f007 | ||
|
|
2955790be1 | ||
|
|
89ff9309e9 | ||
|
|
e1df3489f6 | ||
|
|
f0fa9ca5e1 | ||
|
|
9d92477c2a | ||
|
|
b1a198be85 | ||
|
|
5fb385d43c | ||
|
|
8014b279fe | ||
|
|
f4bde57294 | ||
|
|
2e5ff07824 | ||
|
|
7accb42793 | ||
|
|
9229e7f686 | ||
|
|
b570b9c4c1 | ||
|
|
ed78261aad | ||
|
|
363ca9c6e1 | ||
|
|
bafd3c384c | ||
|
|
3b830abe07 | ||
|
|
1787e22b69 | ||
|
|
f105133959 | ||
|
|
aff0e46d23 | ||
|
|
7e4aafcb77 | ||
|
|
643d5408cd | ||
|
|
2a2af3ba9c | ||
|
|
8d4e4a1228 | ||
|
|
84068c8081 | ||
|
|
e1185b9a96 | ||
|
|
f390448c24 | ||
|
|
70943c523c | ||
|
|
dcb79e70d3 | ||
|
|
cc72cdbe16 | ||
|
|
3fd1817e1e | ||
|
|
b483c13f81 | ||
|
|
bde7a9fbf9 | ||
|
|
5fd93ee9c8 | ||
|
|
9ac07fd98a | ||
|
|
9c975ed822 | ||
|
|
138bf7da00 | ||
|
|
3e7fe517f3 | ||
|
|
4f960d09b3 | ||
|
|
351a5aaf96 | ||
|
|
381d9011a0 | ||
|
|
861760b4f9 | ||
|
|
18f1c0014c | ||
|
|
474543d91b | ||
|
|
662753c74b | ||
|
|
71aa8882bc | ||
|
|
ecfbccf8ed | ||
|
|
2aad6b1a14 | ||
|
|
8463d371ad | ||
|
|
0914e7ce4b |
@@ -13,7 +13,7 @@ env:
|
||||
|
||||
jobs:
|
||||
add_issue:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: 'Add issue to project board'
|
||||
run: |
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
name: PR Validations
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
- release/*
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
@@ -12,42 +11,55 @@ on:
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
jobs:
|
||||
pr:
|
||||
runs-on: ubuntu-latest
|
||||
lint-test-build:
|
||||
runs-on: ubuntu-22.04
|
||||
name: '(CI) lint + unit test + build'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check node version
|
||||
id: node-version
|
||||
run: |
|
||||
npmVersion=$(cat .nvmrc | head -n 1)
|
||||
echo ::set-output name=npmVersion::${npmVersion}
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ steps.node-version.outputs.npmVersion }}
|
||||
node-version-file: '.nvmrc'
|
||||
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
|
||||
cache: yarn
|
||||
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Derive appropriate SHAs for base and head for `nx affected` commands
|
||||
uses: nrwl/nx-set-shas@v3
|
||||
with:
|
||||
main-branch-name: develop
|
||||
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node_modules-${{ hashFiles('**/yarn.lock') }}
|
||||
- name: Check formatting
|
||||
run: yarn nx format:check
|
||||
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Lint affected
|
||||
run: yarn nx affected:lint --max-warnings=0
|
||||
|
||||
- name: Build affected spec
|
||||
run: yarn nx affected --target=build-spec
|
||||
|
||||
- name: Test affected
|
||||
run: yarn nx affected:test
|
||||
|
||||
- name: Build affected
|
||||
run: yarn nx affected:build
|
||||
|
||||
# See affected apps
|
||||
- name: See affected apps
|
||||
run: |
|
||||
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=HEAD --select=projects)"
|
||||
echo ">>>> debug"
|
||||
echo "NX Version: $nx_version"
|
||||
echo "NX_BASE: ${{ env.NX_BASE }}"
|
||||
echo "NX_HEAD: ${{ env.NX_HEAD }}"
|
||||
echo ">>>> eof debug"
|
||||
|
||||
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
|
||||
echo -n "Affected projects: $affected"
|
||||
|
||||
projects_e2e=""
|
||||
@@ -64,32 +76,33 @@ jobs:
|
||||
projects: ${{ env.PROJECTS }}
|
||||
projects-e2e: ${{ env.PROJECTS_E2E }}
|
||||
|
||||
run-cypress:
|
||||
needs: pr
|
||||
if: ${{ needs.pr.outputs.projects != '[]' }}
|
||||
cypress:
|
||||
needs: lint-test-build
|
||||
name: '(CI) cypress'
|
||||
if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
|
||||
uses: ./.github/workflows/cypress-run.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
projects: ${{ needs.pr.outputs.projects-e2e }}
|
||||
projects: ${{ needs.lint-test-build.outputs.projects-e2e }}
|
||||
tags: '@smoke @regression'
|
||||
|
||||
run-docker-build:
|
||||
needs: pr
|
||||
if: ${{ needs.pr.outputs.projects != '[]' }}
|
||||
uses: ./.github/workflows/publish-docker-containers.yml
|
||||
publish-dist:
|
||||
needs: lint-test-build
|
||||
name: '(CD) publish dist'
|
||||
if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
|
||||
uses: ./.github/workflows/publish-dist.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
projects: ${{ needs.pr.outputs.projects }}
|
||||
projects: ${{ needs.lint-test-build.outputs.projects }}
|
||||
|
||||
# Report single result at the end, to avoid mess with required checks in PR
|
||||
result:
|
||||
cypress-result:
|
||||
if: ${{ always() }}
|
||||
needs: run-cypress
|
||||
runs-on: ubuntu-latest
|
||||
name: Cypress result
|
||||
needs: cypress
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- run: |
|
||||
result="${{ needs.run-cypress.result }}"
|
||||
result="${{ needs.cypress.result }}"
|
||||
if [[ $result == "success" || $result == "skipped" ]]; then
|
||||
exit 0
|
||||
else
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
jobs:
|
||||
cypress-run:
|
||||
name: Run Cypress Trading tests -- live environment
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Cypress Run
|
||||
name: (CI) Cypress Run
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
|
||||
@@ -8,22 +8,25 @@ on:
|
||||
jobs:
|
||||
master:
|
||||
name: Generate Queries
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Use Node.js 16
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.15.1
|
||||
node-version-file: '.nvmrc'
|
||||
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
|
||||
cache: yarn
|
||||
|
||||
- name: Install root dependencies
|
||||
run: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Generate queries
|
||||
run: node ./scripts/get-queries.js
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: queries
|
||||
|
||||
@@ -3,21 +3,28 @@ name: Verify PR title
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review, reopened, edited, synchronize]
|
||||
types:
|
||||
- opened
|
||||
- ready_for_review
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
jobs:
|
||||
lint_pr:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Use Node.js 16
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.15.1
|
||||
node-version-file: '.nvmrc'
|
||||
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
|
||||
cache: yarn
|
||||
|
||||
- name: Install root dependencies
|
||||
run: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Check PR title
|
||||
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
master:
|
||||
name: Generate Queries
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
+56
-42
@@ -1,4 +1,4 @@
|
||||
name: Docker build
|
||||
name: (CD) Publish docker + s3
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -8,13 +8,13 @@ on:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
master:
|
||||
publish-dist:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
app: ${{ fromJSON(inputs.projects) }}
|
||||
name: ${{ matrix.app }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
@@ -29,41 +29,6 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||
# https://github.com/actions/checkout#Checkout-pull-request-HEAD-commit-instead-of-merge-commit
|
||||
- name: Determine Docker Image tag
|
||||
id: tags
|
||||
run: |
|
||||
npmVersion=$(cat .nvmrc | head -n 1)
|
||||
versionTag=${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || github.event.pull_request.head.sha }}
|
||||
echo ::set-output name=npmVersion::${npmVersion}
|
||||
echo ::set-output name=version::${versionTag}
|
||||
|
||||
- name: Print config
|
||||
run: |
|
||||
git rev-parse --verify HEAD
|
||||
git status
|
||||
echo "steps.tags.outputs.version=${{ steps.tags.outputs.version }}"
|
||||
|
||||
- name: Build and export to local Docker
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
load: true
|
||||
build-args: |
|
||||
APP=${{ matrix.app }}
|
||||
NODE_VERSION=${{ steps.tags.outputs.npmVersion }}
|
||||
tags: |
|
||||
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
|
||||
|
||||
- name: Sanity check docker image
|
||||
run: |
|
||||
echo "Check .env file"
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat .env
|
||||
echo "Check ipfs-hash"
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat ipfs-hash
|
||||
echo "List html directory"
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local ls -lah
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -71,17 +36,66 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||
- name: Check node version
|
||||
id: tags
|
||||
run: |
|
||||
nodeVersion=$(cat .nvmrc | head -n 1)
|
||||
echo ::set-output name=nodeVersion::${nodeVersion}
|
||||
|
||||
if [[ "${{ github.event_name }}" = "push" ]]; then
|
||||
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
|
||||
bucketName="${{ github.event.repository.name }}-$envName"
|
||||
echo ::set-output name=bucketName::${bucketName}
|
||||
echo ::set-output name=envName::${envName}
|
||||
fi
|
||||
|
||||
- name: Build and export to local Docker
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
load: true
|
||||
build-args: |
|
||||
APP=${{ matrix.app }}
|
||||
NODE_VERSION=${{ steps.tags.outputs.nodeVersion }}
|
||||
ENV_NAME=${{ steps.tags.outputs.envName || '' }}
|
||||
tags: |
|
||||
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
|
||||
|
||||
- name: Sanity check docker image
|
||||
run: |
|
||||
echo "Check ipfs-hash"
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat ipfs-hash
|
||||
|
||||
echo "List html directory"
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local ls -lah
|
||||
|
||||
echo "Copy dist to local filesystem"
|
||||
docker create --name=dist ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
|
||||
docker cp dist:/usr/share/nginx/html dist
|
||||
|
||||
echo "Check local dist"
|
||||
ls -al dist
|
||||
|
||||
- name: Publish dist as docker image
|
||||
uses: docker/build-push-action@v3
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
with:
|
||||
push: true
|
||||
build-args: |
|
||||
APP=${{ matrix.app }}
|
||||
NODE_VERSION=${{ steps.tags.outputs.npmVersion }}
|
||||
NODE_VERSION=${{ steps.tags.outputs.nodeVersion }}
|
||||
tags: |
|
||||
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:latest
|
||||
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ steps.tags.outputs.version }}
|
||||
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
# - uses: shallwefootball/s3-upload-action@master
|
||||
# if: ${{ github.event_name == 'push' }}
|
||||
# name: Upload dist S3
|
||||
# with:
|
||||
# aws_key_id: ${{ secrets.AWS_KEY_ID }}
|
||||
# aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY}}
|
||||
# aws_bucket: ${{ steps.tags.outputs.bucketName }}
|
||||
# source_dir: 'dist'
|
||||
|
||||
- name: Add preview label
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
@@ -19,29 +19,27 @@ on:
|
||||
jobs:
|
||||
publish:
|
||||
name: Build & Publish - Tag
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: 'read'
|
||||
actions: 'read'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: User Node.js 16
|
||||
id: Node
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.15.1
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node_modules-${{ hashFiles('**/yarn.lock') }}
|
||||
node-version-file: '.nvmrc'
|
||||
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
|
||||
cache: yarn
|
||||
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build project
|
||||
run: yarn nx build ${{inputs.project}}
|
||||
|
||||
- name: Publish project to @vegaprotocol
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
name: Unit tests & build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
pr:
|
||||
name: Test and lint - PR
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: 'read'
|
||||
actions: 'read'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Derive appropriate SHAs for base and head for `nx affected` commands
|
||||
uses: nrwl/nx-set-shas@v2
|
||||
with:
|
||||
main-branch-name: ${{ github.base_ref }}
|
||||
- name: Use Node.js 16
|
||||
id: Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.15.1
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: node_modules-${{ hashFiles('**/yarn.lock') }}
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Check formatting
|
||||
run: yarn nx format:check
|
||||
- name: Lint affected
|
||||
run: yarn nx affected:lint --max-warnings=0
|
||||
- name: Test affected
|
||||
run: yarn nx affected:test
|
||||
- name: Build affected
|
||||
run: yarn nx affected:build
|
||||
- name: Build affected spec
|
||||
run: yarn nx affected --target=build-spec
|
||||
+2
-7
@@ -4,6 +4,7 @@ FROM --platform=amd64 node:${NODE_VERSION}-alpine3.16 as build
|
||||
WORKDIR /app
|
||||
# Argument to allow building of different apps
|
||||
ARG APP
|
||||
ARG ENV_NAME=""
|
||||
RUN apk add --update --no-cache \
|
||||
python3 \
|
||||
make \
|
||||
@@ -18,16 +19,10 @@ RUN sh ./docker-build.sh
|
||||
# if this fails you need to docker pull nginx:1.23-alpine and pin new SHA
|
||||
# this is to ensure that we run always same version of alpine to make sure ipfs is indempotent
|
||||
FROM --platform=amd64 nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
|
||||
ARG APP
|
||||
# configuration of system
|
||||
RUN apk add --no-cache bash go-ipfs
|
||||
EXPOSE 80
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
CMD ["/entrypoint.sh"]
|
||||
|
||||
# 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
|
||||
COPY ./apps/${APP}/.env .env
|
||||
RUN ipfs init && echo "$(ipfs add -rQ .)" > ipfs-hash
|
||||
RUN apk add --no-cache go-ipfs; ipfs init && echo "$(ipfs add -rQ .)" > ipfs-hash; apk del go-ipfs
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml
|
||||
NX_VEGA_NETWORKS='{"SANDBOX":"https://sandbox.explorer.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||
NX_VEGA_NETWORKS='{"STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||
NX_VEGA_ENV=STAGNET3
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# App configuration variables
|
||||
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.xyz/websocket
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
|
||||
NX_VEGA_ENV=MIRROR
|
||||
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS=1
|
||||
NX_EXPLORER_GENESIS=1
|
||||
NX_EXPLORER_GOVERNANCE=1
|
||||
NX_EXPLORER_NETWORK_PARAMETERS=1
|
||||
NX_EXPLORER_PARTIES=1
|
||||
NX_EXPLORER_VALIDATORS=1
|
||||
NX_EXPLORER_MARKETS=0
|
||||
NX_EXPLORER_ORACLES=0
|
||||
NX_EXPLORER_TXS_LIST=1
|
||||
@@ -1,12 +0,0 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV=SANDBOX
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
|
||||
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.sandbox.vega.xyz/websocket
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
@@ -9,5 +9,5 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
|
||||
|
||||
NX_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
|
||||
NX_TENDERMINT_URL=https://tm-be.validators-testnet.vega.rocks
|
||||
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/
|
||||
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV=MIRROR
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
|
||||
NX_VEGA_URL=https://api.n00.mainnet-mirror.vega.xyz/graphql
|
||||
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_EXPLORER_URL=https://mirror.explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||
@@ -1,9 +0,0 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_URL=https://api.sandbox.vega.xyz/graphql
|
||||
NX_VEGA_ENV=SANDBOX
|
||||
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
|
||||
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
@@ -8,3 +8,4 @@ NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
|
||||
@@ -12,6 +12,7 @@ const TRUTHY = ['1', 'true'];
|
||||
interface VegaContracts {
|
||||
claimAddress: string;
|
||||
lockedAddress: string;
|
||||
tokenVestingAddress?: string;
|
||||
}
|
||||
|
||||
const customClaimAddress = process.env['NX_CUSTOM_CLAIM_ADDRESS'] as string;
|
||||
@@ -36,21 +37,16 @@ export const ContractAddresses: {
|
||||
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
|
||||
lockedAddress: '0x0', // TODO not deployed to this env
|
||||
},
|
||||
SANDBOX: {
|
||||
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
|
||||
lockedAddress: '0x0', // TODO not deployed to this env
|
||||
},
|
||||
TESTNET: {
|
||||
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
|
||||
lockedAddress: '0x0', // TODO not deployed to this env
|
||||
},
|
||||
MIRROR: {
|
||||
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
|
||||
lockedAddress: '0x0', // TODO not deployed to this env
|
||||
},
|
||||
VALIDATOR_TESTNET: {
|
||||
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
|
||||
lockedAddress: '0x0', // TODO not deployed to this env
|
||||
// This is a fallback contract address for the validator testnet network which does not
|
||||
// have a vesting contract address set and is therefore not in the ethereum config
|
||||
tokenVestingAddress: '0xadFcb7f93a24F8743a8e548d74d2ecB373c92866',
|
||||
},
|
||||
MAINNET: {
|
||||
claimAddress: '0x0ee1fb382caf98e86e97e51f9f42f8b4654020f3',
|
||||
|
||||
@@ -49,6 +49,13 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
|
||||
signer = provider.getSigner();
|
||||
}
|
||||
|
||||
const tokenVestingAddress =
|
||||
config.token_vesting_contract?.address ||
|
||||
ENV.addresses.tokenVestingAddress;
|
||||
if (!tokenVestingAddress) {
|
||||
throw new Error('No token vesting address found');
|
||||
}
|
||||
|
||||
if (provider && config) {
|
||||
const staking = new StakingBridge(
|
||||
config.staking_bridge_contract.address,
|
||||
@@ -63,7 +70,7 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
|
||||
signer || provider
|
||||
),
|
||||
vesting: new TokenVesting(
|
||||
config.token_vesting_contract.address,
|
||||
tokenVestingAddress,
|
||||
signer || provider
|
||||
),
|
||||
claim: new Claim(ENV.addresses.claimAddress, signer || provider),
|
||||
|
||||
@@ -7,9 +7,10 @@ query PreviousEpoch($epochId: ID) {
|
||||
id
|
||||
rewardScore {
|
||||
rawValidatorScore
|
||||
performanceScore
|
||||
}
|
||||
rankingScore {
|
||||
performanceScore
|
||||
stakeScore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export type PreviousEpochQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string } | null, rankingScore: { __typename?: 'RankingScore', performanceScore: string } } } | null> | null } | null } };
|
||||
export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string, performanceScore: string } | null, rankingScore: { __typename?: 'RankingScore', stakeScore: string } } } | null> | null } | null } };
|
||||
|
||||
|
||||
export const PreviousEpochDocument = gql`
|
||||
@@ -21,9 +21,10 @@ export const PreviousEpochDocument = gql`
|
||||
id
|
||||
rewardScore {
|
||||
rawValidatorScore
|
||||
performanceScore
|
||||
}
|
||||
rankingScore {
|
||||
performanceScore
|
||||
stakeScore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -81,9 +81,10 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
|
||||
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
|
||||
rewardScore: {
|
||||
rawValidatorScore: '0.25',
|
||||
performanceScore: '0.9998677767864936',
|
||||
},
|
||||
rankingScore: {
|
||||
performanceScore: '0.9998677767864936',
|
||||
stakeScore: '0.2499583402766206',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -92,9 +93,10 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
|
||||
id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99',
|
||||
rewardScore: {
|
||||
rawValidatorScore: '0.3',
|
||||
performanceScore: '1',
|
||||
},
|
||||
rankingScore: {
|
||||
performanceScore: '1',
|
||||
stakeScore: '0.25',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -103,9 +105,10 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
|
||||
id: '12c81b738e8051152e1afe44376ec37bca9216466e6d44cdd772194bad0ada81',
|
||||
rewardScore: {
|
||||
rawValidatorScore: '0.35',
|
||||
performanceScore: '0.999629748500531',
|
||||
},
|
||||
rankingScore: {
|
||||
performanceScore: '0.999629748500531',
|
||||
stakeScore: '0.2312',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+16
-20
@@ -10,7 +10,6 @@ import {
|
||||
getFormattedPerformanceScore,
|
||||
getLastEpochScoreAndPerformance,
|
||||
getNormalisedVotingPower,
|
||||
getOverstakedAmount,
|
||||
getOverstakingPenalty,
|
||||
getPerformancePenalty,
|
||||
getTotalPenalties,
|
||||
@@ -52,7 +51,6 @@ interface CanonisedConsensusNodeProps {
|
||||
[ValidatorFields.STAKED_BY_OPERATOR]: string;
|
||||
[ValidatorFields.PERFORMANCE_SCORE]: string;
|
||||
[ValidatorFields.PERFORMANCE_PENALTY]: string;
|
||||
[ValidatorFields.OVERSTAKED_AMOUNT]: string;
|
||||
[ValidatorFields.OVERSTAKING_PENALTY]: string;
|
||||
[ValidatorFields.TOTAL_PENALTIES]: string;
|
||||
[ValidatorFields.PENDING_STAKE]: string;
|
||||
@@ -164,14 +162,11 @@ export const ConsensusValidatorsTable = ({
|
||||
pendingUserStake,
|
||||
userStakeShare,
|
||||
}) => {
|
||||
const { rawValidatorScore, performanceScore } =
|
||||
getLastEpochScoreAndPerformance(previousEpochData, id);
|
||||
|
||||
const overstakedAmount = getOverstakedAmount(
|
||||
rawValidatorScore,
|
||||
stakedTotal,
|
||||
totalStake
|
||||
);
|
||||
const {
|
||||
rawValidatorScore: previousEpochValidatorScore,
|
||||
performanceScore: previousEpochPerformanceScore,
|
||||
stakeScore: previousEpochStakeScore,
|
||||
} = getLastEpochScoreAndPerformance(previousEpochData, id);
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -184,7 +179,7 @@ export const ConsensusValidatorsTable = ({
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]:
|
||||
getNormalisedVotingPower(votingPower),
|
||||
[ValidatorFields.UNNORMALISED_VOTING_POWER]:
|
||||
getUnnormalisedVotingPower(rawValidatorScore),
|
||||
getUnnormalisedVotingPower(previousEpochValidatorScore),
|
||||
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
|
||||
[ValidatorFields.STAKED_BY_DELEGATES]: formatNumber(
|
||||
toBigNum(stakedByDelegates, decimals),
|
||||
@@ -194,18 +189,19 @@ export const ConsensusValidatorsTable = ({
|
||||
toBigNum(stakedByOperator, decimals),
|
||||
2
|
||||
),
|
||||
[ValidatorFields.PERFORMANCE_SCORE]:
|
||||
getFormattedPerformanceScore(performanceScore).toString(),
|
||||
[ValidatorFields.PERFORMANCE_PENALTY]:
|
||||
getPerformancePenalty(performanceScore),
|
||||
[ValidatorFields.OVERSTAKED_AMOUNT]: overstakedAmount.toString(),
|
||||
[ValidatorFields.PERFORMANCE_SCORE]: getFormattedPerformanceScore(
|
||||
previousEpochPerformanceScore
|
||||
).toString(),
|
||||
[ValidatorFields.PERFORMANCE_PENALTY]: getPerformancePenalty(
|
||||
previousEpochPerformanceScore
|
||||
),
|
||||
[ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty(
|
||||
overstakedAmount,
|
||||
totalStake
|
||||
previousEpochValidatorScore,
|
||||
previousEpochStakeScore
|
||||
),
|
||||
[ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties(
|
||||
rawValidatorScore,
|
||||
performanceScore,
|
||||
previousEpochValidatorScore,
|
||||
previousEpochPerformanceScore,
|
||||
stakedTotal,
|
||||
totalStake
|
||||
),
|
||||
|
||||
+19
-19
@@ -7,7 +7,6 @@ import { BigNumber } from '../../../../lib/bignumber';
|
||||
import {
|
||||
getFormattedPerformanceScore,
|
||||
getLastEpochScoreAndPerformance,
|
||||
getOverstakedAmount,
|
||||
getOverstakingPenalty,
|
||||
getPerformancePenalty,
|
||||
getTotalPenalties,
|
||||
@@ -82,21 +81,21 @@ export const StandbyPendingValidatorsTable = ({
|
||||
pendingUserStake,
|
||||
userStakeShare,
|
||||
}) => {
|
||||
const { rawValidatorScore, performanceScore } =
|
||||
getLastEpochScoreAndPerformance(previousEpochData, id);
|
||||
const {
|
||||
rawValidatorScore: previousEpochValidatorScore,
|
||||
performanceScore: previousEpochPerformanceScore,
|
||||
stakeScore: previousEpochStakeScore,
|
||||
} = getLastEpochScoreAndPerformance(previousEpochData, id);
|
||||
|
||||
const overstakedAmount = getOverstakedAmount(
|
||||
rawValidatorScore,
|
||||
stakedTotal,
|
||||
totalStake
|
||||
);
|
||||
let individualStakeNeededForPromotion,
|
||||
individualStakeNeededForPromotionDescription;
|
||||
|
||||
if (stakeNeededForPromotion && performanceScore) {
|
||||
if (stakeNeededForPromotion && previousEpochPerformanceScore) {
|
||||
const stakedTotalBigNum = new BigNumber(stakedTotal);
|
||||
const stakeNeededBigNum = new BigNumber(stakeNeededForPromotion);
|
||||
const performanceScoreBigNum = new BigNumber(performanceScore);
|
||||
const performanceScoreBigNum = new BigNumber(
|
||||
previousEpochPerformanceScore
|
||||
);
|
||||
|
||||
const calc = stakeNeededBigNum
|
||||
.dividedBy(performanceScoreBigNum)
|
||||
@@ -142,18 +141,19 @@ export const StandbyPendingValidatorsTable = ({
|
||||
toBigNum(stakedByOperator, decimals),
|
||||
2
|
||||
),
|
||||
[ValidatorFields.PERFORMANCE_SCORE]:
|
||||
getFormattedPerformanceScore(performanceScore).toString(),
|
||||
[ValidatorFields.PERFORMANCE_PENALTY]:
|
||||
getPerformancePenalty(performanceScore),
|
||||
[ValidatorFields.OVERSTAKED_AMOUNT]: overstakedAmount.toString(),
|
||||
[ValidatorFields.PERFORMANCE_SCORE]: getFormattedPerformanceScore(
|
||||
previousEpochPerformanceScore
|
||||
).toString(),
|
||||
[ValidatorFields.PERFORMANCE_PENALTY]: getPerformancePenalty(
|
||||
previousEpochPerformanceScore
|
||||
),
|
||||
[ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty(
|
||||
overstakedAmount,
|
||||
totalStake
|
||||
previousEpochValidatorScore,
|
||||
previousEpochStakeScore
|
||||
),
|
||||
[ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties(
|
||||
rawValidatorScore,
|
||||
performanceScore,
|
||||
previousEpochValidatorScore,
|
||||
previousEpochPerformanceScore,
|
||||
stakedTotal,
|
||||
totalStake
|
||||
),
|
||||
|
||||
@@ -20,7 +20,6 @@ import { SubHeading } from '../../../components/heading';
|
||||
import {
|
||||
getLastEpochScoreAndPerformance,
|
||||
getNormalisedVotingPower,
|
||||
getOverstakedAmount,
|
||||
getOverstakingPenalty,
|
||||
getPerformancePenalty,
|
||||
getTotalPenalties,
|
||||
@@ -75,15 +74,9 @@ export const ValidatorTable = ({
|
||||
|
||||
const stakedOnNode = toBigNum(node.stakedTotal, decimals);
|
||||
|
||||
const { rawValidatorScore, performanceScore } =
|
||||
const { rawValidatorScore, performanceScore, stakeScore } =
|
||||
getLastEpochScoreAndPerformance(previousEpochData, node.id);
|
||||
|
||||
const overstakedAmount = getOverstakedAmount(
|
||||
rawValidatorScore,
|
||||
stakedTotal,
|
||||
node.stakedTotal
|
||||
);
|
||||
|
||||
const stakePercentage = getStakePercentage(total, stakedOnNode);
|
||||
|
||||
const totalPenaltiesAmount = getTotalPenalties(
|
||||
@@ -245,7 +238,7 @@ export const ValidatorTable = ({
|
||||
|
||||
<Tooltip description={t('OverstakedPenaltyDescription')}>
|
||||
<span data-testid="overstaking-penalty">
|
||||
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
|
||||
{getOverstakingPenalty(rawValidatorScore, stakeScore)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</KeyValueTableRow>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
getNormalisedVotingPower,
|
||||
getUnnormalisedVotingPower,
|
||||
getOverstakingPenalty,
|
||||
getOverstakedAmount,
|
||||
getFormattedPerformanceScore,
|
||||
getPerformancePenalty,
|
||||
getTotalPenalties,
|
||||
@@ -22,9 +21,10 @@ describe('getLastEpochScoreAndPerformance', () => {
|
||||
id: '0x123',
|
||||
rewardScore: {
|
||||
rawValidatorScore: '0.25',
|
||||
performanceScore: '0.75',
|
||||
},
|
||||
rankingScore: {
|
||||
performanceScore: '0.75',
|
||||
stakeScore: '0.25',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -33,9 +33,10 @@ describe('getLastEpochScoreAndPerformance', () => {
|
||||
id: '0x234',
|
||||
rewardScore: {
|
||||
rawValidatorScore: '0.35',
|
||||
performanceScore: '0.85',
|
||||
},
|
||||
rankingScore: {
|
||||
performanceScore: '0.85',
|
||||
stakeScore: '0.25',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -50,12 +51,14 @@ describe('getLastEpochScoreAndPerformance', () => {
|
||||
).toEqual({
|
||||
rawValidatorScore: '0.25',
|
||||
performanceScore: '0.75',
|
||||
stakeScore: '0.25',
|
||||
});
|
||||
expect(
|
||||
getLastEpochScoreAndPerformance(mockPreviousEpochData, '0x234')
|
||||
).toEqual({
|
||||
rawValidatorScore: '0.35',
|
||||
performanceScore: '0.85',
|
||||
stakeScore: '0.25',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -79,40 +82,34 @@ describe('getUnnormalisedVotingPower', () => {
|
||||
});
|
||||
|
||||
describe('getOverstakingPenalty', () => {
|
||||
it('should return the overstaking penalty', () => {
|
||||
expect(
|
||||
getOverstakingPenalty(new BigNumber(100), Number(1000).toString())
|
||||
).toEqual('10.00%');
|
||||
expect(
|
||||
getOverstakingPenalty(new BigNumber(500), Number(2000).toString())
|
||||
).toEqual('25.00%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOverstakedAmount', () => {
|
||||
it('should return the overstaked amount', () => {
|
||||
expect(
|
||||
// If a validator score is 0, any amount staked on the node is considered overstaked
|
||||
getOverstakedAmount('0', Number(100).toString(), Number(20).toString())
|
||||
).toEqual(new BigNumber(20));
|
||||
expect(
|
||||
getOverstakedAmount('0.05', Number(100).toString(), Number(20).toString())
|
||||
).toEqual(new BigNumber(15));
|
||||
expect(
|
||||
getOverstakedAmount('0.1', Number(100).toString(), Number(20).toString())
|
||||
).toEqual(new BigNumber(10));
|
||||
expect(
|
||||
getOverstakedAmount('0.15', Number(100).toString(), Number(20).toString())
|
||||
).toEqual(new BigNumber(5));
|
||||
expect(
|
||||
getOverstakedAmount('0.2', Number(100).toString(), Number(20).toString())
|
||||
).toEqual(new BigNumber(0));
|
||||
it('returns "0%" when both arguments are null or undefined', () => {
|
||||
expect(getOverstakingPenalty(null, null)).toBe('0%');
|
||||
expect(getOverstakingPenalty(undefined, undefined)).toBe('0%');
|
||||
expect(getOverstakingPenalty(null, undefined)).toBe('0%');
|
||||
expect(getOverstakingPenalty(undefined, null)).toBe('0%');
|
||||
});
|
||||
|
||||
it('should return 0 if the overstaked amount is negative', () => {
|
||||
expect(
|
||||
getOverstakedAmount('0.8', Number(100).toString(), Number(20).toString())
|
||||
).toEqual(new BigNumber(0));
|
||||
it('returns "0%" when one argument is null or undefined', () => {
|
||||
expect(getOverstakingPenalty('10', null)).toBe('0%');
|
||||
expect(getOverstakingPenalty(null, '20')).toBe('0%');
|
||||
expect(getOverstakingPenalty('10', undefined)).toBe('0%');
|
||||
expect(getOverstakingPenalty(undefined, '20')).toBe('0%');
|
||||
});
|
||||
|
||||
it('returns "0%" when validatorScore or stakeScore is zero', () => {
|
||||
expect(getOverstakingPenalty('0', '20')).toBe('0%');
|
||||
expect(getOverstakingPenalty('10', '0')).toBe('0%');
|
||||
});
|
||||
|
||||
it('returns the correct overstaking penalty', () => {
|
||||
expect(getOverstakingPenalty('0.18', '0.2')).toBe('10.00%');
|
||||
expect(getOverstakingPenalty('0.2', '0.2')).toBe('0.00%');
|
||||
expect(getOverstakingPenalty('0.04', '0.2')).toBe('80.00%');
|
||||
});
|
||||
|
||||
it('handles string numbers with decimals', () => {
|
||||
expect(getOverstakingPenalty('7.5', '15')).toBe('50.00%');
|
||||
expect(getOverstakingPenalty('12.5', '25')).toBe('50.00%');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ export const getLastEpochScoreAndPerformance = (
|
||||
|
||||
return {
|
||||
rawValidatorScore: validator?.rewardScore?.rawValidatorScore,
|
||||
performanceScore: validator?.rankingScore?.performanceScore,
|
||||
performanceScore: validator?.rewardScore?.performanceScore,
|
||||
stakeScore: validator?.rankingScore?.stakeScore,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -42,31 +43,26 @@ export const getPerformancePenalty = (performanceScore?: string) =>
|
||||
2
|
||||
);
|
||||
|
||||
export const getOverstakedAmount = (
|
||||
validatorScore: string | null | undefined,
|
||||
totalStake: string,
|
||||
stakedOnNode: string
|
||||
) => {
|
||||
const toReturn = validatorScore
|
||||
? new BigNumber(stakedOnNode).minus(
|
||||
new BigNumber(validatorScore).times(new BigNumber(totalStake))
|
||||
)
|
||||
: new BigNumber(0);
|
||||
|
||||
return toReturn.isNegative() ? new BigNumber(0) : toReturn;
|
||||
};
|
||||
|
||||
export const getOverstakingPenalty = (
|
||||
overstakedAmount: BigNumber,
|
||||
stakedOnNode: string
|
||||
validatorScore: string | null | undefined,
|
||||
stakeScore: string | null | undefined
|
||||
) => {
|
||||
if (!validatorScore || !stakeScore) {
|
||||
return '0%';
|
||||
}
|
||||
|
||||
// avoid division by zero
|
||||
if (new BigNumber(stakedOnNode).isZero() || overstakedAmount.isZero()) {
|
||||
return '0';
|
||||
if (
|
||||
new BigNumber(validatorScore).isZero() ||
|
||||
new BigNumber(stakeScore).isZero()
|
||||
) {
|
||||
return '0%';
|
||||
}
|
||||
|
||||
return formatNumberPercentage(
|
||||
overstakedAmount.dividedBy(new BigNumber(stakedOnNode)).times(100),
|
||||
new BigNumber(1)
|
||||
.minus(new BigNumber(validatorScore).dividedBy(new BigNumber(stakeScore)))
|
||||
.times(100),
|
||||
2
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import { TokenDetailsCirculating } from './token-details-circulating';
|
||||
import { SplashLoader } from '../../../components/splash-loader';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
import { useContracts } from '../../../contexts/contracts/contracts-context';
|
||||
import { ENV } from '../../../config';
|
||||
|
||||
export const TokenDetails = ({
|
||||
totalSupply,
|
||||
@@ -49,6 +50,9 @@ export const TokenDetails = ({
|
||||
);
|
||||
}
|
||||
|
||||
const tokenVestingContractAddress =
|
||||
config.token_vesting_contract?.address || ENV.addresses.tokenVestingAddress;
|
||||
|
||||
return (
|
||||
<div className="token-details">
|
||||
<RoundedWrapper>
|
||||
@@ -65,18 +69,20 @@ export const TokenDetails = ({
|
||||
{token.address}
|
||||
</Link>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('Vesting contract').toUpperCase()}
|
||||
<Link
|
||||
data-testid="token-contract"
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
className="font-mono text-white text-right"
|
||||
href={`${ETHERSCAN_URL}/address/${config.token_vesting_contract.address}`}
|
||||
target="_blank"
|
||||
>
|
||||
{config.token_vesting_contract.address}
|
||||
</Link>
|
||||
</KeyValueTableRow>
|
||||
{tokenVestingContractAddress && (
|
||||
<KeyValueTableRow>
|
||||
{t('Vesting contract').toUpperCase()}
|
||||
<Link
|
||||
data-testid="token-contract"
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
className="font-mono text-white text-right"
|
||||
href={`${ETHERSCAN_URL}/address/${tokenVestingContractAddress}`}
|
||||
target="_blank"
|
||||
>
|
||||
{tokenVestingContractAddress}
|
||||
</Link>
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
<KeyValueTableRow>
|
||||
{t('Total supply').toUpperCase()}
|
||||
<span className="font-mono" data-testid="total-supply">
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
|
||||
NX_VEGA_URL=https://api.sandbox.vega.xyz/graphql
|
||||
NX_VEGA_ENV=SANDBOX
|
||||
NX_VEGA_NETWORKS={\"DEVNET\":\"https://dev.token.vega.xyz\",\"STAGNET3\":\"https://stagnet3.token.vega.xyz\",\"STAGNET1\":\"https://stagnet1.token.vega.xyz\",\"TESTNET\":\"https://token.fairground.wtf\",\"MAINNET\":\"https://token.vega.xyz\"}
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"hosts": ["https://api-n00.mainnet-mirror.vega.rocks/graphql"]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"hosts": ["https://api-n01.sandbox.vega.rocks/graphql"]
|
||||
}
|
||||
@@ -87,7 +87,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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -95,6 +95,16 @@ describe('Console - market info - live env', { tags: '@live' }, () => {
|
||||
cy.wrap(element).should('have.text', subtitles[index]);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders correctly liquidity in trading tab', () => {
|
||||
cy.getByTestId('Liquidity').click();
|
||||
cy.contains('Loading').should('not.exist');
|
||||
cy.contains('Something went wrong').should('not.exist');
|
||||
cy.contains('Application error').should('not.exist');
|
||||
cy.getByTestId('tab-liquidity').within(() => {
|
||||
cy.get('[col-id="party.id"]').eq(1).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Console - market summary - live env', { tags: '@live' }, () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
+1
-252
@@ -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();
|
||||
@@ -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-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('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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
getId,
|
||||
matchFilter,
|
||||
liquidityProvisionsDataProvider,
|
||||
LiquidityTable,
|
||||
lpAggregatedDataProvider,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { tooltipMapping } from '@vegaprotocol/market-info';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
createDocsLinks,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
@@ -17,28 +18,28 @@ import {
|
||||
useNetworkParams,
|
||||
updateGridData,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
Tab,
|
||||
Tabs,
|
||||
Link as UiToolkitLink,
|
||||
Indicator,
|
||||
ExternalLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Header, HeaderStat, HeaderTitle } from '../../components/header';
|
||||
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { IGetRowsParams } from 'ag-grid-community';
|
||||
|
||||
import type { LiquidityProvisionData } from '@vegaprotocol/liquidity';
|
||||
import type { LiquidityProvisionData, Filter } from '@vegaprotocol/liquidity';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
|
||||
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
const enum LiquidityTabs {
|
||||
Active = 'active',
|
||||
@@ -67,8 +68,10 @@ const useReloadLiquidityData = (marketId: string | undefined) => {
|
||||
|
||||
export const LiquidityContainer = ({
|
||||
marketId,
|
||||
filter,
|
||||
}: {
|
||||
marketId: string | undefined;
|
||||
filter?: Filter;
|
||||
}) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const { data: market } = useMarket(marketId);
|
||||
@@ -87,7 +90,7 @@ export const LiquidityContainer = ({
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: lpAggregatedDataProvider,
|
||||
update,
|
||||
variables: { marketId: marketId || '' },
|
||||
variables: { marketId: marketId || '', filter },
|
||||
skip: !marketId,
|
||||
});
|
||||
|
||||
@@ -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,183 +215,68 @@ 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>
|
||||
);
|
||||
});
|
||||
LiquidityViewHeader.displayName = 'LiquidityViewHeader';
|
||||
|
||||
const filterLiquidities = (
|
||||
tab: string,
|
||||
liquidities?: LiquidityProvisionData[] | null,
|
||||
pubKey?: string | null
|
||||
) => {
|
||||
switch (tab) {
|
||||
case LiquidityTabs.MyLiquidityProvision:
|
||||
return pubKey
|
||||
? (liquidities || []).filter((e) => e.party.id === pubKey)
|
||||
: [];
|
||||
break;
|
||||
case LiquidityTabs.Active:
|
||||
return (liquidities || []).filter(
|
||||
(e) => e.status === Schema.LiquidityProvisionStatus.STATUS_ACTIVE
|
||||
);
|
||||
case LiquidityTabs.Inactive:
|
||||
return (liquidities || []).filter(
|
||||
(e) => e.status !== Schema.LiquidityProvisionStatus.STATUS_ACTIVE
|
||||
);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const LiquidityViewContainer = ({
|
||||
marketId,
|
||||
}: {
|
||||
marketId: string | undefined;
|
||||
}) => {
|
||||
const [tab, setTab] = useState('');
|
||||
const [tab, setTab] = useState<string | undefined>(undefined);
|
||||
const { pubKey } = useVegaWallet();
|
||||
const [liquidityProviders, setLiquidityProviders] = useState<
|
||||
LiquidityProvisionData[] | null
|
||||
>();
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const { data: market } = useMarket(marketId);
|
||||
const dataRef = useRef<LiquidityProvisionData[] | null>(null);
|
||||
|
||||
// To be removed when liquidityProvision subscriptions are working
|
||||
useReloadLiquidityData(marketId);
|
||||
|
||||
const update = useCallback(
|
||||
({ data }: { data: LiquidityProvisionData[] | null }) => {
|
||||
if (!dataRef.current) {
|
||||
setLiquidityProviders(data);
|
||||
dataRef.current = data;
|
||||
}
|
||||
if (!gridRef.current?.api) {
|
||||
return false;
|
||||
}
|
||||
const updateRows: LiquidityProvisionData[] = [];
|
||||
const addRows: LiquidityProvisionData[] = [];
|
||||
if (gridRef.current?.api?.getModel().getType() === 'infinite') {
|
||||
dataRef.current = data;
|
||||
gridRef.current.api.refreshInfiniteCache();
|
||||
} else {
|
||||
const filteredData = filterLiquidities(tab, data, pubKey as string);
|
||||
filteredData?.forEach((d) => {
|
||||
const rowNode = gridRef.current?.api?.getRowNode(getId(d));
|
||||
if (rowNode) {
|
||||
if (!isEqual(rowNode.data, d)) {
|
||||
updateRows.push(d);
|
||||
}
|
||||
} else {
|
||||
addRows.push(d);
|
||||
}
|
||||
});
|
||||
gridRef.current?.api?.applyTransaction({
|
||||
update: updateRows,
|
||||
add: addRows,
|
||||
addIndex: 0,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[gridRef, tab, pubKey]
|
||||
);
|
||||
|
||||
const { loading, error } = useDataProvider({
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: lpAggregatedDataProvider,
|
||||
update,
|
||||
skipUpdates: true,
|
||||
variables: { marketId: marketId || '' },
|
||||
skip: !marketId,
|
||||
});
|
||||
const assetDecimalPlaces =
|
||||
market?.tradableInstrument.instrument.product.settlementAsset.decimals || 0;
|
||||
const symbol =
|
||||
market?.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.market_liquidity_stakeToCcyVolume,
|
||||
NetworkParams.market_liquidity_targetstake_triggering_ratio,
|
||||
]);
|
||||
const stakeToCcyVolume = params.market_liquidity_stakeToCcyVolume;
|
||||
const myLpEdges = useMemo(
|
||||
() =>
|
||||
filterLiquidities(
|
||||
LiquidityTabs.MyLiquidityProvision,
|
||||
liquidityProviders,
|
||||
pubKey
|
||||
),
|
||||
[liquidityProviders, pubKey]
|
||||
);
|
||||
const activeEdges = useMemo(
|
||||
() => filterLiquidities(LiquidityTabs.Active, liquidityProviders),
|
||||
[liquidityProviders]
|
||||
);
|
||||
const inactiveEdges = useMemo(
|
||||
() => filterLiquidities(LiquidityTabs.Inactive, liquidityProviders),
|
||||
[liquidityProviders]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab) {
|
||||
return;
|
||||
if (data) {
|
||||
if (pubKey && data.some((lp) => matchFilter({ partyId: pubKey }, lp))) {
|
||||
setTab(LiquidityTabs.MyLiquidityProvision);
|
||||
return;
|
||||
}
|
||||
if (data.some((lp) => matchFilter({ active: true }, lp))) {
|
||||
setTab(LiquidityTabs.Active);
|
||||
return;
|
||||
}
|
||||
setTab(LiquidityTabs.Inactive);
|
||||
}
|
||||
let initialTab = LiquidityTabs.Active;
|
||||
if (myLpEdges.length > 0) {
|
||||
initialTab = LiquidityTabs.MyLiquidityProvision;
|
||||
}
|
||||
if (activeEdges?.length) {
|
||||
initialTab = LiquidityTabs.Active;
|
||||
} else if (inactiveEdges.length > 0) {
|
||||
initialTab = LiquidityTabs.Inactive;
|
||||
}
|
||||
setTab(initialTab);
|
||||
}, [tab, myLpEdges?.length, activeEdges?.length, inactiveEdges?.length]);
|
||||
}, [data, pubKey]);
|
||||
|
||||
return (
|
||||
<AsyncRenderer loading={loading} error={error} data={liquidityProviders}>
|
||||
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||
<LiquidityViewHeader marketId={marketId} />
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<Tab
|
||||
id={LiquidityTabs.MyLiquidityProvision}
|
||||
name={t('My liquidity provision')}
|
||||
hidden={!pubKey}
|
||||
>
|
||||
{myLpEdges && (
|
||||
<LiquidityTable
|
||||
ref={gridRef}
|
||||
rowData={myLpEdges}
|
||||
symbol={symbol}
|
||||
stakeToCcyVolume={stakeToCcyVolume}
|
||||
assetDecimalPlaces={assetDecimalPlaces}
|
||||
/>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab id={LiquidityTabs.Active} name={t('Active')}>
|
||||
{activeEdges && (
|
||||
<LiquidityTable
|
||||
ref={gridRef}
|
||||
rowData={activeEdges}
|
||||
symbol={symbol}
|
||||
assetDecimalPlaces={assetDecimalPlaces}
|
||||
stakeToCcyVolume={stakeToCcyVolume}
|
||||
/>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab id={LiquidityTabs.Inactive} name={t('Inactive')}>
|
||||
{inactiveEdges && (
|
||||
<LiquidityTable
|
||||
ref={gridRef}
|
||||
rowData={inactiveEdges}
|
||||
symbol={symbol}
|
||||
assetDecimalPlaces={assetDecimalPlaces}
|
||||
stakeToCcyVolume={stakeToCcyVolume}
|
||||
/>
|
||||
)}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
</AsyncRenderer>
|
||||
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||
<LiquidityViewHeader marketId={marketId} />
|
||||
<Tabs value={tab || LiquidityTabs.Active} onValueChange={setTab}>
|
||||
<Tab
|
||||
id={LiquidityTabs.MyLiquidityProvision}
|
||||
name={t('My liquidity provision')}
|
||||
hidden={!pubKey}
|
||||
>
|
||||
<LiquidityContainer
|
||||
marketId={marketId}
|
||||
filter={{ partyId: pubKey || undefined }}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab id={LiquidityTabs.Active} name={t('Active')}>
|
||||
<LiquidityContainer marketId={marketId} filter={{ active: true }} />
|
||||
</Tab>
|
||||
<Tab id={LiquidityTabs.Inactive} name={t('Inactive')}>
|
||||
<LiquidityContainer marketId={marketId} filter={{ active: false }} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -474,10 +478,11 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
}
|
||||
|
||||
if (tx.order && tx.order.rejectionReason) {
|
||||
const rejectionReason = getRejectionReason(tx.order) || ' ';
|
||||
const rejectionReason =
|
||||
getRejectionReason(tx.order) || tx.order.rejectionReason || '';
|
||||
return (
|
||||
<>
|
||||
<ToastHeading>{t('Order rejected')}</ToastHeading>
|
||||
<ToastHeading>{getOrderToastTitle(tx.order.status)}</ToastHeading>
|
||||
{rejectionReason ? (
|
||||
<p>
|
||||
{t('Your order has been rejected because: %s', [rejectionReason])}
|
||||
@@ -503,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">
|
||||
@@ -577,9 +582,9 @@ 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,
|
||||
orderRejection || tx.order?.rejectionReason || ' ',
|
||||
]);
|
||||
}
|
||||
if (walletError) {
|
||||
@@ -646,9 +651,8 @@ export const useVegaTransactionToasts = () => {
|
||||
|
||||
// Transaction can be successful but the order can be rejected by the network
|
||||
const intent =
|
||||
tx.order && [OrderStatus.STATUS_REJECTED].includes(tx.order.status)
|
||||
? Intent.Danger
|
||||
: intentMap[tx.status];
|
||||
(tx.order && getOrderToastIntent(tx.order.status)) ||
|
||||
intentMap[tx.status];
|
||||
|
||||
return {
|
||||
id: `vega-${tx.id}`,
|
||||
|
||||
@@ -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.500');
|
||||
|
||||
/* depth chart */
|
||||
--pennant-color-depth-buy-fill: theme('colors.vega.green.400');
|
||||
--pennant-color-depth-buy-stroke: theme('colors.vega.green.550');
|
||||
|
||||
+15
-3
@@ -1,10 +1,22 @@
|
||||
#!/bin/sh -eux
|
||||
#!/bin/bash -ex
|
||||
|
||||
export PATH="/app/node_modules/.bin:$PATH"
|
||||
|
||||
flags="--network-timeout 100000 --pure-lockfile"
|
||||
|
||||
if [[ ! -z "${ENV_NAME}" ]]; then
|
||||
flags="--env=${ENV_NAME} $flags"
|
||||
fi
|
||||
|
||||
if [ "${APP}" = "trading" ]; then
|
||||
yarn nx export ${APP} --network-timeout 100000 --pure-lockfile
|
||||
yarn nx export ${APP} $flags
|
||||
mv /app/dist/apps/trading/exported/ /app/tmp
|
||||
rm -rf /app/dist/apps/trading
|
||||
mv /app/tmp /app/dist/apps/trading
|
||||
else
|
||||
yarn nx build ${APP} --network-timeout 100000 --pure-lockfile
|
||||
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"
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Recreate config file
|
||||
env_file=/usr/share/nginx/html/assets/env-config.js
|
||||
mkdir -p $(dirname $env_file)
|
||||
rm -rf $env_file || echo "no file to delete"
|
||||
touch $env_file
|
||||
|
||||
env_vars_file=/usr/share/nginx/html/.env
|
||||
sed -i '/^#/d' $env_vars_file # remove comment lines
|
||||
sed -i '/^$/d' $env_vars_file # remove empty lines
|
||||
|
||||
# Add assignment
|
||||
echo "window._env_ = {" >> $env_file
|
||||
|
||||
# Read each line in .env file
|
||||
# Each line represents key=value pairs
|
||||
while read -r line || [[ -n "$line" ]];
|
||||
do
|
||||
# Split env variables by character `=`
|
||||
if printf '%s\n' "$line" | grep -q -e '='; then
|
||||
varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
|
||||
varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
|
||||
fi
|
||||
|
||||
# Read value of current variable if exists as Environment variable
|
||||
value=$(printf '%s\n' "${!varname}")
|
||||
# Otherwise use value from .env file
|
||||
[[ -z $value ]] && value=${varvalue}
|
||||
|
||||
# Append configuration property to JS file if non-empty
|
||||
if [ ! -z "$varname" ]; then
|
||||
echo " $varname: \"$value\"," >> $env_file
|
||||
fi
|
||||
done < $env_vars_file
|
||||
|
||||
rm $env_vars_file
|
||||
|
||||
echo "}" >> $env_file
|
||||
|
||||
# start serving
|
||||
nginx -g 'daemon off;'
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import classnames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
@@ -11,9 +12,12 @@ interface DealTicketFeeDetailsProps {
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
market: Market;
|
||||
marketData: MarketData;
|
||||
margin: string;
|
||||
totalMargin: string;
|
||||
balance: string;
|
||||
currentInitialMargin?: string;
|
||||
currentMaintenanceMargin?: string;
|
||||
estimatedInitialMargin: string;
|
||||
estimatedTotalInitialMargin: string;
|
||||
marginAccountBalance: string;
|
||||
generalAccountBalance: string;
|
||||
}
|
||||
|
||||
export interface DealTicketFeeDetailProps {
|
||||
@@ -45,23 +49,22 @@ export const DealTicketFeeDetails = ({
|
||||
order,
|
||||
market,
|
||||
marketData,
|
||||
margin,
|
||||
totalMargin,
|
||||
balance,
|
||||
...args
|
||||
}: DealTicketFeeDetailsProps) => {
|
||||
const feeDetails = useFeeDealTicketDetails(order, market, marketData);
|
||||
const details = getFeeDetailsValues({
|
||||
...feeDetails,
|
||||
margin,
|
||||
totalMargin,
|
||||
balance,
|
||||
...args,
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
{details.map(({ label, value, labelDescription, symbol }) => (
|
||||
{details.map(({ label, value, labelDescription, symbol, indent }) => (
|
||||
<div
|
||||
key={typeof label === 'string' ? label : 'value-dropdown'}
|
||||
className="text-xs mt-2 flex justify-between items-center gap-4 flex-wrap"
|
||||
className={classnames(
|
||||
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
|
||||
{ 'ml-2': indent }
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<Tooltip description={labelDescription}>
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('DealTicket', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should use local storage state for initial values reduceOnly and postOnly', () => {
|
||||
it('should set values for a non-persistent reduce only order and disable post only checkbox', () => {
|
||||
const expectedOrder = {
|
||||
marketId: market.id,
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
@@ -115,7 +115,7 @@ describe('DealTicket', () => {
|
||||
size: '0.1',
|
||||
price: '300.22',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
persist: true,
|
||||
persist: false,
|
||||
reduceOnly: true,
|
||||
postOnly: false,
|
||||
};
|
||||
@@ -149,6 +149,58 @@ describe('DealTicket', () => {
|
||||
expect(screen.getByTestId('order-price')).toHaveDisplayValue(
|
||||
expectedOrder.price
|
||||
);
|
||||
expect(screen.getByTestId('post-only')).toBeDisabled();
|
||||
expect(screen.getByTestId('reduce-only')).toBeEnabled();
|
||||
expect(screen.getByTestId('reduce-only')).toBeChecked();
|
||||
expect(screen.getByTestId('post-only')).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should set values for a persistent post only order and disable reduce only checkbox', () => {
|
||||
const expectedOrder = {
|
||||
marketId: market.id,
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '0.1',
|
||||
price: '300.22',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
persist: true,
|
||||
reduceOnly: false,
|
||||
postOnly: true,
|
||||
};
|
||||
|
||||
useOrderStore.setState({
|
||||
orders: {
|
||||
[expectedOrder.marketId]: expectedOrder,
|
||||
},
|
||||
});
|
||||
|
||||
render(generateJsx());
|
||||
|
||||
// Assert correct defaults are used from store
|
||||
expect(
|
||||
screen
|
||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
||||
.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
||||
).not.toBeChecked();
|
||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||
expectedOrder.size
|
||||
);
|
||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||
expectedOrder.timeInForce
|
||||
);
|
||||
expect(screen.getByTestId('order-price')).toHaveDisplayValue(
|
||||
expectedOrder.price
|
||||
);
|
||||
expect(screen.getByTestId('post-only')).toBeEnabled();
|
||||
expect(screen.getByTestId('reduce-only')).toBeDisabled();
|
||||
expect(screen.getByTestId('post-only')).toBeChecked();
|
||||
expect(screen.getByTestId('reduce-only')).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('handles TIF select box dependent on order type', async () => {
|
||||
|
||||
@@ -44,6 +44,9 @@ import {
|
||||
|
||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import { useOrderForm } from '../../hooks/use-order-form';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
|
||||
import { marketMarginDataProvider } from '@vegaprotocol/positions';
|
||||
|
||||
export interface DealTicketProps {
|
||||
market: Market;
|
||||
@@ -103,6 +106,12 @@ export const DealTicket = ({
|
||||
|
||||
const { margin, totalMargin } = useInitialMargin(market.id, normalizedOrder);
|
||||
|
||||
const { data: currentMargins } = useDataProvider({
|
||||
dataProvider: marketMarginDataProvider,
|
||||
variables: { marketId: market.id, partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!pubKey) {
|
||||
setError('summary', {
|
||||
@@ -158,6 +167,16 @@ export const DealTicket = ({
|
||||
return disabled;
|
||||
}, [order]);
|
||||
|
||||
const disableReduceOnlyCheckbox = useMemo(() => {
|
||||
const disabled = order
|
||||
? ![
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
].includes(order.timeInForce)
|
||||
: true;
|
||||
return disabled;
|
||||
}, [order]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(order: OrderSubmission) => {
|
||||
const now = new Date().getTime();
|
||||
@@ -202,8 +221,18 @@ export const DealTicket = ({
|
||||
if (type === OrderType.TYPE_NETWORK) return;
|
||||
update({
|
||||
type,
|
||||
// when changing type also update the tif to what was last used of new type
|
||||
// when changing type also update the TIF to what was last used of new type
|
||||
timeInForce: lastTIF[type] || order.timeInForce,
|
||||
postOnly:
|
||||
type === OrderType.TYPE_MARKET ? false : order.postOnly,
|
||||
reduceOnly:
|
||||
type === OrderType.TYPE_LIMIT &&
|
||||
![
|
||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(lastTIF[type] || order.timeInForce)
|
||||
? false
|
||||
: order.postOnly,
|
||||
expiresAt: undefined,
|
||||
});
|
||||
clearErrors('expiresAt');
|
||||
@@ -251,8 +280,23 @@ export const DealTicket = ({
|
||||
value={order.timeInForce}
|
||||
orderType={order.type}
|
||||
onSelect={(timeInForce) => {
|
||||
update({ timeInForce, postOnly: false, reduceOnly: false });
|
||||
// Set tif value for the given order type, so that when switching
|
||||
// Reset post only and reduce only when changing TIF
|
||||
update({
|
||||
timeInForce,
|
||||
postOnly: [
|
||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(timeInForce)
|
||||
? false
|
||||
: order.postOnly,
|
||||
reduceOnly: ![
|
||||
OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
].includes(timeInForce)
|
||||
? false
|
||||
: order.reduceOnly,
|
||||
});
|
||||
// Set TIF value for the given order type, so that when switching
|
||||
// types we know the last used TIF for the given order type
|
||||
setLastTIF((curr) => ({
|
||||
...curr,
|
||||
@@ -327,6 +371,7 @@ export const DealTicket = ({
|
||||
<Checkbox
|
||||
name="reduce-only"
|
||||
checked={order.reduceOnly}
|
||||
disabled={disableReduceOnlyCheckbox}
|
||||
onCheckedChange={() => {
|
||||
update({ postOnly: false, reduceOnly: !order.reduceOnly });
|
||||
}}
|
||||
@@ -334,9 +379,13 @@ export const DealTicket = ({
|
||||
<Tooltip
|
||||
description={
|
||||
<span>
|
||||
{t(
|
||||
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.'
|
||||
)}
|
||||
{disableReduceOnlyCheckbox
|
||||
? t(
|
||||
'"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".'
|
||||
)
|
||||
: t(
|
||||
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.'
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
@@ -367,9 +416,12 @@ export const DealTicket = ({
|
||||
order={normalizedOrder}
|
||||
market={market}
|
||||
marketData={marketData}
|
||||
margin={margin}
|
||||
totalMargin={totalMargin}
|
||||
balance={marginAccountBalance}
|
||||
estimatedInitialMargin={margin}
|
||||
estimatedTotalInitialMargin={totalMargin}
|
||||
currentInitialMargin={currentMargins?.initialLevel}
|
||||
currentMaintenanceMargin={currentMargins?.maintenanceLevel}
|
||||
marginAccountBalance={marginAccountBalance}
|
||||
generalAccountBalance={generalAccountBalance}
|
||||
/>
|
||||
</form>
|
||||
</TinyScroll>
|
||||
|
||||
@@ -10,12 +10,36 @@ export const EST_MARGIN_TOOLTIP_TEXT = (settlementAsset: string) =>
|
||||
export const EST_TOTAL_MARGIN_TOOLTIP_TEXT = t(
|
||||
'Estimated total margin that will cover open position, active orders and this order.'
|
||||
);
|
||||
export const MARGIN_ACCOUNT_TOOLTIP_TEXT = t('Margin account balance');
|
||||
export const MARGIN_ACCOUNT_TOOLTIP_TEXT = t('Margin account balance.');
|
||||
export const MARGIN_DIFF_TOOLTIP_TEXT = (settlementAsset: string) =>
|
||||
t(
|
||||
"The additional margin required for your new position (taking into account volume and open orders), compared to your current margin. Measured in the market's settlement asset (%s).",
|
||||
[settlementAsset]
|
||||
);
|
||||
export const DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT = (
|
||||
settlementAsset: string
|
||||
) =>
|
||||
t(
|
||||
'To cover the required margin, this amount will be drawn from your general (%s) account.',
|
||||
[settlementAsset]
|
||||
);
|
||||
|
||||
export const TOTAL_MARGIN_AVAILABLE = (
|
||||
generalAccountBalance: string,
|
||||
marginAccountBalance: string,
|
||||
marginMaintenance: string,
|
||||
settlementAsset: string
|
||||
) =>
|
||||
t(
|
||||
'Total margin available = general %s balance (%s) + margin balance (%s) - maintenance level (%s).',
|
||||
[
|
||||
settlementAsset,
|
||||
`${generalAccountBalance} ${settlementAsset}`,
|
||||
`${marginAccountBalance} ${settlementAsset}`,
|
||||
`${marginMaintenance} ${settlementAsset}`,
|
||||
]
|
||||
);
|
||||
|
||||
export const CONTRACTS_MARGIN_TOOLTIP_TEXT = t(
|
||||
'The number of contracts determines how many units of the futures contract to buy or sell. For example, this is similar to buying one share of a listed company. The value of 1 contract is equivalent to the price of the contract. For example, if the current price is $50, then one contract is worth $50.'
|
||||
);
|
||||
@@ -40,7 +64,7 @@ export const EST_SLIPPAGE = t(
|
||||
);
|
||||
|
||||
export const ERROR_SIZE_DECIMAL = t(
|
||||
'The size field accepts up to X decimal places'
|
||||
'The size field accepts up to X decimal places.'
|
||||
);
|
||||
|
||||
export enum MarketModeValidationType {
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
NOTIONAL_SIZE_TOOLTIP_TEXT,
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
||||
TOTAL_MARGIN_AVAILABLE,
|
||||
} from '../constants';
|
||||
import { useOrderCloseOut } from './use-order-closeout';
|
||||
import { useMarketAccountBalance } from '@vegaprotocol/accounts';
|
||||
@@ -85,24 +87,32 @@ export const useFeeDealTicketDetails = (
|
||||
};
|
||||
|
||||
export interface FeeDetails {
|
||||
balance: string;
|
||||
generalAccountBalance?: string;
|
||||
marginAccountBalance?: string;
|
||||
market: Market;
|
||||
assetSymbol: string;
|
||||
notionalSize: string | null;
|
||||
estCloseOut: string | null;
|
||||
estimateOrder: EstimateOrderQuery['estimateOrder'] | undefined;
|
||||
margin: string;
|
||||
totalMargin: string;
|
||||
estimatedInitialMargin: string;
|
||||
estimatedTotalInitialMargin: string;
|
||||
currentInitialMargin?: string;
|
||||
currentMaintenanceMargin?: string;
|
||||
}
|
||||
|
||||
export const getFeeDetailsValues = ({
|
||||
balance,
|
||||
marginAccountBalance,
|
||||
generalAccountBalance,
|
||||
assetSymbol,
|
||||
estimateOrder,
|
||||
market,
|
||||
notionalSize,
|
||||
totalMargin,
|
||||
estimatedTotalInitialMargin,
|
||||
currentInitialMargin,
|
||||
currentMaintenanceMargin,
|
||||
}: FeeDetails) => {
|
||||
const totalBalance =
|
||||
BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0');
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
const formatValueWithMarketDp = (
|
||||
@@ -123,7 +133,8 @@ export const getFeeDetailsValues = ({
|
||||
label: string;
|
||||
value?: string | null;
|
||||
symbol: string;
|
||||
labelDescription: React.ReactNode;
|
||||
indent?: boolean;
|
||||
labelDescription?: React.ReactNode;
|
||||
}[] = [
|
||||
{
|
||||
label: t('Notional'),
|
||||
@@ -153,38 +164,64 @@ export const getFeeDetailsValues = ({
|
||||
),
|
||||
symbol: assetSymbol,
|
||||
},
|
||||
/*
|
||||
{
|
||||
label: t('Initial margin'),
|
||||
value: margin && `~${formatValueWithAssetDp(margin)}`,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: EST_MARGIN_TOOLTIP_TEXT(assetSymbol),
|
||||
},
|
||||
*/
|
||||
{
|
||||
label: t('Margin required'),
|
||||
value: `~${formatValueWithAssetDp(
|
||||
balance
|
||||
? (BigInt(totalMargin) - BigInt(balance)).toString()
|
||||
: totalMargin
|
||||
currentInitialMargin
|
||||
? (
|
||||
BigInt(estimatedTotalInitialMargin) - BigInt(currentInitialMargin)
|
||||
).toString()
|
||||
: estimatedTotalInitialMargin
|
||||
)}`,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol),
|
||||
},
|
||||
];
|
||||
if (balance) {
|
||||
if (totalBalance) {
|
||||
const totalMarginAvailable = (
|
||||
currentMaintenanceMargin
|
||||
? totalBalance - BigInt(currentMaintenanceMargin)
|
||||
: totalBalance
|
||||
).toString();
|
||||
|
||||
details.push({
|
||||
indent: true,
|
||||
label: t('Total margin available'),
|
||||
value: `~${formatValueWithAssetDp(totalMarginAvailable)}`,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: TOTAL_MARGIN_AVAILABLE(
|
||||
formatValueWithAssetDp(generalAccountBalance),
|
||||
formatValueWithAssetDp(marginAccountBalance),
|
||||
formatValueWithAssetDp(currentMaintenanceMargin),
|
||||
assetSymbol
|
||||
),
|
||||
});
|
||||
|
||||
if (marginAccountBalance) {
|
||||
const deductionFromCollateral =
|
||||
BigInt(estimatedTotalInitialMargin) - BigInt(marginAccountBalance);
|
||||
|
||||
details.push({
|
||||
indent: true,
|
||||
label: t('Deduction from collateral'),
|
||||
value: `~${formatValueWithAssetDp(
|
||||
deductionFromCollateral > 0 ? deductionFromCollateral.toString() : '0'
|
||||
)}`,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT(assetSymbol),
|
||||
});
|
||||
}
|
||||
|
||||
details.push({
|
||||
label: t('Projected margin'),
|
||||
value: `~${formatValueWithAssetDp(totalMargin)}`,
|
||||
value: `~${formatValueWithAssetDp(estimatedTotalInitialMargin)}`,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
});
|
||||
}
|
||||
details.push({
|
||||
label: t('Current margin allocation'),
|
||||
value: balance
|
||||
? `~${formatValueWithAssetDp(balance)}`
|
||||
: `${formatValueWithAssetDp(balance)}`,
|
||||
value: `${formatValueWithAssetDp(marginAccountBalance)}`,
|
||||
symbol: assetSymbol,
|
||||
labelDescription: MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
});
|
||||
|
||||
@@ -65,5 +65,11 @@ export const useInitialMargin = (
|
||||
sellMargin > buyMargin ? sellMargin.toString() : buyMargin.toString();
|
||||
}
|
||||
|
||||
return useMemo(() => ({ totalMargin, margin }), [totalMargin, margin]);
|
||||
return useMemo(
|
||||
() => ({
|
||||
totalMargin,
|
||||
margin,
|
||||
}),
|
||||
[totalMargin, margin]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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} 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>}
|
||||
</>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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,26 @@ export const DepositManager = ({
|
||||
closeDepositDialog();
|
||||
};
|
||||
|
||||
const onAmountChange = useCallback(
|
||||
(amount: string) => {
|
||||
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([{ assetId: '' }, 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,42 @@
|
||||
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, (entry: PersistedDeposit) => void] => {
|
||||
const { deposits, lastVisited, saveValue } = usePersistentDepositStore();
|
||||
const discoveredData = useMemo(() => {
|
||||
return deposits[assetId || ''] || lastVisited || { assetId: assetId || '' };
|
||||
}, [deposits, lastVisited, assetId]);
|
||||
|
||||
return [discoveredData, saveValue];
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
@@ -140,8 +140,6 @@ describe('Network switcher', () => {
|
||||
[Networks.STAGNET3]: 'https://stag3.net',
|
||||
[Networks.DEVNET]: 'https://dev.net',
|
||||
[Networks.STAGNET1]: 'https://stag1.net',
|
||||
[Networks.SANDBOX]: 'https://sandbox.net',
|
||||
[Networks.MIRROR]: 'https://mirror.net',
|
||||
};
|
||||
// @ts-ignore Typescript doesn't know about this module being mocked
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
@@ -181,8 +179,6 @@ describe('Network switcher', () => {
|
||||
[Networks.STAGNET3]: 'https://stag3.net',
|
||||
[Networks.DEVNET]: 'https://dev.net',
|
||||
[Networks.STAGNET1]: 'https://stag1.net',
|
||||
[Networks.SANDBOX]: 'https://sandbox.net',
|
||||
[Networks.MIRROR]: 'https://mirror.net',
|
||||
};
|
||||
// @ts-ignore Typescript doesn't know about this module being mocked
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
@@ -215,8 +211,6 @@ describe('Network switcher', () => {
|
||||
[Networks.STAGNET3]: 'https://stag3.net',
|
||||
[Networks.DEVNET]: 'https://dev.net',
|
||||
[Networks.STAGNET1]: 'https://stag1.net',
|
||||
[Networks.SANDBOX]: 'https://sandbox.net',
|
||||
[Networks.MIRROR]: 'https://mirror.net',
|
||||
};
|
||||
// @ts-ignore Typescript doesn't know about this module being mocked
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
|
||||
@@ -17,12 +17,10 @@ export const envNameMapping: Record<Networks, string> = {
|
||||
[Networks.VALIDATOR_TESTNET]: t('VALIDATOR_TESTNET'),
|
||||
[Networks.CUSTOM]: t('Custom'),
|
||||
[Networks.DEVNET]: t('Devnet'),
|
||||
[Networks.SANDBOX]: t('Sandbox'),
|
||||
[Networks.STAGNET1]: t('Stagnet'),
|
||||
[Networks.STAGNET3]: t('Stagnet3'),
|
||||
[Networks.TESTNET]: t('Fairground testnet'),
|
||||
[Networks.MAINNET]: t('Mainnet'),
|
||||
[Networks.MIRROR]: t('Mainnet mirror'),
|
||||
};
|
||||
|
||||
export const envTriggerMapping: Record<Networks, string> = {
|
||||
@@ -32,7 +30,6 @@ export const envTriggerMapping: Record<Networks, string> = {
|
||||
|
||||
export const envDescriptionMapping: Record<Networks, string> = {
|
||||
[Networks.CUSTOM]: '',
|
||||
[Networks.SANDBOX]: t('A playground test environment'),
|
||||
[Networks.VALIDATOR_TESTNET]: t('The validator deployed testnet'),
|
||||
[Networks.DEVNET]: t('The latest Vega code auto-deployed'),
|
||||
[Networks.STAGNET1]: t('A release candidate for the staging environment'),
|
||||
@@ -41,9 +38,6 @@ export const envDescriptionMapping: Record<Networks, string> = {
|
||||
'Public testnet run by the Vega team, often used for incentives'
|
||||
),
|
||||
[Networks.MAINNET]: t('The vega mainnet'),
|
||||
[Networks.MIRROR]: t(
|
||||
'A mirror of the mainnet environment running on an Ethereum test network'
|
||||
),
|
||||
};
|
||||
|
||||
const standardNetworkKeys = [Networks.MAINNET, Networks.TESTNET];
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useCallback } from 'react';
|
||||
import { Networks } from '../types';
|
||||
import { useEnvironment } from './use-environment';
|
||||
|
||||
type Net = Exclude<Networks, 'CUSTOM' | 'SANDBOX'>;
|
||||
type Net = Exclude<Networks, 'CUSTOM'>;
|
||||
export enum DApp {
|
||||
Explorer = 'Explorer',
|
||||
Console = 'Console',
|
||||
@@ -60,7 +60,7 @@ export const useLinks = (dapp: DApp, network?: Net) => {
|
||||
};
|
||||
|
||||
let net = network || VEGA_ENV;
|
||||
if (net === Networks.CUSTOM || net === Networks.SANDBOX) {
|
||||
if (net === Networks.CUSTOM) {
|
||||
net = Networks.TESTNET;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,11 @@ import type { envSchema } from './utils/validate-environment';
|
||||
export enum Networks {
|
||||
VALIDATOR_TESTNET = 'VALIDATOR_TESTNET',
|
||||
CUSTOM = 'CUSTOM',
|
||||
SANDBOX = 'SANDBOX',
|
||||
TESTNET = 'TESTNET',
|
||||
STAGNET1 = 'STAGNET1',
|
||||
STAGNET3 = 'STAGNET3',
|
||||
DEVNET = 'DEVNET',
|
||||
MAINNET = 'MAINNET',
|
||||
MIRROR = 'MIRROR',
|
||||
}
|
||||
export type Environment = z.infer<typeof envSchema>;
|
||||
export type Configuration = z.infer<typeof tomlConfigSchema>;
|
||||
|
||||
@@ -3,13 +3,11 @@ import z from 'zod';
|
||||
export enum Networks {
|
||||
VALIDATOR_TESTNET = 'VALIDATOR_TESTNET',
|
||||
CUSTOM = 'CUSTOM',
|
||||
SANDBOX = 'SANDBOX',
|
||||
TESTNET = 'TESTNET',
|
||||
STAGNET1 = 'STAGNET1',
|
||||
STAGNET3 = 'STAGNET3',
|
||||
DEVNET = 'DEVNET',
|
||||
MAINNET = 'MAINNET',
|
||||
MIRROR = 'MIRROR',
|
||||
}
|
||||
|
||||
const schemaObject = {
|
||||
|
||||
@@ -103,13 +103,3 @@ query LiquidityProviderFeeShare($marketId: ID!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscription LiquidityProviderFeeShareUpdate($marketId: ID!) {
|
||||
marketsData(marketIds: [$marketId]) {
|
||||
liquidityProviderFeeShare {
|
||||
partyId
|
||||
equityLikeShare
|
||||
averageEntryValuation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-42
@@ -36,13 +36,6 @@ export type LiquidityProviderFeeShareQueryVariables = Types.Exact<{
|
||||
|
||||
export type LiquidityProviderFeeShareQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, data?: { __typename?: 'MarketData', market: { __typename?: 'Market', id: string }, liquidityProviderFeeShare?: Array<{ __typename?: 'LiquidityProviderFeeShare', equityLikeShare: string, averageEntryValuation: string, party: { __typename?: 'Party', id: string } }> | null } | null } | null };
|
||||
|
||||
export type LiquidityProviderFeeShareUpdateSubscriptionVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type LiquidityProviderFeeShareUpdateSubscription = { __typename?: 'Subscription', marketsData: Array<{ __typename?: 'ObservableMarketData', liquidityProviderFeeShare?: Array<{ __typename?: 'ObservableLiquidityProviderFeeShare', partyId: string, equityLikeShare: string, averageEntryValuation: string }> | null }> };
|
||||
|
||||
export const LiquidityProvisionFieldsFragmentDoc = gql`
|
||||
fragment LiquidityProvisionFields on LiquidityProvision {
|
||||
party {
|
||||
@@ -256,38 +249,4 @@ export function useLiquidityProviderFeeShareLazyQuery(baseOptions?: Apollo.LazyQ
|
||||
}
|
||||
export type LiquidityProviderFeeShareQueryHookResult = ReturnType<typeof useLiquidityProviderFeeShareQuery>;
|
||||
export type LiquidityProviderFeeShareLazyQueryHookResult = ReturnType<typeof useLiquidityProviderFeeShareLazyQuery>;
|
||||
export type LiquidityProviderFeeShareQueryResult = Apollo.QueryResult<LiquidityProviderFeeShareQuery, LiquidityProviderFeeShareQueryVariables>;
|
||||
export const LiquidityProviderFeeShareUpdateDocument = gql`
|
||||
subscription LiquidityProviderFeeShareUpdate($marketId: ID!) {
|
||||
marketsData(marketIds: [$marketId]) {
|
||||
liquidityProviderFeeShare {
|
||||
partyId
|
||||
equityLikeShare
|
||||
averageEntryValuation
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useLiquidityProviderFeeShareUpdateSubscription__
|
||||
*
|
||||
* To run a query within a React component, call `useLiquidityProviderFeeShareUpdateSubscription` and pass it any options that fit your needs.
|
||||
* When your component renders, `useLiquidityProviderFeeShareUpdateSubscription` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useLiquidityProviderFeeShareUpdateSubscription({
|
||||
* variables: {
|
||||
* marketId: // value for 'marketId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useLiquidityProviderFeeShareUpdateSubscription(baseOptions: Apollo.SubscriptionHookOptions<LiquidityProviderFeeShareUpdateSubscription, LiquidityProviderFeeShareUpdateSubscriptionVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useSubscription<LiquidityProviderFeeShareUpdateSubscription, LiquidityProviderFeeShareUpdateSubscriptionVariables>(LiquidityProviderFeeShareUpdateDocument, options);
|
||||
}
|
||||
export type LiquidityProviderFeeShareUpdateSubscriptionHookResult = ReturnType<typeof useLiquidityProviderFeeShareUpdateSubscription>;
|
||||
export type LiquidityProviderFeeShareUpdateSubscriptionResult = Apollo.SubscriptionResult<LiquidityProviderFeeShareUpdateSubscription>;
|
||||
export type LiquidityProviderFeeShareQueryResult = Apollo.QueryResult<LiquidityProviderFeeShareQuery, LiquidityProviderFeeShareQueryVariables>;
|
||||
@@ -6,7 +6,6 @@ import produce from 'immer';
|
||||
|
||||
import {
|
||||
LiquidityProviderFeeShareDocument,
|
||||
LiquidityProviderFeeShareUpdateDocument,
|
||||
LiquidityProvisionsDocument,
|
||||
LiquidityProvisionsUpdateDocument,
|
||||
MarketLpDocument,
|
||||
@@ -18,7 +17,6 @@ import type {
|
||||
LiquidityProviderFeeShareFieldsFragment,
|
||||
LiquidityProviderFeeShareQuery,
|
||||
LiquidityProviderFeeShareQueryVariables,
|
||||
LiquidityProviderFeeShareUpdateSubscription,
|
||||
LiquidityProvisionFieldsFragment,
|
||||
LiquidityProvisionsQuery,
|
||||
LiquidityProvisionsQueryVariables,
|
||||
@@ -115,73 +113,94 @@ export const marketLiquidityDataProvider = makeDataProvider<
|
||||
export const liquidityFeeShareDataProvider = makeDataProvider<
|
||||
LiquidityProviderFeeShareQuery,
|
||||
LiquidityProviderFeeShareFieldsFragment[],
|
||||
LiquidityProviderFeeShareUpdateSubscription,
|
||||
LiquidityProviderFeeShareUpdateSubscription['marketsData'][0]['liquidityProviderFeeShare'],
|
||||
never,
|
||||
never,
|
||||
LiquidityProviderFeeShareQueryVariables
|
||||
>({
|
||||
query: LiquidityProviderFeeShareDocument,
|
||||
subscriptionQuery: LiquidityProviderFeeShareUpdateDocument,
|
||||
update: (
|
||||
data: LiquidityProviderFeeShareFieldsFragment[] | null,
|
||||
deltas: LiquidityProviderFeeShareUpdateSubscription['marketsData'][0]['liquidityProviderFeeShare']
|
||||
) => {
|
||||
return produce(data || [], (draft) => {
|
||||
deltas?.forEach((delta) => {
|
||||
const id = delta.partyId;
|
||||
const index = draft.findIndex((a) => a.party.id === id);
|
||||
if (index !== -1) {
|
||||
draft[index].equityLikeShare = delta.equityLikeShare;
|
||||
draft[index].averageEntryValuation = delta.averageEntryValuation;
|
||||
} else {
|
||||
draft.unshift({
|
||||
equityLikeShare: delta.equityLikeShare,
|
||||
averageEntryValuation: delta.averageEntryValuation,
|
||||
party: {
|
||||
id: delta.partyId,
|
||||
},
|
||||
// TODO add accounts connection to the subscription
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
getData: (data) => {
|
||||
return data?.market?.data?.liquidityProviderFeeShare || [];
|
||||
},
|
||||
getDelta: (subscriptionData: LiquidityProviderFeeShareUpdateSubscription) => {
|
||||
return subscriptionData.marketsData[0].liquidityProviderFeeShare;
|
||||
},
|
||||
});
|
||||
|
||||
export type Filter = { partyId?: string; active?: boolean };
|
||||
|
||||
export const lpAggregatedDataProvider = makeDerivedDataProvider<
|
||||
ReturnType<typeof getLiquidityProvision>,
|
||||
LiquidityProvisionData[],
|
||||
never,
|
||||
MarketLpQueryVariables
|
||||
MarketLpQueryVariables & { filter?: Filter }
|
||||
>(
|
||||
[
|
||||
liquidityProvisionsDataProvider,
|
||||
marketLiquidityDataProvider,
|
||||
liquidityFeeShareDataProvider,
|
||||
(callback, client, variables) =>
|
||||
liquidityProvisionsDataProvider(callback, client, {
|
||||
marketId: variables.marketId,
|
||||
}),
|
||||
(callback, client, variables) =>
|
||||
marketLiquidityDataProvider(callback, client, {
|
||||
marketId: variables.marketId,
|
||||
}),
|
||||
(callback, client, variables) =>
|
||||
liquidityFeeShareDataProvider(callback, client, {
|
||||
marketId: variables.marketId,
|
||||
}),
|
||||
],
|
||||
([
|
||||
liquidityProvisions,
|
||||
marketLiquidity,
|
||||
liquidityFeeShare,
|
||||
]): LiquidityProvisionData[] => {
|
||||
(
|
||||
[liquidityProvisions, marketLiquidity, liquidityFeeShare],
|
||||
{ filter }
|
||||
): LiquidityProvisionData[] => {
|
||||
return getLiquidityProvision(
|
||||
liquidityProvisions,
|
||||
marketLiquidity,
|
||||
liquidityFeeShare
|
||||
liquidityFeeShare,
|
||||
filter
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const matchFilter = (
|
||||
filter: Filter,
|
||||
lp: LiquidityProvisionFieldsFragment
|
||||
) => {
|
||||
if (filter.partyId && lp.party.id !== filter.partyId) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter.active === true &&
|
||||
lp.status !== Schema.LiquidityProvisionStatus.STATUS_ACTIVE
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter.active === false &&
|
||||
lp.status === Schema.LiquidityProvisionStatus.STATUS_ACTIVE
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getLiquidityProvision = (
|
||||
liquidityProvisions: LiquidityProvisionFieldsFragment[],
|
||||
marketLiquidity: MarketLpQuery,
|
||||
liquidityFeeShare: LiquidityProviderFeeShareFieldsFragment[]
|
||||
liquidityFeeShare: LiquidityProviderFeeShareFieldsFragment[],
|
||||
filter?: Filter
|
||||
): LiquidityProvisionData[] => {
|
||||
return liquidityProvisions
|
||||
.filter((lp) => {
|
||||
if (
|
||||
![
|
||||
Schema.LiquidityProvisionStatus.STATUS_ACTIVE,
|
||||
Schema.LiquidityProvisionStatus.STATUS_UNDEPLOYED,
|
||||
Schema.LiquidityProvisionStatus.STATUS_PENDING,
|
||||
].includes(lp.status)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (filter && !matchFilter(filter, lp)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((lp) => {
|
||||
const market = marketLiquidity?.market;
|
||||
const feeShare = liquidityFeeShare.find(
|
||||
@@ -210,14 +229,7 @@ export const getLiquidityProvision = (
|
||||
.decimals,
|
||||
balance,
|
||||
};
|
||||
})
|
||||
.filter((e) =>
|
||||
[
|
||||
Schema.LiquidityProvisionStatus.STATUS_ACTIVE,
|
||||
Schema.LiquidityProvisionStatus.STATUS_UNDEPLOYED,
|
||||
Schema.LiquidityProvisionStatus.STATUS_PENDING,
|
||||
].includes(e.status)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export interface LiquidityProvisionData
|
||||
|
||||
@@ -48,12 +48,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 +148,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 +161,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 +173,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
return () => {
|
||||
throttleRunner.cancel();
|
||||
};
|
||||
}, [data, marketData, resolution]);
|
||||
}, [data, resolution]);
|
||||
|
||||
useEffect(() => {
|
||||
resolutionRef.current = resolution;
|
||||
|
||||
@@ -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) => {
|
||||
@@ -40,6 +51,7 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
|
||||
);
|
||||
}}
|
||||
onMarketClick={onSelect}
|
||||
onFilterChanged={onFilterChanged}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
@@ -47,6 +59,7 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
|
||||
error={error}
|
||||
data={data}
|
||||
noDataMessage={t('No markets')}
|
||||
noDataCondition={() => !dataCount}
|
||||
reload={reload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,8 @@ fragment OrderFields on Order {
|
||||
expiresAt
|
||||
createdAt
|
||||
updatedAt
|
||||
postOnly
|
||||
reduceOnly
|
||||
liquidityProvision {
|
||||
__typename
|
||||
}
|
||||
@@ -30,12 +32,16 @@ query OrderById($orderId: ID!) {
|
||||
|
||||
query Orders(
|
||||
$partyId: ID!
|
||||
$marketIds: [ID!]
|
||||
$pagination: Pagination
|
||||
$filter: OrderByMarketIdsFilter
|
||||
$filter: OrderFilter
|
||||
) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
ordersConnection(pagination: $pagination, filter: $filter) {
|
||||
ordersConnection(
|
||||
pagination: $pagination
|
||||
filter: { order: $filter, marketIds: $marketIds }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
...OrderFields
|
||||
@@ -72,8 +78,8 @@ fragment OrderUpdateFields on OrderUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
subscription OrdersUpdate($partyId: ID!) {
|
||||
orders(filter: { partyIds: [$partyId] }) {
|
||||
subscription OrdersUpdate($partyId: ID!, $marketIds: [ID!]) {
|
||||
orders(filter: { partyIds: [$partyId], marketIds: $marketIds }) {
|
||||
...OrderUpdateFields
|
||||
}
|
||||
}
|
||||
|
||||
+17
-8
@@ -3,28 +3,30 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type OrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null };
|
||||
export type OrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, postOnly?: boolean | null, reduceOnly?: boolean | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null };
|
||||
|
||||
export type OrderByIdQueryVariables = Types.Exact<{
|
||||
orderId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type OrderByIdQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null } };
|
||||
export type OrderByIdQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, postOnly?: boolean | null, reduceOnly?: boolean | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null } };
|
||||
|
||||
export type OrdersQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
|
||||
pagination?: Types.InputMaybe<Types.Pagination>;
|
||||
filter?: Types.InputMaybe<Types.OrderByMarketIdsFilter>;
|
||||
filter?: Types.InputMaybe<Types.OrderFilter>;
|
||||
}>;
|
||||
|
||||
|
||||
export type OrdersQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, ordersConnection?: { __typename?: 'OrderConnection', edges?: Array<{ __typename?: 'OrderEdge', cursor?: string | null, node: { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null } }> | null, pageInfo?: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } | null } | null } | null };
|
||||
export type OrdersQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, ordersConnection?: { __typename?: 'OrderConnection', edges?: Array<{ __typename?: 'OrderEdge', cursor?: string | null, node: { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, postOnly?: boolean | null, reduceOnly?: boolean | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null } }> | null, pageInfo?: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } | null } | null } | null };
|
||||
|
||||
export type OrderUpdateFieldsFragment = { __typename?: 'OrderUpdate', id: string, marketId: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, liquidityProvisionId?: string | null, peggedOrder?: { __typename: 'PeggedOrder' } | null };
|
||||
|
||||
export type OrdersUpdateSubscriptionVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -47,6 +49,8 @@ export const OrderFieldsFragmentDoc = gql`
|
||||
expiresAt
|
||||
createdAt
|
||||
updatedAt
|
||||
postOnly
|
||||
reduceOnly
|
||||
liquidityProvision {
|
||||
__typename
|
||||
}
|
||||
@@ -112,10 +116,13 @@ export type OrderByIdQueryHookResult = ReturnType<typeof useOrderByIdQuery>;
|
||||
export type OrderByIdLazyQueryHookResult = ReturnType<typeof useOrderByIdLazyQuery>;
|
||||
export type OrderByIdQueryResult = Apollo.QueryResult<OrderByIdQuery, OrderByIdQueryVariables>;
|
||||
export const OrdersDocument = gql`
|
||||
query Orders($partyId: ID!, $pagination: Pagination, $filter: OrderByMarketIdsFilter) {
|
||||
query Orders($partyId: ID!, $marketIds: [ID!], $pagination: Pagination, $filter: OrderFilter) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
ordersConnection(pagination: $pagination, filter: $filter) {
|
||||
ordersConnection(
|
||||
pagination: $pagination
|
||||
filter: {order: $filter, marketIds: $marketIds}
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
...OrderFields
|
||||
@@ -146,6 +153,7 @@ export const OrdersDocument = gql`
|
||||
* const { data, loading, error } = useOrdersQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* marketIds: // value for 'marketIds'
|
||||
* pagination: // value for 'pagination'
|
||||
* filter: // value for 'filter'
|
||||
* },
|
||||
@@ -163,8 +171,8 @@ export type OrdersQueryHookResult = ReturnType<typeof useOrdersQuery>;
|
||||
export type OrdersLazyQueryHookResult = ReturnType<typeof useOrdersLazyQuery>;
|
||||
export type OrdersQueryResult = Apollo.QueryResult<OrdersQuery, OrdersQueryVariables>;
|
||||
export const OrdersUpdateDocument = gql`
|
||||
subscription OrdersUpdate($partyId: ID!) {
|
||||
orders(filter: {partyIds: [$partyId]}) {
|
||||
subscription OrdersUpdate($partyId: ID!, $marketIds: [ID!]) {
|
||||
orders(filter: {partyIds: [$partyId], marketIds: $marketIds}) {
|
||||
...OrderUpdateFields
|
||||
}
|
||||
}
|
||||
@@ -183,6 +191,7 @@ export const OrdersUpdateDocument = gql`
|
||||
* const { data, loading, error } = useOrdersUpdateSubscription({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* marketIds: // value for 'marketIds'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -112,9 +112,7 @@ describe('order data provider', () => {
|
||||
const updatedData = update(data, delta, () => null, {
|
||||
partyId: '0x123',
|
||||
filter: {
|
||||
order: {
|
||||
dateRange: { end: new Date('2022-02-01').toISOString() },
|
||||
},
|
||||
dateRange: { end: new Date('2022-02-01').toISOString() },
|
||||
},
|
||||
});
|
||||
expect(
|
||||
|
||||
@@ -35,45 +35,40 @@ const orderMatchFilters = (
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
variables?.filter?.order?.status &&
|
||||
!(order.status && variables.filter.order.status.includes(order.status))
|
||||
variables?.filter?.status &&
|
||||
!(order.status && variables.filter.status.includes(order.status))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
variables?.filter?.order?.types &&
|
||||
!(order.type && variables.filter.order.types.includes(order.type))
|
||||
variables?.filter?.types &&
|
||||
!(order.type && variables.filter.types.includes(order.type))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
variables?.filter?.order?.timeInForce &&
|
||||
!variables.filter.order.timeInForce.includes(order.timeInForce)
|
||||
variables?.filter?.timeInForce &&
|
||||
!variables.filter.timeInForce.includes(order.timeInForce)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
variables?.filter?.order?.excludeLiquidity &&
|
||||
order.liquidityProvisionId
|
||||
) {
|
||||
if (variables?.filter?.excludeLiquidity && order.liquidityProvisionId) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
variables?.filter?.order?.dateRange?.start &&
|
||||
variables?.filter?.dateRange?.start &&
|
||||
!(
|
||||
(order.updatedAt || order.createdAt) &&
|
||||
variables.filter.order.dateRange.start <
|
||||
(order.updatedAt || order.createdAt)
|
||||
variables.filter.dateRange.start < (order.updatedAt || order.createdAt)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
variables?.filter?.order?.dateRange?.end &&
|
||||
variables?.filter?.dateRange?.end &&
|
||||
!(
|
||||
(order.updatedAt || order.createdAt) &&
|
||||
variables.filter.order.dateRange.end >
|
||||
(order.updatedAt || order.createdAt)
|
||||
variables.filter.dateRange.end > (order.updatedAt || order.createdAt)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
@@ -243,18 +238,17 @@ export const hasActiveOrderProvider = makeDerivedDataProvider<
|
||||
{ partyId: string; marketId?: string }
|
||||
>(
|
||||
[
|
||||
(callback, client, variables) =>
|
||||
(callback, client, { partyId, marketId }) =>
|
||||
hasActiveOrderProviderInternal(callback, client, {
|
||||
marketIds: marketId ? [marketId] : undefined,
|
||||
filter: {
|
||||
order: {
|
||||
status: [OrderStatus.STATUS_ACTIVE],
|
||||
excludeLiquidity: true,
|
||||
},
|
||||
status: [OrderStatus.STATUS_ACTIVE],
|
||||
excludeLiquidity: true,
|
||||
},
|
||||
pagination: {
|
||||
first: 1,
|
||||
},
|
||||
...variables,
|
||||
partyId,
|
||||
} as OrdersQueryVariables),
|
||||
],
|
||||
(parts) => parts[0]
|
||||
|
||||
@@ -72,12 +72,10 @@ export const useOrderListData = ({
|
||||
const allVars: OrdersQueryVariables & OrdersUpdateSubscriptionVariables = {
|
||||
partyId,
|
||||
filter: {
|
||||
order: {
|
||||
dateRange: filter?.updatedAt?.value,
|
||||
status: filter?.status?.value,
|
||||
timeInForce: filter?.timeInForce?.value,
|
||||
types: filter?.type?.value,
|
||||
},
|
||||
dateRange: filter?.updatedAt?.value,
|
||||
status: filter?.status?.value,
|
||||
timeInForce: filter?.timeInForce?.value,
|
||||
types: filter?.type?.value,
|
||||
},
|
||||
pagination: {
|
||||
first: 1000,
|
||||
|
||||
@@ -129,8 +129,9 @@ export const OrderListTable = memo(
|
||||
}: VegaValueFormatterParams<Order, 'status'>) => {
|
||||
if (data?.rejectionReason && value) {
|
||||
return `${Schema.OrderStatusMapping[value]}: ${
|
||||
data?.rejectionReason &&
|
||||
Schema.OrderRejectionReasonMapping[data.rejectionReason]
|
||||
(data?.rejectionReason &&
|
||||
Schema.OrderRejectionReasonMapping[data.rejectionReason]) ||
|
||||
data?.rejectionReason
|
||||
}`;
|
||||
}
|
||||
return value ? Schema.OrderStatusMapping[value] : '';
|
||||
@@ -218,7 +219,14 @@ export const OrderListTable = memo(
|
||||
return `${Schema.OrderTimeInForceMapping[value]}: ${expiry}`;
|
||||
}
|
||||
|
||||
return value ? Schema.OrderTimeInForceMapping[value] : '';
|
||||
const tifLabel = value
|
||||
? Schema.OrderTimeInForceMapping[value]
|
||||
: '';
|
||||
const label = `${tifLabel}${
|
||||
data?.postOnly ? t('. Post Only') : ''
|
||||
}${data?.reduceOnly ? t('. Reduce only') : ''}`;
|
||||
|
||||
return label;
|
||||
}}
|
||||
minWidth={150}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
export * from './__generated__/OrdersSubscription';
|
||||
export * from './use-has-active-order';
|
||||
export * from './use-order-cancel';
|
||||
export * from './use-order-edit';
|
||||
export * from './use-order-submit';
|
||||
export * from './use-order-update';
|
||||
export * from './use-pending-orders-volume';
|
||||
export * from './use-order-store';
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { useOrderCancel } from './use-order-cancel';
|
||||
import type { OrderSubSubscription } from './';
|
||||
import { OrderSubDocument } from './';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const defaultWalletContext = {
|
||||
pubKey: null,
|
||||
pubKeys: [],
|
||||
isReadOnly: false,
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
selectPubKey: jest.fn(),
|
||||
connector: null,
|
||||
};
|
||||
|
||||
function setup(context?: Partial<VegaWalletContextShape>) {
|
||||
const mocks: MockedResponse<OrderSubSubscription> = {
|
||||
request: {
|
||||
query: OrderSubDocument,
|
||||
variables: {
|
||||
partyId: context?.pubKey || '',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
orders: [
|
||||
{
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
rejectionReason: null,
|
||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
||||
size: '10',
|
||||
price: '300000',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
marketId: 'market-id',
|
||||
__typename: 'OrderUpdate',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const filterMocks: MockedResponse<OrderSubSubscription> = {
|
||||
request: {
|
||||
query: OrderSubDocument,
|
||||
variables: {
|
||||
partyId: context?.pubKey || '',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
orders: [
|
||||
{
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
rejectionReason: null,
|
||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
||||
size: '10',
|
||||
price: '300000',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
marketId: 'market-id',
|
||||
__typename: 'OrderUpdate',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={[mocks, filterMocks]}>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ ...defaultWalletContext, ...context }}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
return renderHook(() => useOrderCancel(), { wrapper });
|
||||
}
|
||||
|
||||
describe('useOrderCancel', () => {
|
||||
it('has the correct default state', () => {
|
||||
const { result } = setup();
|
||||
expect(typeof result.current.cancel).toEqual('function');
|
||||
expect(typeof result.current.reset).toEqual('function');
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
||||
expect(result.current.transaction.txHash).toEqual(null);
|
||||
expect(result.current.transaction.error).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not sendTx if no keypair', () => {
|
||||
const mockSendTx = jest.fn();
|
||||
const { result } = setup({
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [],
|
||||
pubKey: null,
|
||||
});
|
||||
act(() => {
|
||||
result.current.cancel({ orderId: 'order-id', marketId: 'market-id' });
|
||||
});
|
||||
expect(mockSendTx).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should cancel a correctly formatted order', async () => {
|
||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
|
||||
const { result } = setup({
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [pubKeyObj],
|
||||
pubKey: pubKeyObj.publicKey,
|
||||
});
|
||||
|
||||
const args = {
|
||||
orderId: 'order-id',
|
||||
marketId: 'market-id',
|
||||
};
|
||||
act(() => {
|
||||
result.current.cancel(args);
|
||||
});
|
||||
|
||||
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
|
||||
orderCancellation: args,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import {
|
||||
useVegaWallet,
|
||||
useVegaTransaction,
|
||||
useTransactionResult,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type {
|
||||
OrderCancellationBody,
|
||||
TransactionResult,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type { OrderSubFieldsFragment } from './';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useOrderUpdate } from './use-order-update';
|
||||
|
||||
export const useOrderCancel = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const [cancelledOrder, setCancelledOrder] =
|
||||
useState<OrderSubFieldsFragment | null>(null);
|
||||
const [transactionResult, setTransactionResult] =
|
||||
useState<TransactionResult>();
|
||||
|
||||
const {
|
||||
send,
|
||||
transaction,
|
||||
reset: resetTransaction,
|
||||
setComplete,
|
||||
Dialog,
|
||||
} = useVegaTransaction();
|
||||
|
||||
const waitForOrderUpdate = useOrderUpdate(transaction);
|
||||
const waitForTransactionResult = useTransactionResult();
|
||||
|
||||
const reset = useCallback(() => {
|
||||
resetTransaction();
|
||||
setCancelledOrder(null);
|
||||
}, [resetTransaction]);
|
||||
|
||||
const cancel = useCallback(
|
||||
async (orderCancellation: OrderCancellationBody['orderCancellation']) => {
|
||||
if (!pubKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCancelledOrder(null);
|
||||
|
||||
try {
|
||||
const res = await send(pubKey, {
|
||||
orderCancellation,
|
||||
});
|
||||
if (orderCancellation.orderId) {
|
||||
const cancelledOrder = await waitForOrderUpdate(
|
||||
orderCancellation.orderId,
|
||||
pubKey
|
||||
);
|
||||
setCancelledOrder(cancelledOrder);
|
||||
setComplete();
|
||||
} else if (res) {
|
||||
const txResult = await waitForTransactionResult(
|
||||
res.transactionHash,
|
||||
pubKey
|
||||
);
|
||||
setTransactionResult(txResult);
|
||||
setComplete();
|
||||
}
|
||||
return res;
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[pubKey, send, setComplete, waitForOrderUpdate, waitForTransactionResult]
|
||||
);
|
||||
|
||||
return {
|
||||
transaction,
|
||||
transactionResult,
|
||||
cancelledOrder,
|
||||
Dialog,
|
||||
cancel,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
@@ -1,179 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useOrderEdit } from './use-order-edit';
|
||||
import type { OrderSubSubscription } from './__generated__/OrdersSubscription';
|
||||
import { OrderSubDocument } from './__generated__/OrdersSubscription';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { Order } from '../components';
|
||||
import { generateOrder } from '../components';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const defaultWalletContext = {
|
||||
pubKey: null,
|
||||
pubKeys: [],
|
||||
isReadOnly: false,
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
selectPubKey: jest.fn(),
|
||||
connector: null,
|
||||
};
|
||||
|
||||
function setup(order: Order, context?: Partial<VegaWalletContextShape>) {
|
||||
const mocks: MockedResponse<OrderSubSubscription> = {
|
||||
request: {
|
||||
query: OrderSubDocument,
|
||||
variables: {
|
||||
partyId: context?.pubKey || '',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
orders: [
|
||||
{
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
rejectionReason: null,
|
||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
||||
size: '10',
|
||||
price: '300000',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
marketId: 'market-id',
|
||||
__typename: 'OrderUpdate',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const filterMocks: MockedResponse<OrderSubSubscription> = {
|
||||
request: {
|
||||
query: OrderSubDocument,
|
||||
variables: {
|
||||
partyId: context?.pubKey || '',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
orders: [
|
||||
{
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
rejectionReason: null,
|
||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
||||
size: '10',
|
||||
price: '300000',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
marketId: 'market-id',
|
||||
__typename: 'OrderUpdate',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={[mocks, filterMocks]}>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ ...defaultWalletContext, ...context }}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useOrderEdit(order), { wrapper });
|
||||
}
|
||||
|
||||
describe('useOrderEdit', () => {
|
||||
it('should edit a correctly formatted order if there is no size', async () => {
|
||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
|
||||
const order = generateOrder({
|
||||
price: '123456789',
|
||||
market: { decimalPlaces: 2 },
|
||||
});
|
||||
const { result } = setup(order, {
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [pubKeyObj],
|
||||
pubKey: pubKeyObj.publicKey,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.edit({ price: '1234567.89' });
|
||||
});
|
||||
|
||||
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
|
||||
orderAmendment: {
|
||||
orderId: order.id,
|
||||
// eslint-disable-next-line
|
||||
marketId: order.market!.id,
|
||||
timeInForce: order.timeInForce,
|
||||
price: '123456789', // Decimal removed
|
||||
sizeDelta: 0,
|
||||
expiresAt: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should edit a correctly formatted order', async () => {
|
||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
|
||||
const order = generateOrder({
|
||||
price: '123456789',
|
||||
market: { decimalPlaces: 2 },
|
||||
});
|
||||
const { result } = setup(order, {
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [pubKeyObj],
|
||||
pubKey: pubKeyObj.publicKey,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.edit({ price: '1234567.89', size: '20' });
|
||||
});
|
||||
|
||||
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
|
||||
orderAmendment: {
|
||||
orderId: order.id,
|
||||
// eslint-disable-next-line
|
||||
marketId: order.market!.id,
|
||||
timeInForce: order.timeInForce,
|
||||
price: '123456789', // Decimal removed
|
||||
sizeDelta: 1990,
|
||||
expiresAt: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('has the correct default state', () => {
|
||||
const order = generateOrder();
|
||||
const { result } = setup(order);
|
||||
expect(typeof result.current.edit).toEqual('function');
|
||||
expect(typeof result.current.reset).toEqual('function');
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
||||
expect(result.current.transaction.txHash).toEqual(null);
|
||||
expect(result.current.transaction.error).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not sendTx if no keypair', async () => {
|
||||
const order = generateOrder();
|
||||
const mockSendTx = jest.fn();
|
||||
const { result } = setup(order, {
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [],
|
||||
pubKey: null,
|
||||
});
|
||||
await act(async () => {
|
||||
result.current.edit(order);
|
||||
});
|
||||
expect(mockSendTx).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,82 +0,0 @@
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import type { OrderSubFieldsFragment } from './';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import type { Order } from '../components';
|
||||
import { useOrderUpdate } from './use-order-update';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export interface EditOrderArgs {
|
||||
price: string;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export const useOrderEdit = (order: Order | null) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const [updatedOrder, setUpdatedOrder] =
|
||||
useState<OrderSubFieldsFragment | null>(null);
|
||||
|
||||
const {
|
||||
send,
|
||||
transaction,
|
||||
reset: resetTransaction,
|
||||
setComplete,
|
||||
Dialog,
|
||||
} = useVegaTransaction();
|
||||
|
||||
const waitForOrderUpdate = useOrderUpdate(transaction);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
resetTransaction();
|
||||
setUpdatedOrder(null);
|
||||
}, [resetTransaction]);
|
||||
|
||||
const edit = useCallback(
|
||||
async (args: EditOrderArgs) => {
|
||||
if (!pubKey || !order || !order.market) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUpdatedOrder(null);
|
||||
|
||||
try {
|
||||
await send(pubKey, {
|
||||
orderAmendment: {
|
||||
orderId: order.id,
|
||||
marketId: order.market.id,
|
||||
price: removeDecimal(args.price, order.market.decimalPlaces),
|
||||
timeInForce: order.timeInForce,
|
||||
sizeDelta: args.size
|
||||
? new BigNumber(
|
||||
removeDecimal(args.size, order.market.positionDecimalPlaces)
|
||||
)
|
||||
.minus(order.size)
|
||||
.toNumber()
|
||||
: 0,
|
||||
expiresAt: order.expiresAt
|
||||
? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const updatedOrder = await waitForOrderUpdate(order.id, pubKey);
|
||||
setUpdatedOrder(updatedOrder);
|
||||
setComplete();
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[pubKey, send, order, setComplete, waitForOrderUpdate]
|
||||
);
|
||||
|
||||
return {
|
||||
transaction,
|
||||
updatedOrder,
|
||||
Dialog,
|
||||
edit,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
@@ -1,205 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { PubKey, VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useOrderSubmit } from './use-order-submit';
|
||||
import type { OrderSubSubscription } from './';
|
||||
import { OrderSubDocument } from './';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
const marketId = 'market-id';
|
||||
|
||||
const defaultWalletContext = {
|
||||
pubKey: null,
|
||||
pubKeys: [],
|
||||
isReadOnly: false,
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
selectPubKey: jest.fn(),
|
||||
connector: null,
|
||||
};
|
||||
|
||||
function setup(context?: Partial<VegaWalletContextShape>) {
|
||||
const mocks: MockedResponse<OrderSubSubscription> = {
|
||||
request: {
|
||||
query: OrderSubDocument,
|
||||
variables: {
|
||||
partyId: context?.pubKey || '',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
orders: [
|
||||
{
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
rejectionReason: null,
|
||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
||||
size: '10',
|
||||
price: '300000',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
marketId: 'market-id',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const filterMocks: MockedResponse<OrderSubSubscription> = {
|
||||
request: {
|
||||
query: OrderSubDocument,
|
||||
variables: {
|
||||
partyId: context?.pubKey || '',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
orders: [
|
||||
{
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
rejectionReason: null,
|
||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
||||
size: '10',
|
||||
price: '300000',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
marketId: 'market-id',
|
||||
__typename: 'OrderUpdate',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={[mocks, filterMocks]}>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ ...defaultWalletContext, ...context }}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useOrderSubmit(), { wrapper });
|
||||
}
|
||||
|
||||
describe('useOrderSubmit', () => {
|
||||
it('should submit a correctly formatted order on GTT', async () => {
|
||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
const pubKey = '0x123';
|
||||
const { result } = setup({
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [{ publicKey: pubKey, name: 'test key 1' }],
|
||||
pubKey,
|
||||
});
|
||||
|
||||
const order = {
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
size: '10',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
price: '123456789',
|
||||
expiresAt: new Date('2022-01-01').toISOString(),
|
||||
};
|
||||
await act(async () => {
|
||||
result.current.submit({ ...order, marketId });
|
||||
});
|
||||
|
||||
expect(mockSendTx).toHaveBeenCalledWith(pubKey, {
|
||||
orderSubmission: {
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
marketId,
|
||||
size: '10',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
|
||||
price: '123456789',
|
||||
expiresAt: new Date('2022-01-01').toISOString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit a correctly formatted order on GTC', async () => {
|
||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
const publicKeyObj: PubKey = {
|
||||
publicKey: '0x123',
|
||||
name: 'test key 1',
|
||||
};
|
||||
const { result } = setup({
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [publicKeyObj],
|
||||
pubKey: publicKeyObj.publicKey,
|
||||
});
|
||||
|
||||
const order = {
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
size: '10',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
price: '123456789',
|
||||
expiresAt: new Date('2022-01-01').toISOString(),
|
||||
};
|
||||
await act(async () => {
|
||||
result.current.submit({ ...order, marketId });
|
||||
});
|
||||
|
||||
expect(mockSendTx).toHaveBeenCalledWith(publicKeyObj.publicKey, {
|
||||
orderSubmission: {
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
marketId,
|
||||
size: '10',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
price: '123456789',
|
||||
expiresAt: new Date('2022-01-01').toISOString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('has the correct default state', () => {
|
||||
const { result } = setup();
|
||||
expect(typeof result.current.submit).toEqual('function');
|
||||
expect(typeof result.current.reset).toEqual('function');
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
||||
expect(result.current.transaction.txHash).toEqual(null);
|
||||
expect(result.current.transaction.error).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not sendTx if no keypair', async () => {
|
||||
const mockSendTx = jest.fn();
|
||||
const { result } = setup({
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [],
|
||||
pubKey: null,
|
||||
});
|
||||
await act(async () => {
|
||||
result.current.submit({} as OrderSubmissionBody['orderSubmission']);
|
||||
});
|
||||
expect(mockSendTx).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not sendTx side is not specified', async () => {
|
||||
const mockSendTx = jest.fn();
|
||||
const publicKeyObj: PubKey = {
|
||||
publicKey: '0x123',
|
||||
name: 'test key 1',
|
||||
};
|
||||
const { result } = setup({
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [publicKeyObj],
|
||||
pubKey: publicKeyObj.publicKey,
|
||||
});
|
||||
await act(async () => {
|
||||
result.current.submit({} as OrderSubmissionBody['orderSubmission']);
|
||||
});
|
||||
expect(mockSendTx).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,142 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { OrderSubFieldsFragment } from './__generated__/OrdersSubscription';
|
||||
import {
|
||||
useVegaWallet,
|
||||
useVegaTransaction,
|
||||
determineId,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useOrderUpdate } from './use-order-update';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { Icon, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
|
||||
export const getOrderDialogTitle = (
|
||||
status?: Schema.OrderStatus
|
||||
): string | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case Schema.OrderStatus.STATUS_ACTIVE:
|
||||
return t('Order submitted');
|
||||
case Schema.OrderStatus.STATUS_FILLED:
|
||||
return t('Order filled');
|
||||
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
|
||||
return t('Order partially filled');
|
||||
case Schema.OrderStatus.STATUS_PARKED:
|
||||
return t('Order parked');
|
||||
case Schema.OrderStatus.STATUS_STOPPED:
|
||||
return t('Order stopped');
|
||||
case Schema.OrderStatus.STATUS_CANCELLED:
|
||||
return t('Order cancelled');
|
||||
case Schema.OrderStatus.STATUS_EXPIRED:
|
||||
return t('Order expired');
|
||||
case Schema.OrderStatus.STATUS_REJECTED:
|
||||
return t('Order rejected');
|
||||
default:
|
||||
return t('Submission failed');
|
||||
}
|
||||
};
|
||||
|
||||
export const getOrderDialogIntent = (
|
||||
status?: Schema.OrderStatus
|
||||
): Intent | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
switch (status) {
|
||||
case Schema.OrderStatus.STATUS_PARKED:
|
||||
case Schema.OrderStatus.STATUS_EXPIRED:
|
||||
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
|
||||
return Intent.Warning;
|
||||
case Schema.OrderStatus.STATUS_REJECTED:
|
||||
case Schema.OrderStatus.STATUS_STOPPED:
|
||||
case Schema.OrderStatus.STATUS_CANCELLED:
|
||||
return Intent.Danger;
|
||||
case Schema.OrderStatus.STATUS_FILLED:
|
||||
case Schema.OrderStatus.STATUS_ACTIVE:
|
||||
return Intent.Success;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const getOrderDialogIcon = (
|
||||
status?: Schema.OrderStatus
|
||||
): ReactNode | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case Schema.OrderStatus.STATUS_PARKED:
|
||||
case Schema.OrderStatus.STATUS_EXPIRED:
|
||||
return <Icon name="warning-sign" size={16} />;
|
||||
case Schema.OrderStatus.STATUS_REJECTED:
|
||||
case Schema.OrderStatus.STATUS_STOPPED:
|
||||
case Schema.OrderStatus.STATUS_CANCELLED:
|
||||
return <Icon name="error" size={16} />;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const useOrderSubmit = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const {
|
||||
send,
|
||||
transaction,
|
||||
reset: resetTransaction,
|
||||
setComplete,
|
||||
Dialog,
|
||||
} = useVegaTransaction();
|
||||
|
||||
const waitForOrderUpdate = useOrderUpdate(transaction);
|
||||
|
||||
const [finalizedOrder, setFinalizedOrder] =
|
||||
useState<OrderSubFieldsFragment | null>(null);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
resetTransaction();
|
||||
setFinalizedOrder(null);
|
||||
}, [resetTransaction]);
|
||||
|
||||
const submit = useCallback(
|
||||
async (orderSubmission: OrderSubmissionBody['orderSubmission']) => {
|
||||
if (!pubKey || !orderSubmission.side) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFinalizedOrder(null);
|
||||
|
||||
try {
|
||||
const res = await send(pubKey, { orderSubmission });
|
||||
|
||||
if (res) {
|
||||
const orderId = determineId(res.signature);
|
||||
if (orderId) {
|
||||
const order = await waitForOrderUpdate(orderId, pubKey);
|
||||
setFinalizedOrder(order);
|
||||
setComplete();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
},
|
||||
[pubKey, send, setComplete, waitForOrderUpdate]
|
||||
);
|
||||
|
||||
return {
|
||||
transaction,
|
||||
finalizedOrder,
|
||||
Dialog,
|
||||
submit,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
@@ -53,14 +53,12 @@ export const useActiveOrdersVolumeAndMargin = (
|
||||
update,
|
||||
variables: {
|
||||
partyId: partyId || '',
|
||||
marketIds: [marketId],
|
||||
filter: {
|
||||
marketIds: [marketId],
|
||||
order: {
|
||||
status: [
|
||||
OrderStatus.STATUS_ACTIVE,
|
||||
OrderStatus.STATUS_PARTIALLY_FILLED,
|
||||
],
|
||||
},
|
||||
status: [
|
||||
OrderStatus.STATUS_ACTIVE,
|
||||
OrderStatus.STATUS_PARTIALLY_FILLED,
|
||||
],
|
||||
},
|
||||
},
|
||||
skip: !partyId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { OrderSubFieldsFragment } from './order-hooks';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
// More detail in https://docs.vega.xyz/mainnet/graphql/enums/order-time-in-force
|
||||
export const timeInForceLabel = (tif: string) => {
|
||||
@@ -38,3 +39,55 @@ export const getRejectionReason = (
|
||||
: null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getOrderToastTitle = (
|
||||
status?: Schema.OrderStatus
|
||||
): string | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case Schema.OrderStatus.STATUS_ACTIVE:
|
||||
return t('Order submitted');
|
||||
case Schema.OrderStatus.STATUS_FILLED:
|
||||
return t('Order filled');
|
||||
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
|
||||
return t('Order partially filled');
|
||||
case Schema.OrderStatus.STATUS_PARKED:
|
||||
return t('Order parked');
|
||||
case Schema.OrderStatus.STATUS_STOPPED:
|
||||
return t('Order stopped');
|
||||
case Schema.OrderStatus.STATUS_CANCELLED:
|
||||
return t('Order cancelled');
|
||||
case Schema.OrderStatus.STATUS_EXPIRED:
|
||||
return t('Order expired');
|
||||
case Schema.OrderStatus.STATUS_REJECTED:
|
||||
return t('Order rejected');
|
||||
default:
|
||||
return t('Submission failed');
|
||||
}
|
||||
};
|
||||
|
||||
export const getOrderToastIntent = (
|
||||
status?: Schema.OrderStatus
|
||||
): Intent | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
switch (status) {
|
||||
case Schema.OrderStatus.STATUS_PARKED:
|
||||
case Schema.OrderStatus.STATUS_EXPIRED:
|
||||
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
|
||||
return Intent.Warning;
|
||||
case Schema.OrderStatus.STATUS_REJECTED:
|
||||
case Schema.OrderStatus.STATUS_STOPPED:
|
||||
case Schema.OrderStatus.STATUS_CANCELLED:
|
||||
return Intent.Danger;
|
||||
case Schema.OrderStatus.STATUS_FILLED:
|
||||
case Schema.OrderStatus.STATUS_ACTIVE:
|
||||
return Intent.Success;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ export * from './lib/positions-data-providers';
|
||||
export * from './lib/margin-data-provider';
|
||||
export * from './lib/margin-calculator';
|
||||
export * from './lib/positions-table';
|
||||
export * from './lib/use-close-position';
|
||||
export * from './lib/use-market-margin';
|
||||
export * from './lib/use-market-position-open-volume';
|
||||
export * from './lib/use-open-volume';
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { OrderFieldsFragment } from '@vegaprotocol/orders';
|
||||
import { truncateByChars } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import type { TransactionResult, VegaTxState } from '@vegaprotocol/wallet';
|
||||
import type { ClosingOrder as IClosingOrder } from '../use-close-position';
|
||||
import { useRequestClosePositionData } from '../use-request-close-position-data';
|
||||
import { ClosingOrder } from './shared';
|
||||
|
||||
interface CompleteProps {
|
||||
partyId: string;
|
||||
transaction: VegaTxState;
|
||||
transactionResult?: TransactionResult;
|
||||
closingOrder?: IClosingOrder;
|
||||
closingOrderResult?: OrderFieldsFragment;
|
||||
}
|
||||
|
||||
export const Complete = ({
|
||||
partyId,
|
||||
transaction,
|
||||
transactionResult,
|
||||
closingOrder,
|
||||
closingOrderResult,
|
||||
}: CompleteProps) => {
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
|
||||
if (!transactionResult || !closingOrderResult) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{closingOrderResult.status === Schema.OrderStatus.STATUS_FILLED &&
|
||||
transactionResult.status ? (
|
||||
<Success partyId={partyId} order={closingOrder} />
|
||||
) : (
|
||||
<Error
|
||||
transactionResult={transactionResult}
|
||||
closingOrderResult={closingOrderResult}
|
||||
/>
|
||||
)}
|
||||
{transaction.txHash && (
|
||||
<>
|
||||
<p className="font-semibold mt-4">{t('Transaction')}</p>
|
||||
<p>
|
||||
<Link
|
||||
href={`${VEGA_EXPLORER_URL}/txs/${transaction.txHash}`}
|
||||
target="_blank"
|
||||
>
|
||||
{truncateByChars(transaction.txHash)}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Success = ({
|
||||
partyId,
|
||||
order,
|
||||
}: {
|
||||
partyId: string;
|
||||
order?: IClosingOrder;
|
||||
}) => {
|
||||
const { market, marketData, orders } = useRequestClosePositionData(
|
||||
order?.marketId,
|
||||
partyId
|
||||
);
|
||||
|
||||
if (!market || !marketData || !orders) {
|
||||
return <div>{t('Loading...')}</div>;
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
return (
|
||||
<div className="text-vega-pink">{t('Could retrieve closing order')}</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h2 className="font-bold">{t('Position closed')}</h2>
|
||||
<ClosingOrder order={order} market={market} marketData={marketData} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Error = ({
|
||||
transactionResult,
|
||||
closingOrderResult,
|
||||
}: {
|
||||
transactionResult: TransactionResult;
|
||||
closingOrderResult: OrderFieldsFragment;
|
||||
}) => {
|
||||
const reason =
|
||||
closingOrderResult.rejectionReason &&
|
||||
Schema.OrderRejectionReasonMapping[closingOrderResult.rejectionReason];
|
||||
return (
|
||||
<div className="text-vega-pink">
|
||||
{reason ? (
|
||||
<p>{reason}</p>
|
||||
) : (
|
||||
<p>
|
||||
{t('Transaction failed')}: {transactionResult.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import * as dataHook from '../use-request-close-position-data';
|
||||
import { Requested } from './requested';
|
||||
|
||||
jest.mock('./use-request-close-position-data');
|
||||
|
||||
describe('Close position dialog - Request', () => {
|
||||
const props = {
|
||||
partyId: 'party-id',
|
||||
order: {
|
||||
marketId: 'market-id',
|
||||
type: Schema.OrderType.TYPE_MARKET as const,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK as const,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
size: '10',
|
||||
},
|
||||
};
|
||||
|
||||
it('loading state', async () => {
|
||||
jest.spyOn(dataHook, 'useRequestClosePositionData').mockReturnValue({
|
||||
loading: false,
|
||||
market: null,
|
||||
marketData: null,
|
||||
orders: [],
|
||||
});
|
||||
render(<Requested {...props} />);
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders message if no closing order found', async () => {
|
||||
const orders = [
|
||||
{
|
||||
size: '200',
|
||||
price: '999',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
},
|
||||
{
|
||||
size: '300',
|
||||
price: '888',
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
},
|
||||
];
|
||||
jest.spyOn(dataHook, 'useRequestClosePositionData').mockReturnValue({
|
||||
market: {
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 2,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'test market',
|
||||
product: {
|
||||
// @ts-ignore avoiding having to add every property on the type
|
||||
settlementAsset: {
|
||||
symbol: 'SYM',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore avoid all fields
|
||||
marketData: {
|
||||
markPrice: '100',
|
||||
},
|
||||
// @ts-ignore avoid all fields
|
||||
orders,
|
||||
});
|
||||
render(<Requested {...props} />);
|
||||
|
||||
// closing order
|
||||
const closingOrderHeader = screen.getByText('Position to be closed');
|
||||
const closingOrderTable = within(
|
||||
closingOrderHeader.nextElementSibling?.querySelector(
|
||||
'tbody'
|
||||
) as HTMLElement
|
||||
);
|
||||
const closingOrderRow = closingOrderTable.getAllByRole('row');
|
||||
expect(closingOrderRow[0].children[0]).toHaveTextContent('test market');
|
||||
expect(closingOrderRow[0].children[1]).toHaveTextContent('+0.1');
|
||||
expect(closingOrderRow[0].children[2]).toHaveTextContent('~1.00 SYM');
|
||||
|
||||
// orders
|
||||
const ordersHeading = screen.getByText('Orders to be closed');
|
||||
const ordersTable = within(
|
||||
ordersHeading.nextElementSibling?.querySelector('tbody') as HTMLElement
|
||||
);
|
||||
const orderRows = ordersTable.getAllByRole('row');
|
||||
expect(orderRows).toHaveLength(orders.length);
|
||||
expect(orderRows[0].children[0]).toHaveTextContent('+2');
|
||||
expect(orderRows[0].children[1]).toHaveTextContent('9.99 SYM');
|
||||
expect(orderRows[0].children[2]).toHaveTextContent(
|
||||
"Good 'til Cancelled (GTC)"
|
||||
);
|
||||
|
||||
expect(orderRows[1].children[0]).toHaveTextContent('-3');
|
||||
expect(orderRows[1].children[1]).toHaveTextContent('8.88 SYM');
|
||||
expect(orderRows[1].children[2]).toHaveTextContent(
|
||||
"Good 'til Cancelled (GTC)"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { ClosingOrder as IClosingOrder } from '../use-close-position';
|
||||
import { useRequestClosePositionData } from '../use-request-close-position-data';
|
||||
import { ActiveOrders, ClosingOrder } from './shared';
|
||||
|
||||
export const Requested = ({
|
||||
order,
|
||||
partyId,
|
||||
}: {
|
||||
order?: IClosingOrder;
|
||||
partyId: string;
|
||||
}) => {
|
||||
const { market, marketData, orders, loading } = useRequestClosePositionData(
|
||||
order?.marketId,
|
||||
partyId
|
||||
);
|
||||
|
||||
if (loading || !market || !marketData || !orders) {
|
||||
return <div>{t('Loading...')}</div>;
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
return (
|
||||
<div className="text-vega-pink">
|
||||
{t('Could not create closing order')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="font-bold">{t('Position to be closed')}</h2>
|
||||
<ClosingOrder order={order} market={market} marketData={marketData} />
|
||||
<ActiveOrders market={market} orders={orders} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
import type { MarketData, Market } from '@vegaprotocol/market-list';
|
||||
import type { Order } from '@vegaprotocol/orders';
|
||||
import { timeInForceLabel } from '@vegaprotocol/orders';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Size } from '@vegaprotocol/react-helpers';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ClosingOrder as IClosingOrder } from '../use-close-position';
|
||||
|
||||
export const ClosingOrder = ({
|
||||
order,
|
||||
market,
|
||||
marketData,
|
||||
}: {
|
||||
order: IClosingOrder;
|
||||
market: Market;
|
||||
marketData: MarketData;
|
||||
}) => {
|
||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||
const estimatedPrice =
|
||||
marketData && market
|
||||
? addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
|
||||
: '-';
|
||||
const size = market ? (
|
||||
<Size
|
||||
value={order.size}
|
||||
side={order.side}
|
||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
|
||||
return (
|
||||
<BasicTable
|
||||
headers={[t('Market'), t('Amount'), t('Est price')]}
|
||||
rows={[
|
||||
[
|
||||
market.tradableInstrument.instrument.name,
|
||||
size,
|
||||
`~${estimatedPrice} ${asset?.symbol}`,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ActiveOrders = ({
|
||||
market,
|
||||
orders,
|
||||
}: {
|
||||
market: Market;
|
||||
orders: Order[];
|
||||
}) => {
|
||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||
|
||||
if (!orders.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<h2 className="font-bold">{t('Orders to be closed')}</h2>
|
||||
<BasicTable
|
||||
headers={[t('Amount'), t('Target price'), t('Time in force')]}
|
||||
rows={orders.map((o) => {
|
||||
return [
|
||||
<Size
|
||||
value={o.size}
|
||||
side={o.side}
|
||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||
/>,
|
||||
`${addDecimalsFormatNumber(o.price, market.decimalPlaces)} ${
|
||||
asset.symbol
|
||||
}`,
|
||||
timeInForceLabel(o.timeInForce),
|
||||
];
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface BasicTableProps {
|
||||
headers: ReactNode[];
|
||||
rows: ReactNode[][];
|
||||
}
|
||||
|
||||
const BasicTable = ({ headers, rows }: BasicTableProps) => {
|
||||
return (
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((h, i) => (
|
||||
<th key={i} className="text-left font-medium">
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((cells, i) => (
|
||||
<tr key={i}>
|
||||
{cells.map((c, i) => (
|
||||
<td key={i} className="align-top">
|
||||
{c}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
@@ -349,16 +349,15 @@ export const volumeAndMarginProvider = makeDerivedDataProvider<
|
||||
PositionsQueryVariables & MarketDataQueryVariables
|
||||
>(
|
||||
[
|
||||
(callback, client, variables) =>
|
||||
(callback, client, { partyId, marketId }) =>
|
||||
ordersProvider(callback, client, {
|
||||
...variables,
|
||||
partyId,
|
||||
marketIds: [marketId],
|
||||
filter: {
|
||||
order: {
|
||||
status: [
|
||||
OrderStatus.STATUS_ACTIVE,
|
||||
OrderStatus.STATUS_PARTIALLY_FILLED,
|
||||
],
|
||||
},
|
||||
status: [
|
||||
OrderStatus.STATUS_ACTIVE,
|
||||
OrderStatus.STATUS_PARTIALLY_FILLED,
|
||||
],
|
||||
},
|
||||
}),
|
||||
(callback, client, variables) =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user