Merge branch 'release/testnet' of github.com:vegaprotocol/frontend-monorepo into release/testnet
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
---
|
||||
name: Release
|
||||
about:
|
||||
A template to outline the steps needed to for a successful release of our frontend apps
|
||||
about: A template to outline the steps needed to for a successful release of our frontend apps
|
||||
title: 'Release [add dapp version]-core-[add core version]'
|
||||
labels:
|
||||
labels:
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Tasks
|
||||
|
||||
- [ ] Review [link to core release](xxx)
|
||||
- [ ] Review [link to core release](xxx)
|
||||
- [ ] Tag frontend-monorepo
|
||||
- [ ] Create release and generate release notes
|
||||
- [ ] Run `@smoke` tests
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release/*
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
jobs:
|
||||
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: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
|
||||
cache: yarn
|
||||
|
||||
- name: 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: Check formatting
|
||||
run: yarn nx format:check
|
||||
|
||||
- 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: |
|
||||
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=""
|
||||
if [[ $affected == *"governance"* ]]; then projects_e2e+='"governance-e2e" '; fi
|
||||
if [[ $affected == *"trading"* ]]; then projects_e2e+='"trading-e2e" '; fi
|
||||
if [[ $affected == *"explorer"* ]]; then projects_e2e+='"explorer-e2e" '; fi
|
||||
if [[ -z "$projects_e2e" ]]; then projects_e2e+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi
|
||||
projects_e2e=${projects_e2e%?}
|
||||
projects_e2e=[${projects_e2e// /,}]
|
||||
echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
|
||||
echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV
|
||||
|
||||
outputs:
|
||||
projects: ${{ env.PROJECTS }}
|
||||
projects-e2e: ${{ env.PROJECTS_E2E }}
|
||||
|
||||
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.lint-test-build.outputs.projects-e2e }}
|
||||
tags: '@smoke @regression'
|
||||
|
||||
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.lint-test-build.outputs.projects }}
|
||||
|
||||
# Report single result at the end, to avoid mess with required checks in PR
|
||||
cypress-result:
|
||||
if: ${{ always() }}
|
||||
needs: cypress
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- run: |
|
||||
result="${{ needs.cypress.result }}"
|
||||
if [[ $result == "success" || $result == "skipped" ]]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
@@ -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
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
name: PR Validations
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
jobs:
|
||||
pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout frontend mono repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# We need to fetch all branches and commits so that Nx affected has a base to compare against.
|
||||
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 }}
|
||||
|
||||
# Check SHAs
|
||||
- name: Derive appropriate SHAs for base and head for `nx affected` commands
|
||||
uses: nrwl/nx-set-shas@v3
|
||||
with:
|
||||
main-branch-name: develop
|
||||
|
||||
# See affected apps
|
||||
- name: See affected apps
|
||||
run: |
|
||||
nx_version=$(cat package.json | grep '"nx"' | cut -d ':' -f 2 | tr -d '",[:space:]')
|
||||
rm package.json yarn.lock
|
||||
yarn add nx@$nx_version
|
||||
|
||||
echo ">>>> debug"
|
||||
echo "NX Version: $nx_version"
|
||||
echo "NX_BASE: ${{ env.NX_BASE }}"
|
||||
echo "NX_HEAD: ${{ env.NX_HEAD }}"
|
||||
|
||||
# echo "Main branch name: ${{ github.base_ref || github.ref_name }}"
|
||||
# echo "git rev-parse HEAD: $(git rev-parse HEAD)"
|
||||
# echo "Head: ${{ github.head_ref }}"
|
||||
|
||||
# echo "command to execute: 'yarn nx print-affected --base=${{ github.base_ref || github.ref_name }} --head=${{ github.head_ref }} --select=projects'"
|
||||
|
||||
# merge_base=$(git merge-base origin/develop HEAD)
|
||||
# echo "git merge-base origin/develop HEAD: $merge_base"
|
||||
|
||||
# head_sha="${{ github.event.pull_request.head.sha || github.sha }}"
|
||||
# echo "Head SHA: $head_sha"
|
||||
|
||||
# echo "command to execute (without nx-set-sha): 'yarn nx print-affected --base=$merge_base --head=$head_sha --select=projects'"
|
||||
echo ">>>> eof debug"
|
||||
|
||||
# affected_1=$(yarn nx print-affected --base=$merge_base --head=$head_sha --select=projects || true)
|
||||
# echo -n "Affected projects (allowed to fail): $affected_1"
|
||||
|
||||
# affected=$(yarn nx print-affected --base=${{ github.base_ref || github.ref_name }} --head=${{ github.head_ref }} --select=projects)
|
||||
# echo -n "Affected projects: $affected"
|
||||
|
||||
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=HEAD --select=projects)"
|
||||
echo -n "Affected projects: $affected"
|
||||
|
||||
projects_e2e=""
|
||||
if [[ $affected == *"governance"* ]]; then projects_e2e+='"governance-e2e" '; fi
|
||||
if [[ $affected == *"trading"* ]]; then projects_e2e+='"trading-e2e" '; fi
|
||||
if [[ $affected == *"explorer"* ]]; then projects_e2e+='"explorer-e2e" '; fi
|
||||
if [[ -z "$projects_e2e" ]]; then projects_e2e+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi
|
||||
projects_e2e=${projects_e2e%?}
|
||||
projects_e2e=[${projects_e2e// /,}]
|
||||
echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
|
||||
echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV
|
||||
|
||||
outputs:
|
||||
projects: ${{ env.PROJECTS }}
|
||||
projects-e2e: ${{ env.PROJECTS_E2E }}
|
||||
|
||||
run-cypress:
|
||||
needs: pr
|
||||
if: ${{ needs.pr.outputs.projects != '[]' }}
|
||||
uses: ./.github/workflows/cypress-run.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
projects: ${{ needs.pr.outputs.projects-e2e }}
|
||||
tags: '@smoke @regression'
|
||||
|
||||
run-docker-build:
|
||||
needs: pr
|
||||
if: ${{ needs.pr.outputs.projects != '[]' }}
|
||||
uses: ./.github/workflows/publish-docker-containers.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
projects: ${{ needs.pr.outputs.projects }}
|
||||
|
||||
# Report single result at the end, to avoid mess with required checks in PR
|
||||
result:
|
||||
if: ${{ always() }}
|
||||
needs: run-cypress
|
||||
runs-on: ubuntu-latest
|
||||
name: Cypress result
|
||||
steps:
|
||||
- run: |
|
||||
result="${{ needs.run-cypress.result }}"
|
||||
if [[ $result == "success" || $result == "skipped" ]]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
@@ -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
|
||||
@@ -13,6 +13,8 @@ NX_LOCAL_PROVIDER_URL=http://localhost:8545/
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
|
||||
#Test configuration variables
|
||||
CYPRESS_FAIRGROUND=false
|
||||
|
||||
@@ -56,7 +56,7 @@ describe(
|
||||
navigateTo(navigation.proposals);
|
||||
});
|
||||
|
||||
// 3001-VOTE-055
|
||||
// 3001-VOTE-050 3001-VOTE-054 3001-VOTE-055 3002-PROP-019
|
||||
it('Newly created raw proposal details - shows proposal title and full description', function () {
|
||||
createRawProposal();
|
||||
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
|
||||
|
||||
@@ -128,7 +128,7 @@ context(
|
||||
.and('be.visible');
|
||||
});
|
||||
|
||||
// 3001-VOTE-048 3001-VOTE-049
|
||||
// 3001-VOTE-048 3001-VOTE-049 3001-VOTE-050
|
||||
it('Able to fail proposal due to lack of participation', function () {
|
||||
const proposalTitle = 'Add New free form proposal with short enactment';
|
||||
const proposalTx = createFreeFormProposalTxBody();
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
waitForSpinner,
|
||||
navigateTo,
|
||||
navigation,
|
||||
closeDialog,
|
||||
} from '../../support/common.functions';
|
||||
import {
|
||||
clickOnValidatorFromList,
|
||||
@@ -41,7 +42,6 @@ const vegaWalletNameElement = '[data-testid="wallet-name"]';
|
||||
const vegaWallet = '[data-testid="vega-wallet"]';
|
||||
const connectToVegaWalletButton = '[data-testid="connect-to-vega-wallet-btn"]';
|
||||
const newProposalSubmitButton = '[data-testid="proposal-submit"]';
|
||||
const dialogCloseButton = '[data-testid="dialog-close"]';
|
||||
const viewProposalButton = '[data-testid="view-proposal-btn"]';
|
||||
const rawProposalData = '[data-testid="proposal-data"]';
|
||||
const minVoteButton = '[data-testid="min-vote"]';
|
||||
@@ -177,7 +177,7 @@ context(
|
||||
'be.visible'
|
||||
);
|
||||
cy.contains('Proposal rejected', proposalTimeout).should('be.visible');
|
||||
cy.get(dialogCloseButton).click();
|
||||
closeDialog();
|
||||
waitForProposalSync();
|
||||
navigateTo(navigation.proposals);
|
||||
cy.get(rejectProposalsLink).click();
|
||||
@@ -214,7 +214,7 @@ context(
|
||||
enterRawProposalBody(createTenDigitUnixTimeStampForSpecifiedDays(8));
|
||||
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
|
||||
cy.get(feedbackError).should('have.text', errorMsg);
|
||||
cy.get(dialogCloseButton).click();
|
||||
closeDialog();
|
||||
});
|
||||
|
||||
// 3002-PROP-009
|
||||
@@ -227,7 +227,7 @@ context(
|
||||
enterRawProposalBody(createTenDigitUnixTimeStampForSpecifiedDays(8));
|
||||
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
|
||||
cy.get(feedbackError).should('have.text', errorMsg);
|
||||
cy.get(dialogCloseButton).click();
|
||||
closeDialog();
|
||||
});
|
||||
|
||||
it('Unable to create a freeform proposal - when json parent section contains unexpected field', function () {
|
||||
@@ -251,7 +251,7 @@ context(
|
||||
|
||||
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
|
||||
cy.get(feedbackError).should('have.text', errorMsg);
|
||||
cy.get(dialogCloseButton).click();
|
||||
closeDialog();
|
||||
cy.get(rawProposalData)
|
||||
.invoke('val')
|
||||
.should('contain', "i shouldn't be here");
|
||||
@@ -279,7 +279,7 @@ context(
|
||||
|
||||
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
|
||||
cy.get(feedbackError).should('have.text', errorMsg);
|
||||
cy.get(dialogCloseButton).click();
|
||||
closeDialog();
|
||||
});
|
||||
|
||||
// 1005-PROP-009
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
closeDialog,
|
||||
navigateTo,
|
||||
navigation,
|
||||
waitForSpinner,
|
||||
@@ -39,7 +40,6 @@ const maxVoteDeadline = '[data-testid="max-vote"]';
|
||||
const minValidationDeadline = '[data-testid="min-validation"]';
|
||||
const minEnactDeadline = '[data-testid="min-enactment"]';
|
||||
const maxEnactDeadline = '[data-testid="max-enactment"]';
|
||||
const dialogCloseButton = '[data-testid="dialog-close"]';
|
||||
const inputError = '[data-testid="input-error-text"]';
|
||||
const enactmentDeadlineError =
|
||||
'[data-testid="enactment-before-voting-deadline"]';
|
||||
@@ -48,6 +48,7 @@ const feedbackError = '[data-testid="Error"]';
|
||||
const viewProposalBtn = 'view-proposal-btn';
|
||||
const liquidityVoteStatus = 'liquidity-votes-status';
|
||||
const tokenVoteStatus = 'token-votes-status';
|
||||
const proposalTermsSection = 'proposal';
|
||||
const vegaWalletPublicKey = Cypress.env('vegaWalletPublicKey');
|
||||
const epochTimeout = Cypress.env('epochTimeout');
|
||||
const proposalTimeout = { timeout: 14000 };
|
||||
@@ -68,7 +69,6 @@ context(
|
||||
{ tags: '@slow' },
|
||||
function () {
|
||||
before('connect wallets and set approval limit', function () {
|
||||
cy.createMarket();
|
||||
cy.visit('/');
|
||||
vegaWalletSetSpecifiedApprovalAmount('1000');
|
||||
});
|
||||
@@ -78,6 +78,7 @@ context(
|
||||
waitForSpinner();
|
||||
cy.connectVegaWallet();
|
||||
ethereumWalletConnect();
|
||||
cy.createMarket();
|
||||
ensureSpecifiedUnstakedTokensAreAssociated('1');
|
||||
navigateTo(navigation.proposals);
|
||||
});
|
||||
@@ -194,7 +195,7 @@ context(
|
||||
'have.text',
|
||||
'Invalid params: proposal_submission.terms.closing_timestamp (cannot be after enactment time)'
|
||||
);
|
||||
cy.get(dialogCloseButton).click();
|
||||
closeDialog();
|
||||
cy.get(minVoteDeadline).click();
|
||||
cy.get(enactmentDeadlineError).should('not.exist');
|
||||
});
|
||||
@@ -286,7 +287,7 @@ context(
|
||||
);
|
||||
});
|
||||
|
||||
// 3001-VOTE-092
|
||||
// 3001-VOTE-092 3004-PMAC-001
|
||||
it('Able to submit update market proposal and vote for proposal', function () {
|
||||
vegaWalletFaucetAssetsWithoutCheck(
|
||||
'fUSDC',
|
||||
@@ -347,8 +348,9 @@ context(
|
||||
|
||||
// 3001-VOTE-026 3001-VOTE-027 3001-VOTE-028 3001-VOTE-095 3001-VOTE-096 3005-PASN-001
|
||||
it('Able to submit new asset proposal using min deadlines', function () {
|
||||
const proposalTitle = 'Test new asset proposal';
|
||||
goToMakeNewProposal(governanceProposalType.NEW_ASSET);
|
||||
cy.get(newProposalTitle).type('Test new asset proposal');
|
||||
cy.get(newProposalTitle).type(proposalTitle);
|
||||
cy.get(newProposalDescription).type('E2E test for proposals');
|
||||
cy.fixture('/proposals/new-asset').then((newAssetProposal) => {
|
||||
const newAssetPayload = JSON.stringify(newAssetProposal);
|
||||
@@ -367,7 +369,7 @@ context(
|
||||
cy.contains('Proposal waiting for node vote', proposalTimeout).should(
|
||||
'be.visible'
|
||||
);
|
||||
cy.get(dialogCloseButton).click();
|
||||
closeDialog();
|
||||
cy.get(newProposalSubmitButton).should('be.visible').click();
|
||||
// cannot submit a proposal with ERC20 address already in use
|
||||
cy.contains('Proposal rejected', proposalTimeout).should('be.visible');
|
||||
@@ -377,6 +379,17 @@ context(
|
||||
'PROPOSAL_ERROR_ERC20_ADDRESS_ALREADY_IN_USE'
|
||||
);
|
||||
});
|
||||
closeDialog();
|
||||
navigateTo(navigation.proposals);
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil(proposalListItem)
|
||||
.within(() => {
|
||||
cy.getByTestId(viewProposalBtn).click();
|
||||
});
|
||||
cy.getByTestId(proposalTermsSection).within(() => {
|
||||
cy.contains('USDT Coin').should('be.visible');
|
||||
cy.contains('USDT').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('Unable to submit new asset proposal with missing/invalid fields', function () {
|
||||
@@ -415,9 +428,15 @@ context(
|
||||
getProposalInformationFromTable('Proposed enactment') // 3001-VOTE-044
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
// 3001-VOTE-030 3001-VOTE-031
|
||||
cy.getByTestId(proposalTermsSection).within(() => {
|
||||
cy.contains('UpdateAsset').should('be.visible');
|
||||
cy.contains('UpdateERC20').should('be.visible');
|
||||
cy.contains('"lifetimeLimit": "10"').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('Able to submit update asset proposal using max deadline', function () {
|
||||
it.only('Able to submit update asset proposal using max deadline', function () {
|
||||
goToMakeNewProposal(governanceProposalType.UPDATE_ASSET);
|
||||
enterUpdateAssetProposalDetails();
|
||||
cy.get(maxVoteDeadline).click();
|
||||
|
||||
@@ -183,6 +183,7 @@ context(
|
||||
// 1004-ASSO-018
|
||||
// 1004-ASSO-024
|
||||
// 1004-ASSO-023
|
||||
// 1004-ASSO-032
|
||||
|
||||
stakingPageAssociateTokens('2', {
|
||||
type: 'contract',
|
||||
|
||||
@@ -6,159 +6,203 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
cy.get('nav', { timeout: 10000 }).should('be.visible');
|
||||
});
|
||||
|
||||
describe('with wallets disconnected', function () {
|
||||
describe('Links and buttons', function () {
|
||||
it('should have link for proposal page', function () {
|
||||
cy.getByTestId('home-proposals').within(() => {
|
||||
cy.get('[href="/proposals"]')
|
||||
.should('exist')
|
||||
.and('have.text', 'Browse, vote, and propose');
|
||||
});
|
||||
});
|
||||
it('should show open or enacted proposals with proposal summary', function () {
|
||||
cy.get('body').then(($body) => {
|
||||
if (!$body.find('[data-testid="proposals-list-item"]').length) {
|
||||
cy.createMarket();
|
||||
cy.reload();
|
||||
waitForSpinner();
|
||||
}
|
||||
});
|
||||
cy.getByTestId('proposals-list-item')
|
||||
.should('have.length.at.least', 1)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.getByTestId('proposal-title')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('proposal-type')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('proposal-description')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('proposal-details')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('proposal-status')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('vote-details')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('view-proposal-btn').should('be.visible');
|
||||
});
|
||||
});
|
||||
it('should have external link for governance', function () {
|
||||
cy.getByTestId('home-proposals').within(() => {
|
||||
cy.getByTestId('external-link')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://vega.xyz/governance');
|
||||
});
|
||||
});
|
||||
it('should have link for validator page', function () {
|
||||
cy.getByTestId('home-validators').within(() => {
|
||||
cy.get('[href="/validators"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'Browse, and stake');
|
||||
});
|
||||
});
|
||||
it('should have external link for validators', function () {
|
||||
cy.getByTestId('home-validators').within(() => {
|
||||
cy.getByTestId('external-link')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'contain',
|
||||
'https://community.vega.xyz/c/mainnet-validator-candidates'
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should have information on active nodes', function () {
|
||||
cy.getByTestId('node-information')
|
||||
.first()
|
||||
.should('contain.text', '2')
|
||||
.and('contain.text', 'active nodes');
|
||||
});
|
||||
it('should have information on consensus nodes', function () {
|
||||
cy.getByTestId('node-information')
|
||||
.last()
|
||||
.should('contain.text', '2')
|
||||
.and('contain.text', 'consensus nodes');
|
||||
});
|
||||
it('should contain link to specific validators', function () {
|
||||
cy.getByTestId('validators')
|
||||
.should('have.length', '2')
|
||||
.each(($validator) => {
|
||||
cy.wrap($validator).find('a').should('have.attr', 'href');
|
||||
});
|
||||
});
|
||||
it('should have link for rewards page', function () {
|
||||
cy.getByTestId('home-rewards').within(() => {
|
||||
cy.get('[href="/rewards"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'See rewards');
|
||||
});
|
||||
});
|
||||
it('should have link for withdrawal page', function () {
|
||||
cy.getByTestId('home-vega-token').within(() => {
|
||||
cy.get('[href="/token/withdraw"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'Manage tokens');
|
||||
});
|
||||
describe('Links and buttons', function () {
|
||||
it('should have link for proposal page', function () {
|
||||
cy.getByTestId('home-proposals').within(() => {
|
||||
cy.get('[href="/proposals"]')
|
||||
.should('exist')
|
||||
.and('have.text', 'Browse, vote, and propose');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mobile view - navigation bar', function () {
|
||||
before('Change to mobile resolution', function () {
|
||||
cy.viewport('iphone-xr');
|
||||
});
|
||||
|
||||
it('should have burger button', () => {
|
||||
cy.getByTestId('button-menu-drawer').should('be.visible').click();
|
||||
cy.getByTestId('menu-drawer').should('be.visible');
|
||||
});
|
||||
|
||||
it('should have link for proposal page', function () {
|
||||
cy.getByTestId('menu-drawer').within(() => {
|
||||
cy.get('[href="/proposals"]')
|
||||
.should('exist')
|
||||
.and('have.text', 'Proposals');
|
||||
it('should display announcement banner', function () {
|
||||
cy.getByTestId('app-announcement')
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
cy.getByTestId('external-link').should('exist');
|
||||
});
|
||||
cy.getByTestId('app-announcement-close').should('be.visible').click();
|
||||
cy.getByTestId('app-announcement').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show open or enacted proposals with proposal summary', function () {
|
||||
cy.get('body').then(($body) => {
|
||||
if (!$body.find('[data-testid="proposals-list-item"]').length) {
|
||||
cy.createMarket();
|
||||
cy.reload();
|
||||
waitForSpinner();
|
||||
}
|
||||
});
|
||||
it('should have link for validator page', function () {
|
||||
cy.getByTestId('menu-drawer').within(() => {
|
||||
cy.get('[href="/validators"]')
|
||||
cy.getByTestId('proposals-list-item')
|
||||
.should('have.length.at.least', 1)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.getByTestId('proposal-title')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('proposal-type').invoke('text').should('not.be.empty');
|
||||
cy.getByTestId('proposal-description')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('proposal-details')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('proposal-status')
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.getByTestId('vote-details').invoke('text').should('not.be.empty');
|
||||
cy.getByTestId('view-proposal-btn').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have external link for governance', function () {
|
||||
cy.getByTestId('home-proposals').within(() => {
|
||||
cy.getByTestId('external-link')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://vega.xyz/governance');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have link for validator page', function () {
|
||||
cy.getByTestId('home-validators').within(() => {
|
||||
cy.get('[href="/validators"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'Browse, and stake');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have external link for validators', function () {
|
||||
cy.getByTestId('home-validators').within(() => {
|
||||
cy.getByTestId('external-link')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'contain',
|
||||
'https://community.vega.xyz/c/mainnet-validator-candidates'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have information on active nodes', function () {
|
||||
cy.getByTestId('node-information')
|
||||
.first()
|
||||
.should('contain.text', '2')
|
||||
.and('contain.text', 'active nodes');
|
||||
});
|
||||
|
||||
it('should have information on consensus nodes', function () {
|
||||
cy.getByTestId('node-information')
|
||||
.last()
|
||||
.should('contain.text', '2')
|
||||
.and('contain.text', 'consensus nodes');
|
||||
});
|
||||
|
||||
it('should contain link to specific validators', function () {
|
||||
cy.getByTestId('validators')
|
||||
.should('have.length', '2')
|
||||
.each(($validator) => {
|
||||
cy.wrap($validator).find('a').should('have.attr', 'href');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have link for rewards page', function () {
|
||||
cy.getByTestId('home-rewards').within(() => {
|
||||
cy.get('[href="/rewards"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'See rewards');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have link for withdrawal page', function () {
|
||||
cy.getByTestId('home-vega-token').within(() => {
|
||||
cy.get('[href="/token/withdraw"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'Manage tokens');
|
||||
});
|
||||
});
|
||||
|
||||
it('should display network data', function () {
|
||||
cy.getByTestId('git-network-data')
|
||||
.should('contain.text', 'Reading network data from')
|
||||
.within(() => {
|
||||
cy.get('span')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'Validators');
|
||||
.should('have.text', 'http://localhost:3028/query');
|
||||
cy.getByTestId('link').should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should have link for rewards page', function () {
|
||||
cy.getByTestId('menu-drawer').within(() => {
|
||||
cy.get('[href="/rewards"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'Rewards');
|
||||
it('should display eth data', function () {
|
||||
cy.getByTestId('git-eth-data')
|
||||
.should('contain.text', 'Reading Ethereum data from')
|
||||
.within(() => {
|
||||
cy.get('span').should('have.text', 'http://localhost:8545');
|
||||
});
|
||||
});
|
||||
it('should have link for withdrawal page', function () {
|
||||
cy.getByTestId('menu-drawer').within(() => {
|
||||
cy.get('[href="/token/withdraw"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'Withdraw');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(function () {
|
||||
cy.viewport(
|
||||
Cypress.config('viewportWidth'),
|
||||
Cypress.config('viewportHeight')
|
||||
);
|
||||
it('should contain link for known issues on Github', function () {
|
||||
cy.getByTestId('git-info').within(() => {
|
||||
cy.contains('Known issues and feedback on')
|
||||
.find('[data-testid="link"]')
|
||||
.should(
|
||||
'have.attr',
|
||||
'href',
|
||||
'https://github.com/vegaprotocol/feedback/discussions'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mobile view - navigation bar', function () {
|
||||
before('Change to mobile resolution', function () {
|
||||
cy.viewport('iphone-xr');
|
||||
});
|
||||
|
||||
it('should have burger button', () => {
|
||||
cy.getByTestId('button-menu-drawer').should('be.visible').click();
|
||||
cy.getByTestId('menu-drawer').should('be.visible');
|
||||
});
|
||||
|
||||
it('should have link for proposal page', function () {
|
||||
cy.getByTestId('menu-drawer').within(() => {
|
||||
cy.get('[href="/proposals"]')
|
||||
.should('exist')
|
||||
.and('have.text', 'Proposals');
|
||||
});
|
||||
});
|
||||
it('should have link for validator page', function () {
|
||||
cy.getByTestId('menu-drawer').within(() => {
|
||||
cy.get('[href="/validators"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'Validators');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have link for rewards page', function () {
|
||||
cy.getByTestId('menu-drawer').within(() => {
|
||||
cy.get('[href="/rewards"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'Rewards');
|
||||
});
|
||||
});
|
||||
it('should have link for withdrawal page', function () {
|
||||
cy.getByTestId('menu-drawer').within(() => {
|
||||
cy.get('[href="/token/withdraw"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'Withdraw');
|
||||
});
|
||||
});
|
||||
|
||||
after(function () {
|
||||
cy.viewport(
|
||||
Cypress.config('viewportWidth'),
|
||||
Cypress.config('viewportHeight')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,3 +84,7 @@ export function verifyEthWalletAssociatedBalance(amount: string) {
|
||||
.parent(txTimeout)
|
||||
.should('contain', amount, txTimeout);
|
||||
}
|
||||
|
||||
export function closeDialog() {
|
||||
cy.getByTestId('dialog-close').click();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { navigateTo, navigation } from './common.functions';
|
||||
import { closeDialog, navigateTo, navigation } from './common.functions';
|
||||
import { ensureSpecifiedUnstakedTokensAreAssociated } from './staking.functions';
|
||||
|
||||
const newProposalButton = '[data-testid="new-proposal-link"]';
|
||||
@@ -12,7 +12,6 @@ const voteButtons = '[data-testid="vote-buttons"]';
|
||||
const dialogTitle = '[data-testid="dialog-title"]';
|
||||
const proposalVoteDeadline = '[data-testid="proposal-vote-deadline"]';
|
||||
const newProposalSubmitButton = '[data-testid="proposal-submit"]';
|
||||
const dialogCloseButton = '[data-testid="dialog-close"]';
|
||||
const epochTimeout = Cypress.env('epochTimeout');
|
||||
const proposalTimeout = { timeout: 14000 };
|
||||
|
||||
@@ -125,7 +124,7 @@ export function voteForProposal(vote: string) {
|
||||
'have.text',
|
||||
'Transaction complete'
|
||||
);
|
||||
cy.get(dialogCloseButton).click();
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
export function waitForProposalSync() {
|
||||
@@ -176,7 +175,7 @@ export function waitForProposalSubmitted() {
|
||||
'be.visible'
|
||||
);
|
||||
cy.contains('Proposal submitted', proposalTimeout).should('be.visible');
|
||||
cy.get(dialogCloseButton).click();
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
export function createRawProposal(proposerBalance?: string) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { closeDialog } from './common.functions';
|
||||
import { vegaWalletTeardown } from './wallet-teardown.functions';
|
||||
|
||||
const tokenAmountInputBox = '[data-testid="token-amount-input"]';
|
||||
@@ -18,7 +19,6 @@ const stakeValidatorListTotalStake = 'total-stake';
|
||||
const stakeValidatorListTotalShare = 'total-stake-share';
|
||||
const stakeValidatorListName = '[col-id="validator"]';
|
||||
const vegaKeySelector = '#vega-key-selector';
|
||||
const dialogCloseButton = '[data-testid="dialog-close"]';
|
||||
|
||||
const txTimeout = Cypress.env('txTimeout');
|
||||
const epochTimeout = Cypress.env('epochTimeout');
|
||||
@@ -54,7 +54,7 @@ export function stakingValidatorPageRemoveStake(stake: string) {
|
||||
.and('contain', `Remove ${stake} $VEGA tokens at the end of epoch`)
|
||||
.and('be.visible')
|
||||
.click();
|
||||
cy.get(dialogCloseButton).click();
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
export function stakingPageAssociateTokens(
|
||||
|
||||
@@ -5,6 +5,7 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_FAIRGROUND=false
|
||||
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_CONFIG_URL=''
|
||||
|
||||
NX_VEGA_URL=http://localhost:3028/query
|
||||
@@ -16,7 +17,6 @@ NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet3-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
|
||||
|
||||
#Test configuration variables
|
||||
CYPRESS_FAIRGROUND=false
|
||||
|
||||
@@ -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
|
||||
@@ -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),
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -19,6 +19,7 @@ CYPRESS_ETHEREUM_WALLET_ADDRESS=0xEe7D375bcB50C26d52E1A4a472D8822A2A22d94F
|
||||
CYPRESS_ETHEREUM_PROVIDER_URL=http://localhost:8545
|
||||
CYPRESS_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
CYPRESS_FAUCET_URL=http://localhost:1790/api/v1/mint
|
||||
CYPRESS_ORACLE_PUBKEY=6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61
|
||||
CYPRESS_TRUNCATED_VEGA_PUBLIC_KEY=02ecea…342f65
|
||||
CYPRESS_TRUNCATED_VEGA_PUBLIC_KEY2=7f9cf0…c25535
|
||||
CYPRESS_VEGA_ENV=CUSTOM
|
||||
|
||||
@@ -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' }, () => {
|
||||
|
||||
@@ -180,7 +180,15 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
'termination.BTC.value'
|
||||
);
|
||||
|
||||
// check that links to github for oracle proofs are shown
|
||||
cy.getByTestId(accordionContent)
|
||||
.getByTestId('oracle-proof-links')
|
||||
.find(`[data-testid="${externalLink}"]`)
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://github.com/vegaprotocol/well-known');
|
||||
|
||||
cy.getByTestId(accordionContent)
|
||||
.getByTestId('oracle-spec-links')
|
||||
.find(`[data-testid="${externalLink}"]`)
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', '/oracles');
|
||||
|
||||
@@ -84,6 +84,8 @@ describe('must submit order', { tags: '@smoke' }, () => {
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
size: '100',
|
||||
};
|
||||
createOrder(order);
|
||||
@@ -98,6 +100,8 @@ describe('must submit order', { tags: '@smoke' }, () => {
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
size: '100',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
};
|
||||
createOrder(order);
|
||||
testOrderSubmission(order);
|
||||
@@ -112,6 +116,8 @@ describe('must submit order', { tags: '@smoke' }, () => {
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
size: '100',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
price: '200',
|
||||
};
|
||||
createOrder(order);
|
||||
@@ -126,6 +132,8 @@ describe('must submit order', { tags: '@smoke' }, () => {
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GFN,
|
||||
size: '100',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
price: '50000',
|
||||
};
|
||||
createOrder(order);
|
||||
@@ -143,6 +151,8 @@ describe('must submit order', { tags: '@smoke' }, () => {
|
||||
size: '100',
|
||||
price: '1.00',
|
||||
expiresAt: expiresAt.toISOString().substring(0, 16),
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
};
|
||||
|
||||
createOrder(order);
|
||||
@@ -150,6 +160,8 @@ describe('must submit order', { tags: '@smoke' }, () => {
|
||||
price: '100000',
|
||||
expiresAt:
|
||||
new Date(order.expiresAt as string).getTime().toString() + '000000',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -182,6 +194,8 @@ describe(
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
size: '100',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
price: '200',
|
||||
};
|
||||
createOrder(order);
|
||||
@@ -196,6 +210,8 @@ describe(
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
size: '100',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
price: '50000',
|
||||
};
|
||||
createOrder(order);
|
||||
@@ -210,12 +226,16 @@ describe(
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
|
||||
size: '100',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
price: '1.00',
|
||||
expiresAt: displayTomorrow(),
|
||||
};
|
||||
createOrder(order);
|
||||
testOrderSubmission(order, {
|
||||
price: '100000',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
expiresAt:
|
||||
new Date(order.expiresAt as string).getTime().toString() + '000000',
|
||||
});
|
||||
@@ -251,6 +271,8 @@ describe(
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
size: '100',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
price: '200',
|
||||
};
|
||||
createOrder(order);
|
||||
@@ -265,6 +287,8 @@ describe(
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
size: '100',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
price: '50000',
|
||||
};
|
||||
createOrder(order);
|
||||
@@ -281,6 +305,8 @@ describe(
|
||||
size: '100',
|
||||
price: '1.00',
|
||||
expiresAt: displayTomorrow(),
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
};
|
||||
createOrder(order);
|
||||
testOrderSubmission(order, {
|
||||
@@ -320,6 +346,8 @@ describe(
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
size: '100',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
price: '200',
|
||||
};
|
||||
createOrder(order);
|
||||
@@ -335,6 +363,8 @@ describe(
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
size: '100',
|
||||
price: '50000',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
};
|
||||
createOrder(order);
|
||||
testOrderSubmission(order, { price: '5000000000' });
|
||||
@@ -350,6 +380,8 @@ describe(
|
||||
size: '100',
|
||||
price: '1.00',
|
||||
expiresAt: displayTomorrow(),
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
};
|
||||
createOrder(order);
|
||||
testOrderSubmission(order, {
|
||||
|
||||
@@ -35,6 +35,8 @@ type MarketPageMockData = {
|
||||
trigger?: Schema.AuctionTrigger;
|
||||
};
|
||||
|
||||
const ORACLE_PUBKEY = Cypress.env('ORACLE_PUBKEY');
|
||||
|
||||
const marketDataOverride = (
|
||||
data: MarketPageMockData
|
||||
): PartialDeep<MarketDataQuery> => ({
|
||||
@@ -96,7 +98,54 @@ const mockTradingPage = (
|
||||
aliasGQLQuery(req, 'Margins', marginsQuery());
|
||||
aliasGQLQuery(req, 'Assets', assetsQuery());
|
||||
aliasGQLQuery(req, 'Asset', assetQuery());
|
||||
aliasGQLQuery(req, 'MarketInfo', marketInfoQuery());
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'MarketInfo',
|
||||
marketInfoQuery({
|
||||
market: {
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
dataSourceSpecForSettlementData: {
|
||||
data: {
|
||||
sourceType: {
|
||||
sourceType: {
|
||||
signers: [
|
||||
{
|
||||
__typename: 'Signer',
|
||||
signer: {
|
||||
__typename: 'PubKey',
|
||||
key: ORACLE_PUBKEY,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
data: {
|
||||
sourceType: {
|
||||
sourceType: {
|
||||
signers: [
|
||||
{
|
||||
__typename: 'Signer',
|
||||
signer: {
|
||||
__typename: 'PubKey',
|
||||
key: ORACLE_PUBKEY,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
aliasGQLQuery(req, 'Trades', tradesQuery());
|
||||
aliasGQLQuery(req, 'Chart', chartQuery());
|
||||
aliasGQLQuery(req, 'Candles', candlesQuery());
|
||||
@@ -127,6 +176,40 @@ export const addMockTradingPage = () => {
|
||||
cy.mockGQL((req) => {
|
||||
mockTradingPage(req, state, tradingMode, trigger);
|
||||
});
|
||||
|
||||
// Prevent request to github, return some dummy content
|
||||
cy.intercept(
|
||||
'GET',
|
||||
/^https:\/\/raw.githubusercontent.com\/vegaprotocol\/well-known/,
|
||||
{
|
||||
body: [
|
||||
{
|
||||
name: 'Another oracle',
|
||||
url: 'https://zombo.com',
|
||||
description_markdown:
|
||||
'Some markdown describing the oracle provider.\n\nTwitter: @FacesPics2\n',
|
||||
oracle: {
|
||||
status: 'GOOD',
|
||||
status_reason: '',
|
||||
first_verified: '2022-01-01T00:00:00.000Z',
|
||||
last_verified: '2022-12-31T00:00:00.000Z',
|
||||
type: 'public_key',
|
||||
public_key: ORACLE_PUBKEY,
|
||||
},
|
||||
proofs: [
|
||||
{
|
||||
format: 'signed_message',
|
||||
available: true,
|
||||
type: 'public_key',
|
||||
public_key: ORACLE_PUBKEY,
|
||||
message: 'SOMEHEX',
|
||||
},
|
||||
],
|
||||
github_link: `https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/public_key-${ORACLE_PUBKEY}.toml`,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ 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_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml
|
||||
NX_VEGA_ENV=STAGNET3
|
||||
NX_VEGA_EXPLORER_URL=https://stagnet3.explorer.vega.xyz
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
matchFilter,
|
||||
liquidityProvisionsDataProvider,
|
||||
LiquidityTable,
|
||||
lpAggregatedDataProvider,
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
useNetworkParams,
|
||||
updateGridData,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
Tab,
|
||||
@@ -25,19 +25,25 @@ import {
|
||||
Indicator,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useCallback, useEffect, useMemo, useRef } 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';
|
||||
|
||||
const enum LiquidityTabs {
|
||||
Active = 'active',
|
||||
Inactive = 'inactive',
|
||||
MyLiquidityProvision = 'myLP',
|
||||
}
|
||||
|
||||
export const Liquidity = () => {
|
||||
const params = useParams();
|
||||
const marketId = params.marketId;
|
||||
@@ -48,18 +54,21 @@ const useReloadLiquidityData = (marketId: string | undefined) => {
|
||||
const { reload } = useDataProvider({
|
||||
dataProvider: liquidityProvisionsDataProvider,
|
||||
variables: { marketId: marketId || '' },
|
||||
update: () => true,
|
||||
skip: !marketId,
|
||||
});
|
||||
useEffect(() => {
|
||||
const interval = setInterval(reload, 10000);
|
||||
const interval = setInterval(reload, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [reload]);
|
||||
};
|
||||
|
||||
export const LiquidityContainer = ({
|
||||
marketId,
|
||||
filter,
|
||||
}: {
|
||||
marketId: string | undefined;
|
||||
filter?: Filter;
|
||||
}) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const { data: market } = useMarket(marketId);
|
||||
@@ -78,7 +87,7 @@ export const LiquidityContainer = ({
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: lpAggregatedDataProvider,
|
||||
update,
|
||||
variables: { marketId: marketId || '' },
|
||||
variables: { marketId: marketId || '', filter },
|
||||
skip: !marketId,
|
||||
});
|
||||
|
||||
@@ -126,47 +135,9 @@ export const LiquidityContainer = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const LiquidityViewContainer = ({
|
||||
marketId,
|
||||
}: {
|
||||
marketId: string | undefined;
|
||||
}) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
|
||||
const { data: market } = useMarket(marketId);
|
||||
const { data: marketData } = useStaticMarketData(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 (!gridRef.current?.api) {
|
||||
return false;
|
||||
}
|
||||
if (dataRef.current?.length) {
|
||||
dataRef.current = data;
|
||||
gridRef.current.api.refreshInfiniteCache();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[gridRef]
|
||||
);
|
||||
|
||||
const {
|
||||
data: liquidityProviders,
|
||||
loading,
|
||||
error,
|
||||
} = useDataProvider({
|
||||
dataProvider: lpAggregatedDataProvider,
|
||||
update,
|
||||
variables: { marketId: marketId || '' },
|
||||
skip: !marketId,
|
||||
});
|
||||
|
||||
const targetStake = marketData?.targetStake;
|
||||
const suppliedStake = marketData?.suppliedStake;
|
||||
const assetDecimalPlaces =
|
||||
@@ -178,44 +149,8 @@ export const LiquidityViewContainer = ({
|
||||
NetworkParams.market_liquidity_stakeToCcyVolume,
|
||||
NetworkParams.market_liquidity_targetstake_triggering_ratio,
|
||||
]);
|
||||
const stakeToCcyVolume = params.market_liquidity_stakeToCcyVolume;
|
||||
const triggeringRatio =
|
||||
params.market_liquidity_targetstake_triggering_ratio || '1';
|
||||
const myLpEdges = useMemo(
|
||||
() => liquidityProviders?.filter((e) => e.party.id === pubKey),
|
||||
[liquidityProviders, pubKey]
|
||||
);
|
||||
const activeEdges = useMemo(
|
||||
() =>
|
||||
liquidityProviders?.filter(
|
||||
(e) => e.status === Schema.LiquidityProvisionStatus.STATUS_ACTIVE
|
||||
),
|
||||
[liquidityProviders]
|
||||
);
|
||||
const inactiveEdges = useMemo(
|
||||
() =>
|
||||
liquidityProviders?.filter(
|
||||
(e) => e.status !== Schema.LiquidityProvisionStatus.STATUS_ACTIVE
|
||||
),
|
||||
[liquidityProviders]
|
||||
);
|
||||
|
||||
const enum LiquidityTabs {
|
||||
Active = 'active',
|
||||
Inactive = 'inactive',
|
||||
MyLiquidityProvision = 'myLP',
|
||||
}
|
||||
|
||||
const getActiveDefaultId = () => {
|
||||
if (myLpEdges && myLpEdges.length > 0) {
|
||||
return LiquidityTabs.MyLiquidityProvision;
|
||||
}
|
||||
if (activeEdges?.length) return LiquidityTabs.Active;
|
||||
else if (inactiveEdges && inactiveEdges.length > 0) {
|
||||
return LiquidityTabs.Inactive;
|
||||
}
|
||||
return LiquidityTabs.Active;
|
||||
};
|
||||
|
||||
const { percentage, status } = useCheckLiquidityStatus({
|
||||
suppliedStake: suppliedStake || 0,
|
||||
@@ -224,106 +159,113 @@ export const LiquidityViewContainer = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<AsyncRenderer loading={loading} error={error} data={liquidityProviders}>
|
||||
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||
<Header
|
||||
title={
|
||||
market?.tradableInstrument.instrument.name &&
|
||||
market?.tradableInstrument.instrument.code &&
|
||||
marketId && (
|
||||
<HeaderTitle
|
||||
primaryContent={`${
|
||||
market.tradableInstrument.instrument.code
|
||||
} ${t('liquidity provision')}`}
|
||||
secondaryContent={
|
||||
<Link to={Links[Routes.MARKET](marketId)}>
|
||||
<UiToolkitLink>{t('Go to trading')}</UiToolkitLink>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<HeaderStat
|
||||
heading={t('Target stake')}
|
||||
description={tooltipMapping['targetStake']}
|
||||
>
|
||||
<div>
|
||||
{targetStake
|
||||
? `${addDecimalsFormatNumber(
|
||||
targetStake,
|
||||
assetDecimalPlaces ?? 0
|
||||
)} ${symbol}`
|
||||
: '-'}
|
||||
</div>
|
||||
</HeaderStat>
|
||||
<HeaderStat
|
||||
heading={t('Supplied stake')}
|
||||
description={tooltipMapping['suppliedStake']}
|
||||
>
|
||||
<div>
|
||||
{suppliedStake
|
||||
? `${addDecimalsFormatNumber(
|
||||
suppliedStake,
|
||||
assetDecimalPlaces ?? 0
|
||||
)} ${symbol}`
|
||||
: '-'}
|
||||
</div>
|
||||
</HeaderStat>
|
||||
<HeaderStat
|
||||
heading={t('Liquidity supplied')}
|
||||
testId="liquidity-supplied"
|
||||
>
|
||||
<Indicator variant={status} />
|
||||
<Header
|
||||
title={
|
||||
market?.tradableInstrument.instrument.name &&
|
||||
market?.tradableInstrument.instrument.code &&
|
||||
marketId && (
|
||||
<HeaderTitle
|
||||
primaryContent={`${market.tradableInstrument.instrument.code} ${t(
|
||||
'liquidity provision'
|
||||
)}`}
|
||||
secondaryContent={
|
||||
<Link to={Links[Routes.MARKET](marketId)}>
|
||||
<UiToolkitLink>{t('Go to trading')}</UiToolkitLink>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<HeaderStat
|
||||
heading={t('Target stake')}
|
||||
description={tooltipMapping['targetStake']}
|
||||
>
|
||||
<div>
|
||||
{targetStake
|
||||
? `${addDecimalsFormatNumber(
|
||||
targetStake,
|
||||
assetDecimalPlaces ?? 0
|
||||
)} ${symbol}`
|
||||
: '-'}
|
||||
</div>
|
||||
</HeaderStat>
|
||||
<HeaderStat
|
||||
heading={t('Supplied stake')}
|
||||
description={tooltipMapping['suppliedStake']}
|
||||
>
|
||||
<div>
|
||||
{suppliedStake
|
||||
? `${addDecimalsFormatNumber(
|
||||
suppliedStake,
|
||||
assetDecimalPlaces ?? 0
|
||||
)} ${symbol}`
|
||||
: '-'}
|
||||
</div>
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Liquidity supplied')} testId="liquidity-supplied">
|
||||
<Indicator variant={status} />
|
||||
|
||||
{formatNumberPercentage(percentage, 2)}
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Market ID')}>
|
||||
<div className="break-word">{marketId}</div>
|
||||
</HeaderStat>
|
||||
</Header>
|
||||
<Tabs defaultValue={getActiveDefaultId()}>
|
||||
<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>
|
||||
{formatNumberPercentage(percentage, 2)}
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Market ID')}>
|
||||
<div className="break-word">{marketId}</div>
|
||||
</HeaderStat>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
LiquidityViewHeader.displayName = 'LiquidityViewHeader';
|
||||
|
||||
export const LiquidityViewContainer = ({
|
||||
marketId,
|
||||
}: {
|
||||
marketId: string | undefined;
|
||||
}) => {
|
||||
const [tab, setTab] = useState<string | undefined>(undefined);
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: lpAggregatedDataProvider,
|
||||
skipUpdates: true,
|
||||
variables: { marketId: marketId || '' },
|
||||
skip: !marketId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
}
|
||||
}, [data, pubKey]);
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
@@ -13,6 +13,7 @@ import { useGlobalStore, usePageTitleStore } from '../../stores';
|
||||
import { TradeGrid, TradePanels } from './trade-grid';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
|
||||
const calculatePrice = (markPrice?: string, decimalPlaces?: number) => {
|
||||
return markPrice && decimalPlaces
|
||||
@@ -66,14 +67,7 @@ export const MarketPage = () => {
|
||||
const update = useGlobalStore((store) => store.update);
|
||||
const lastMarketId = useGlobalStore((store) => store.marketId);
|
||||
|
||||
const onSelect = useCallback(
|
||||
(id: string) => {
|
||||
if (id && id !== marketId) {
|
||||
navigate(Links[Routes.MARKET](id));
|
||||
}
|
||||
},
|
||||
[marketId, navigate]
|
||||
);
|
||||
const onSelect = useMarketClickHandler();
|
||||
|
||||
const { data, error, loading } = useDataProvider({
|
||||
dataProvider: marketProvider,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TradesContainer } from '@vegaprotocol/trades';
|
||||
import { LayoutPriority } from 'allotment';
|
||||
import classNames from 'classnames';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
import type { ReactNode, ComponentProps } from 'react';
|
||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||
@@ -27,9 +27,9 @@ import { TradeMarketHeader } from './trade-market-header';
|
||||
import { NO_MARKET } from './constants';
|
||||
import { LiquidityContainer } from '../liquidity/liquidity';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
|
||||
type MarketDependantView =
|
||||
| typeof CandlesChartContainer
|
||||
@@ -66,7 +66,7 @@ type TradingView = keyof typeof TradingViews;
|
||||
|
||||
interface TradeGridProps {
|
||||
market: Market | null;
|
||||
onSelect: (marketId: string) => void;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}
|
||||
|
||||
@@ -78,15 +78,7 @@ interface BottomPanelProps {
|
||||
const MarketBottomPanel = memo(
|
||||
({ marketId, pinnedAsset }: BottomPanelProps) => {
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const navigate = useNavigate();
|
||||
const onMarketClick = useCallback(
|
||||
(marketId: string) => {
|
||||
navigate(Links[Routes.MARKET](marketId), {
|
||||
replace: true,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const onMarketClick = useMarketClickHandler(true);
|
||||
|
||||
return 'xxxl' === screenSize ? (
|
||||
<ResizableGrid proportionalLayout minSize={200}>
|
||||
@@ -189,7 +181,7 @@ const MainGrid = memo(
|
||||
pinnedAsset,
|
||||
}: {
|
||||
marketId: string;
|
||||
onSelect?: (marketId: string) => void;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -230,12 +222,7 @@ const MainGrid = memo(
|
||||
/>
|
||||
</Tab>
|
||||
<Tab id="info" name={t('Info')}>
|
||||
<TradingViews.Info
|
||||
marketId={marketId}
|
||||
onSelect={(id: string) => {
|
||||
onSelect?.(id);
|
||||
}}
|
||||
/>
|
||||
<TradingViews.Info marketId={marketId} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
@@ -304,7 +291,7 @@ const TradeGridChild = ({ children }: TradeGridChildProps) => {
|
||||
|
||||
interface TradePanelsProps {
|
||||
market: Market | null;
|
||||
onSelect: (marketId: string) => void;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onClickCollateral: () => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
@@ -320,7 +307,7 @@ export const TradePanels = ({
|
||||
const renderView = () => {
|
||||
const Component = memo<{
|
||||
marketId: string;
|
||||
onSelect: (marketId: string) => void;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onClickCollateral: () => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
|
||||
@@ -22,7 +22,7 @@ import { MarketState as State } from '@vegaprotocol/types';
|
||||
|
||||
interface TradeMarketHeaderProps {
|
||||
market: Market | null;
|
||||
onSelect: (marketId: string) => void;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
}
|
||||
|
||||
export const TradeMarketHeader = ({
|
||||
@@ -91,7 +91,6 @@ export const TradeMarketHeader = ({
|
||||
</HeaderStat>
|
||||
<HeaderStatMarketTradingMode
|
||||
marketId={market?.id}
|
||||
onSelect={onSelect}
|
||||
initialTradingMode={market?.tradingMode}
|
||||
/>
|
||||
<MarketState market={market} />
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { MarketsContainer } from '@vegaprotocol/market-list';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
|
||||
export const Markets = () => {
|
||||
const navigate = useNavigate();
|
||||
const handleOnSelect = useCallback(
|
||||
(marketId: string) => {
|
||||
navigate(Links[Routes.MARKET](marketId));
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const handleOnSelect = useMarketClickHandler();
|
||||
return <MarketsContainer onSelect={handleOnSelect} />;
|
||||
};
|
||||
|
||||
@@ -19,25 +19,18 @@ import { usePageTitleStore } from '../../stores';
|
||||
import { LedgerContainer } from '@vegaprotocol/ledger';
|
||||
import { AccountsContainer } from '../../components/accounts-container';
|
||||
import { AccountHistoryContainer } from './account-history-container';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
|
||||
export const Portfolio = () => {
|
||||
const { updateTitle } = usePageTitleStore((store) => ({
|
||||
updateTitle: store.updateTitle,
|
||||
}));
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
updateTitle(titlefy([t('Portfolio')]));
|
||||
}, [updateTitle]);
|
||||
|
||||
const onMarketClick = (marketId: string) => {
|
||||
navigate(Links[Routes.MARKET](marketId), {
|
||||
replace: true,
|
||||
});
|
||||
};
|
||||
const onMarketClick = useMarketClickHandler(true);
|
||||
|
||||
const wrapperClasses = 'h-full max-h-full flex flex-col';
|
||||
return (
|
||||
|
||||
@@ -25,7 +25,7 @@ const getTradingModeLabel = (
|
||||
|
||||
interface HeaderStatMarketTradingModeProps {
|
||||
marketId?: string;
|
||||
onSelect?: (marketId: string) => void;
|
||||
onSelect?: (marketId: string, metaKey?: boolean) => void;
|
||||
initialTradingMode?: Schema.MarketTradingMode;
|
||||
initialTrigger?: Schema.AuctionTrigger;
|
||||
}
|
||||
@@ -66,7 +66,9 @@ export const MarketTradingMode = ({
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
description={<TradingModeTooltip marketId={marketId} skip={!inView} />}
|
||||
description={
|
||||
<TradingModeTooltip marketId={marketId} skip={!inView} skipGrid />
|
||||
}
|
||||
>
|
||||
<span ref={ref}>
|
||||
{getTradingModeLabel(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RefObject } from 'react';
|
||||
import type { RefObject, MouseEvent } from 'react';
|
||||
import { FeesCell } from '@vegaprotocol/market-info';
|
||||
import {
|
||||
calcCandleHigh,
|
||||
@@ -157,14 +157,14 @@ export const columnHeaders: Column[] = [
|
||||
];
|
||||
|
||||
export type OnCellClickHandler = (
|
||||
e: React.MouseEvent,
|
||||
e: MouseEvent,
|
||||
kind: ColumnKind,
|
||||
value: string
|
||||
) => void;
|
||||
|
||||
export const columns = (
|
||||
market: MarketMaybeWithDataAndCandles,
|
||||
onSelect: (id: string) => void,
|
||||
onSelect: (id: string, metaKey?: boolean) => void,
|
||||
onCellClick: OnCellClickHandler,
|
||||
inViewRoot?: RefObject<HTMLElement>
|
||||
) => {
|
||||
@@ -174,14 +174,7 @@ export const columns = (
|
||||
const candleLow = market.candles && calcCandleLow(market.candles);
|
||||
const candleHigh = market.candles && calcCandleHigh(market.candles);
|
||||
const candleVolume = market.candles && calcCandleVolume(market.candles);
|
||||
const handleKeyPress = (
|
||||
event: React.KeyboardEvent<HTMLAnchorElement>,
|
||||
id: string
|
||||
) => {
|
||||
if (event.key === 'Enter' && onSelect) {
|
||||
return onSelect(id);
|
||||
}
|
||||
};
|
||||
|
||||
const selectMarketColumns: Column[] = [
|
||||
{
|
||||
kind: ColumnKind.Market,
|
||||
@@ -189,10 +182,10 @@ export const columns = (
|
||||
<Link
|
||||
to={Links[Routes.MARKET](market.id)}
|
||||
data-testid={`market-link-${market.id}`}
|
||||
onKeyPress={(event) => handleKeyPress(event, market.id)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onSelect(market.id);
|
||||
e.stopPropagation();
|
||||
onSelect(market.id, e.metaKey);
|
||||
}}
|
||||
>
|
||||
<UILink>{market.tradableInstrument.instrument.code}</UILink>
|
||||
@@ -352,7 +345,7 @@ export const columns = (
|
||||
|
||||
export const columnsPositionMarkets = (
|
||||
market: MarketMaybeWithDataAndCandles,
|
||||
onSelect: (id: string) => void,
|
||||
onSelect: (id: string, metaKey?: boolean) => void,
|
||||
inViewRoot?: RefObject<HTMLElement>,
|
||||
openVolume?: string,
|
||||
onCellClick?: OnCellClickHandler
|
||||
@@ -362,14 +355,6 @@ export const columnsPositionMarkets = (
|
||||
.filter((c: string | undefined): c is CandleClose => !isNil(c));
|
||||
const candleLow = market.candles && calcCandleLow(market.candles);
|
||||
const candleHigh = market.candles && calcCandleHigh(market.candles);
|
||||
const handleKeyPress = (
|
||||
event: React.KeyboardEvent<HTMLSpanElement>,
|
||||
id: string
|
||||
) => {
|
||||
if (event.key === 'Enter' && onSelect) {
|
||||
return onSelect(id);
|
||||
}
|
||||
};
|
||||
const candleVolume = market.candles && calcCandleVolume(market.candles);
|
||||
const selectMarketColumns: Column[] = [
|
||||
{
|
||||
@@ -378,10 +363,10 @@ export const columnsPositionMarkets = (
|
||||
<Link
|
||||
to={Links[Routes.MARKET](market.id)}
|
||||
data-testid={`market-link-${market.id}`}
|
||||
onKeyPress={(event) => handleKeyPress(event, market.id)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onSelect(market.id);
|
||||
e.stopPropagation();
|
||||
onSelect(market.id, e.metaKey);
|
||||
}}
|
||||
>
|
||||
<UILink>{market.tradableInstrument.instrument.code}</UILink>
|
||||
|
||||
@@ -37,14 +37,14 @@ export const SelectMarketTableRow = ({
|
||||
}: {
|
||||
detailed?: boolean;
|
||||
columns: Column[];
|
||||
onSelect: (id: string) => void;
|
||||
onSelect: (id: string, metaKey?: boolean) => void;
|
||||
marketId: string;
|
||||
}) => {
|
||||
return (
|
||||
<tr
|
||||
className={`hover:bg-neutral-200 dark:hover:bg-neutral-700 cursor-pointer relative h-[34px]`}
|
||||
onClick={() => {
|
||||
onSelect(marketId);
|
||||
onClick={(ev) => {
|
||||
onSelect(marketId, ev.metaKey);
|
||||
}}
|
||||
data-testid={`market-link-${marketId}`}
|
||||
>
|
||||
|
||||
@@ -178,6 +178,6 @@ describe('SelectMarket', () => {
|
||||
expect(screen.getByText('25.00%')).toBeTruthy(); // price change
|
||||
expect(container).toHaveTextContent(/1,000/); // volume
|
||||
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
|
||||
expect(onSelect).toHaveBeenCalledWith('1');
|
||||
expect(onSelect).toHaveBeenCalledWith('1', false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ export const SelectAllMarketsTableBody = ({
|
||||
markets?: MarketMaybeWithDataAndCandles[] | null;
|
||||
positions?: PositionFieldsFragment[];
|
||||
title?: string;
|
||||
onSelect: (id: string) => void;
|
||||
onSelect: (id: string, metaKey?: boolean) => void;
|
||||
onCellClick: OnCellClickHandler;
|
||||
headers?: Column[];
|
||||
tableColumns?: (
|
||||
@@ -95,7 +95,7 @@ export const SelectMarketPopover = ({
|
||||
}: {
|
||||
marketCode: string;
|
||||
marketName: string;
|
||||
onSelect: (id: string) => void;
|
||||
onSelect: (id: string, metaKey?: boolean) => void;
|
||||
onCellClick: OnCellClickHandler;
|
||||
}) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
@@ -116,8 +116,8 @@ export const SelectMarketPopover = ({
|
||||
skip: !pubKey,
|
||||
});
|
||||
const onSelectMarket = useCallback(
|
||||
(marketId: string) => {
|
||||
onSelect(marketId);
|
||||
(marketId: string, metaKey?: boolean) => {
|
||||
onSelect(marketId, metaKey);
|
||||
setOpen(false);
|
||||
},
|
||||
[onSelect]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import { useCallback } from 'react';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
|
||||
export const useMarketClickHandler = (replace = false) => {
|
||||
const navigate = useNavigate();
|
||||
const { marketId } = useParams();
|
||||
const { pathname } = useLocation();
|
||||
const isMarketPage = pathname.match(/^\/markets\/(.+)/);
|
||||
return useCallback(
|
||||
(selectedId: string, metaKey?: boolean) => {
|
||||
const link = Links[Routes.MARKET](selectedId);
|
||||
if (metaKey) {
|
||||
window.open(`/#${link}`, '_blank');
|
||||
} else if (selectedId !== marketId || !isMarketPage) {
|
||||
navigate(link, { replace });
|
||||
}
|
||||
},
|
||||
[navigate, marketId, replace, isMarketPage]
|
||||
);
|
||||
};
|
||||
@@ -474,15 +474,20 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
}
|
||||
|
||||
if (tx.order && tx.order.rejectionReason) {
|
||||
const rejectionReason =
|
||||
getRejectionReason(tx.order) || tx.order.rejectionReason || '';
|
||||
return (
|
||||
<>
|
||||
<ToastHeading>{t('Order rejected')}</ToastHeading>
|
||||
<p>
|
||||
{t(
|
||||
'Your order has been rejected because: %s',
|
||||
getRejectionReason(tx.order) || ''
|
||||
)}
|
||||
</p>
|
||||
{rejectionReason || tx.order.rejectionReason ? (
|
||||
<p>
|
||||
{t('Your order has been rejected because: %s', [
|
||||
rejectionReason || tx.order.rejectionReason,
|
||||
])}
|
||||
</p>
|
||||
) : (
|
||||
<p>{t('Your order has been rejected.')}</p>
|
||||
)}
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
@@ -576,10 +581,9 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
walletNoConnectionCodes.includes(tx.error.code);
|
||||
if (orderRejection) {
|
||||
label = t('Order rejected');
|
||||
errorMessage = t(
|
||||
'Your order has been rejected because: %s',
|
||||
orderRejection
|
||||
);
|
||||
errorMessage = t('Your order has been rejected because: %s', [
|
||||
orderRejection || tx.order?.rejectionReason || ' ',
|
||||
]);
|
||||
}
|
||||
if (walletError) {
|
||||
label = t('Wallet disconnected');
|
||||
@@ -645,7 +649,11 @@ 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)
|
||||
(tx.order &&
|
||||
[OrderStatus.STATUS_REJECTED, OrderStatus.STATUS_STOPPED].includes(
|
||||
tx.order.status
|
||||
)) ||
|
||||
tx.order?.rejectionReason
|
||||
? Intent.Danger
|
||||
: intentMap[tx.status];
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Head from 'next/head';
|
||||
import type { AppProps } from 'next/app';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
@@ -23,14 +25,12 @@ import {
|
||||
import './styles.css';
|
||||
import { useGlobalStore, usePageTitleStore } from '../stores';
|
||||
import { Footer } from '../components/footer';
|
||||
import { useMemo, useState } from 'react';
|
||||
import DialogsContainer from './dialogs-container';
|
||||
import ToastsManager from './toasts-manager';
|
||||
import { HashRouter, useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { Connectors } from '../lib/vega-connectors';
|
||||
import { ViewingBanner } from '../components/viewing-banner';
|
||||
import { Banner } from '../components/banner';
|
||||
import classNames from 'classnames';
|
||||
import { AppLoader, DynamicLoader } from '../components/app-loader';
|
||||
import { Navbar } from '../components/navbar';
|
||||
|
||||
@@ -57,7 +57,7 @@ const Title = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const TransactionsHandler = () => {
|
||||
const InitializeHandlers = () => {
|
||||
useVegaTransactionManager();
|
||||
useVegaTransactionUpdater();
|
||||
useEthTransactionManager();
|
||||
@@ -93,7 +93,7 @@ function AppBody({ Component }: AppProps) {
|
||||
</div>
|
||||
<DialogsContainer />
|
||||
<ToastsManager />
|
||||
<TransactionsHandler />
|
||||
<InitializeHandlers />
|
||||
<MaybeConnectEagerly />
|
||||
</div>
|
||||
);
|
||||
|
||||
+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;'
|
||||
@@ -9,6 +9,7 @@ export * from './lib/cells/price-change-cell';
|
||||
export * from './lib/cells/price-flash-cell';
|
||||
export * from './lib/cells/vol-cell';
|
||||
export * from './lib/cells/centered-grid-cell';
|
||||
export * from './lib/cells/market-name-cell';
|
||||
|
||||
export * from './lib/filters/date-range-filter';
|
||||
export * from './lib/filters/set-filter';
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import get from 'lodash/get';
|
||||
|
||||
interface MarketNameCellProps {
|
||||
value?: string;
|
||||
data?: { id?: string; marketId?: string; market?: { id: string } };
|
||||
idPath?: string;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
}
|
||||
|
||||
export const MarketNameCell = ({
|
||||
value,
|
||||
data,
|
||||
idPath,
|
||||
onMarketClick,
|
||||
}: MarketNameCellProps) => {
|
||||
const id = data ? get(data, idPath ?? 'id', 'all') : '';
|
||||
const handleOnClick = useCallback(
|
||||
(ev: MouseEvent<HTMLButtonElement>) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (onMarketClick) {
|
||||
onMarketClick(id, ev.metaKey);
|
||||
}
|
||||
},
|
||||
[id, onMarketClick]
|
||||
);
|
||||
if (!data) return null;
|
||||
return (
|
||||
<button onClick={handleOnClick} tabIndex={0}>
|
||||
{value}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -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,6 +107,102 @@ describe('DealTicket', () => {
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '0.1',
|
||||
price: '300.22',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
persist: false,
|
||||
reduceOnly: true,
|
||||
postOnly: false,
|
||||
};
|
||||
|
||||
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')).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 () => {
|
||||
render(generateJsx());
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { memo, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { memo, useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { DealTicketAmount } from './deal-ticket-amount';
|
||||
import { DealTicketButton } from './deal-ticket-button';
|
||||
@@ -16,10 +16,12 @@ import {
|
||||
useVegaWalletDialogStore,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import {
|
||||
Checkbox,
|
||||
ExternalLink,
|
||||
InputError,
|
||||
Intent,
|
||||
Notification,
|
||||
Tooltip,
|
||||
TinyScroll,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
@@ -42,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;
|
||||
@@ -101,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', {
|
||||
@@ -146,6 +157,26 @@ export const DealTicket = ({
|
||||
clearErrors,
|
||||
]);
|
||||
|
||||
const disablePostOnlyCheckbox = useMemo(() => {
|
||||
const disabled = order
|
||||
? [
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
].includes(order.timeInForce)
|
||||
: true;
|
||||
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();
|
||||
@@ -190,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');
|
||||
@@ -239,8 +280,23 @@ export const DealTicket = ({
|
||||
value={order.timeInForce}
|
||||
orderType={order.type}
|
||||
onSelect={(timeInForce) => {
|
||||
update({ timeInForce });
|
||||
// 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,
|
||||
@@ -276,6 +332,70 @@ export const DealTicket = ({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-2 pb-2 justify-between">
|
||||
<Controller
|
||||
name="postOnly"
|
||||
control={control}
|
||||
render={() => (
|
||||
<Checkbox
|
||||
name="post-only"
|
||||
checked={order.postOnly}
|
||||
disabled={disablePostOnlyCheckbox}
|
||||
onCheckedChange={() => {
|
||||
update({ postOnly: !order.postOnly, reduceOnly: false });
|
||||
}}
|
||||
label={
|
||||
<Tooltip
|
||||
description={
|
||||
<span>
|
||||
{disablePostOnlyCheckbox
|
||||
? t(
|
||||
'"Post only" can not be used on "Fill or Kill" or "Immediate or Cancel" orders.'
|
||||
)
|
||||
: t(
|
||||
'"Post only" will ensure the order is not filled immediately but is placed on the order book as a passive order. When the order is processed it is either stopped (if it would not be filled immediately), or placed in the order book as a passive order until the price taker matches with it.'
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span className="text-xs">{t('Post only')}</span>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="reduceOnly"
|
||||
control={control}
|
||||
render={() => (
|
||||
<Checkbox
|
||||
name="reduce-only"
|
||||
checked={order.reduceOnly}
|
||||
disabled={disableReduceOnlyCheckbox}
|
||||
onCheckedChange={() => {
|
||||
update({ postOnly: false, reduceOnly: !order.reduceOnly });
|
||||
}}
|
||||
label={
|
||||
<Tooltip
|
||||
description={
|
||||
<span>
|
||||
{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>
|
||||
}
|
||||
>
|
||||
<span className="text-xs">{t('Reduce only')}</span>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<SummaryMessage
|
||||
errorMessage={errors.summary?.message}
|
||||
asset={asset}
|
||||
@@ -296,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>
|
||||
|
||||
@@ -96,16 +96,6 @@ export const TimeInForceSelector = ({
|
||||
id="select-time-in-force"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
// setPreviousTimeInForce({
|
||||
// ...previousTimeInForce,
|
||||
// [orderType]: e.target.value,
|
||||
// });
|
||||
|
||||
// if (previousOrderType !== orderType) {
|
||||
// setPreviousOrderType(orderType);
|
||||
// const prev = previousTimeInForce[orderType as OrderType];
|
||||
// onSelect(prev);
|
||||
// }
|
||||
onSelect(e.target.value as Schema.OrderTimeInForce);
|
||||
}}
|
||||
className="w-full"
|
||||
|
||||
@@ -26,7 +26,7 @@ export const compileGridData = (
|
||||
| 'targetStake'
|
||||
| 'trigger'
|
||||
> | null,
|
||||
onSelect?: (id: string) => void
|
||||
onSelect?: (id: string, metaKey?: boolean) => void
|
||||
): { label: ReactNode; value?: ReactNode }[] => {
|
||||
const grid: SimpleGridProps['grid'] = [];
|
||||
const isLiquidityMonitoringAuction =
|
||||
@@ -78,7 +78,7 @@ export const compileGridData = (
|
||||
label: (
|
||||
<Link
|
||||
to={`/liquidity/${market.id}`}
|
||||
onClick={() => onSelect && onSelect(market.id)}
|
||||
onClick={(ev) => onSelect && onSelect(market.id, ev.metaKey)}
|
||||
>
|
||||
<UILink>{t('Current liquidity')}</UILink>
|
||||
</Link>
|
||||
|
||||
@@ -12,14 +12,16 @@ import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
|
||||
|
||||
type TradingModeTooltipProps = {
|
||||
marketId?: string;
|
||||
onSelect?: (marketId: string) => void;
|
||||
onSelect?: (marketId: string, metaKey?: boolean) => void;
|
||||
skip?: boolean;
|
||||
skipGrid?: boolean;
|
||||
};
|
||||
|
||||
export const TradingModeTooltip = ({
|
||||
marketId,
|
||||
onSelect,
|
||||
skip,
|
||||
skipGrid,
|
||||
}: TradingModeTooltipProps) => {
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
const { data: market } = useMarket(marketId);
|
||||
@@ -42,7 +44,7 @@ export const TradingModeTooltip = ({
|
||||
);
|
||||
|
||||
const compiledGrid =
|
||||
onSelect && compileGridData(market, marketData, onSelect);
|
||||
!skipGrid && compileGridData(market, marketData, onSelect);
|
||||
|
||||
switch (marketTradingMode) {
|
||||
case Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS: {
|
||||
@@ -103,6 +105,7 @@ export const TradingModeTooltip = ({
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).AUCTION_TYPE_OPENING}
|
||||
className="ml-1"
|
||||
>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>
|
||||
@@ -129,6 +132,7 @@ export const TradingModeTooltip = ({
|
||||
createDocsLinks(VEGA_DOCS_URL)
|
||||
.AUCTION_TYPE_LIQUIDITY_MONITORING
|
||||
}
|
||||
className="ml-1"
|
||||
>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>
|
||||
@@ -153,6 +157,7 @@ export const TradingModeTooltip = ({
|
||||
createDocsLinks(VEGA_DOCS_URL)
|
||||
.AUCTION_TYPE_LIQUIDITY_MONITORING
|
||||
}
|
||||
className="ml-1"
|
||||
>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>
|
||||
@@ -175,6 +180,7 @@ export const TradingModeTooltip = ({
|
||||
createDocsLinks(VEGA_DOCS_URL)
|
||||
.AUCTION_TYPE_PRICE_MONITORING
|
||||
}
|
||||
className="ml-1"
|
||||
>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import 'jest-canvas-mock';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { defaultFallbackInView } from 'react-intersection-observer';
|
||||
|
||||
defaultFallbackInView(true);
|
||||
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];
|
||||
|
||||
@@ -277,6 +277,7 @@ function compileEnvVars() {
|
||||
),
|
||||
ETH_LOCAL_PROVIDER_URL: process.env['NX_ETH_LOCAL_PROVIDER_URL'],
|
||||
ETH_WALLET_MNEMONIC: process.env['NX_ETH_WALLET_MNEMONIC'],
|
||||
ORACLE_PROOFS_URL: process.env['NX_ORACLE_PROOFS_URL'],
|
||||
VEGA_DOCS_URL: process.env['NX_VEGA_DOCS_URL'],
|
||||
VEGA_EXPLORER_URL: process.env['NX_VEGA_EXPLORER_URL'],
|
||||
VEGA_TOKEN_URL: process.env['NX_VEGA_TOKEN_URL'],
|
||||
|
||||
@@ -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',
|
||||
@@ -21,7 +21,6 @@ const EmptyLinks: DAppLinks = {
|
||||
[Networks.STAGNET3]: '',
|
||||
[Networks.TESTNET]: '',
|
||||
[Networks.MAINNET]: '',
|
||||
[Networks.MIRROR]: '',
|
||||
};
|
||||
|
||||
const ExplorerLinks = {
|
||||
@@ -61,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 = {
|
||||
@@ -20,6 +18,7 @@ const schemaObject = {
|
||||
GIT_COMMIT_HASH: z.optional(z.string()),
|
||||
GIT_ORIGIN_URL: z.optional(z.string()),
|
||||
GITHUB_FEEDBACK_URL: z.optional(z.string()),
|
||||
ORACLE_PROOFS_URL: z.optional(z.string().url()),
|
||||
VEGA_ENV: z.nativeEnum(Networks),
|
||||
VEGA_EXPLORER_URL: z.optional(z.string()),
|
||||
VEGA_TOKEN_URL: z.optional(z.string()),
|
||||
|
||||
@@ -8,7 +8,7 @@ export const FillsContainer = ({
|
||||
onMarketClick,
|
||||
}: {
|
||||
marketId?: string;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
}) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useBottomPlaceholder } from '@vegaprotocol/react-helpers';
|
||||
interface FillsManagerProps {
|
||||
partyId: string;
|
||||
marketId?: string;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
}
|
||||
|
||||
export const FillsManager = ({
|
||||
|
||||
@@ -14,16 +14,13 @@ import {
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AgGridDynamic as AgGrid,
|
||||
positiveClassNames,
|
||||
negativeClassNames,
|
||||
MarketNameCell,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type { VegaValueFormatterParams } from '@vegaprotocol/datagrid';
|
||||
import { forwardRef } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Trade } from './fills-data-provider';
|
||||
@@ -34,7 +31,7 @@ const MAKER = 'MAKER';
|
||||
|
||||
export type Props = (AgGridReactProps | AgReactUiProps) & {
|
||||
partyId: string;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
};
|
||||
|
||||
export const FillsTable = forwardRef<AgGridReact, Props>(
|
||||
@@ -48,30 +45,14 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
|
||||
getRowId={({ data }) => data?.id}
|
||||
tooltipShowDelay={0}
|
||||
tooltipHideDelay={2000}
|
||||
components={{ MarketNameCell }}
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="market.tradableInstrument.instrument.name"
|
||||
cellRenderer={({
|
||||
value,
|
||||
data,
|
||||
}: VegaICellRendererParams<
|
||||
Trade,
|
||||
'market.tradableInstrument.instrument.name'
|
||||
>) =>
|
||||
onMarketClick ? (
|
||||
<Link
|
||||
onClick={() =>
|
||||
data?.market?.id && onMarketClick(data?.market?.id)
|
||||
}
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
) : (
|
||||
value
|
||||
)
|
||||
}
|
||||
cellRenderer="MarketNameCell"
|
||||
cellRendererParams={{ idPath: 'market.id', onMarketClick }}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Size')}
|
||||
|
||||
@@ -57,7 +57,7 @@ fragment LiquidityProvisionFields on LiquidityProvision {
|
||||
|
||||
query LiquidityProvisions($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
liquidityProvisionsConnection {
|
||||
liquidityProvisionsConnection(live: true) {
|
||||
edges {
|
||||
node {
|
||||
...LiquidityProvisionFields
|
||||
@@ -103,13 +103,3 @@ query LiquidityProviderFeeShare($marketId: ID!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscription LiquidityProviderFeeShareUpdate($marketId: ID!) {
|
||||
marketsData(marketIds: [$marketId]) {
|
||||
liquidityProviderFeeShare {
|
||||
partyId
|
||||
equityLikeShare
|
||||
averageEntryValuation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
fragment MarketNode on Market {
|
||||
id
|
||||
liquidityProvisionsConnection {
|
||||
liquidityProvisionsConnection(live: true) {
|
||||
edges {
|
||||
node {
|
||||
commitmentAmount
|
||||
|
||||
+2
-43
@@ -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 {
|
||||
@@ -138,7 +131,7 @@ export type MarketLpQueryResult = Apollo.QueryResult<MarketLpQuery, MarketLpQuer
|
||||
export const LiquidityProvisionsDocument = gql`
|
||||
query LiquidityProvisions($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
liquidityProvisionsConnection {
|
||||
liquidityProvisionsConnection(live: true) {
|
||||
edges {
|
||||
node {
|
||||
...LiquidityProvisionFields
|
||||
@@ -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>;
|
||||
+1
-1
@@ -13,7 +13,7 @@ export type LiquidityProvisionMarketsQuery = { __typename?: 'Query', marketsConn
|
||||
export const MarketNodeFragmentDoc = gql`
|
||||
fragment MarketNode on Market {
|
||||
id
|
||||
liquidityProvisionsConnection {
|
||||
liquidityProvisionsConnection(live: true) {
|
||||
edges {
|
||||
node {
|
||||
commitmentAmount
|
||||
|
||||
@@ -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,
|
||||
@@ -96,8 +94,8 @@ export const getId = (
|
||||
>
|
||||
) =>
|
||||
isLpFragment(entry)
|
||||
? `${entry.party.id}${entry.status}${entry.createdAt}`
|
||||
: `${entry.partyID}${entry.status}${entry.createdAt}`;
|
||||
? `${entry.party.id}${entry.status}${entry.createdAt}${entry.updatedAt}`
|
||||
: `${entry.partyID}${entry.status}${entry.createdAt}${entry.updatedAt}`;
|
||||
|
||||
export const marketLiquidityDataProvider = makeDataProvider<
|
||||
MarketLpQuery,
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
fragment DataSource on DataSourceDefinition {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query MarketInfo($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
id
|
||||
@@ -79,9 +110,15 @@ query MarketInfo($marketId: ID!) {
|
||||
}
|
||||
dataSourceSpecForSettlementData {
|
||||
id
|
||||
data {
|
||||
...DataSource
|
||||
}
|
||||
}
|
||||
dataSourceSpecForTradingTermination {
|
||||
id
|
||||
data {
|
||||
...DataSource
|
||||
}
|
||||
}
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
|
||||
@@ -3,14 +3,47 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type DataSourceFragment = { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null } | null> } } };
|
||||
|
||||
export type MarketInfoQueryVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type MarketInfoQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, lpPriceRange: string, proposal?: { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any }, openingAuction: { __typename?: 'AuctionDuration', durationSecs: number, volume: number }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', id: string } } } | null> | null } | null, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, priceMonitoringSettings: { __typename?: 'PriceMonitoringSettings', parameters?: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null } | null }, riskFactors?: { __typename?: 'RiskFactor', market: string, short: string, long: string } | null, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', triggeringRatio: string, targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } }, riskModel: { __typename?: 'LogNormalRiskModel', tau: number, riskAversionParameter: number, params: { __typename?: 'LogNormalModelParams', r: number, sigma: number, mu: number } } | { __typename?: 'SimpleRiskModel', params: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } }, marginCalculator?: { __typename?: 'MarginCalculator', scalingFactors: { __typename?: 'ScalingFactors', searchLevel: number, initialMargin: number, collateralRelease: number } } | null } } | null };
|
||||
|
||||
export type MarketInfoQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, lpPriceRange: string, proposal?: { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any }, openingAuction: { __typename?: 'AuctionDuration', durationSecs: number, volume: number }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', id: string } } } | null> | null } | null, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, priceMonitoringSettings: { __typename?: 'PriceMonitoringSettings', parameters?: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null } | null }, riskFactors?: { __typename?: 'RiskFactor', market: string, short: string, long: string } | null, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', triggeringRatio: string, targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null } | null> } } } }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', operator: Types.ConditionOperator, value?: string | null } | null> } } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } }, riskModel: { __typename?: 'LogNormalRiskModel', tau: number, riskAversionParameter: number, params: { __typename?: 'LogNormalModelParams', r: number, sigma: number, mu: number } } | { __typename?: 'SimpleRiskModel', params: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } }, marginCalculator?: { __typename?: 'MarginCalculator', scalingFactors: { __typename?: 'ScalingFactors', searchLevel: number, initialMargin: number, collateralRelease: number } } | null } } | null };
|
||||
|
||||
export const DataSourceFragmentDoc = gql`
|
||||
fragment DataSource on DataSourceDefinition {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const MarketInfoDocument = gql`
|
||||
query MarketInfo($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
@@ -93,9 +126,15 @@ export const MarketInfoDocument = gql`
|
||||
}
|
||||
dataSourceSpecForSettlementData {
|
||||
id
|
||||
data {
|
||||
...DataSource
|
||||
}
|
||||
}
|
||||
dataSourceSpecForTradingTermination {
|
||||
id
|
||||
data {
|
||||
...DataSource
|
||||
}
|
||||
}
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
@@ -131,7 +170,7 @@ export const MarketInfoDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
${DataSourceFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useMarketInfoQuery__
|
||||
|
||||
@@ -39,12 +39,12 @@ import {
|
||||
|
||||
export interface InfoProps {
|
||||
market: MarketInfoWithDataAndCandles;
|
||||
onSelect: (id: string) => void;
|
||||
onSelect?: (id: string, metaKey?: boolean) => void;
|
||||
}
|
||||
|
||||
export interface MarketInfoContainerProps {
|
||||
marketId: string;
|
||||
onSelect?: (id: string) => void;
|
||||
onSelect?: (id: string, metaKey?: boolean) => void;
|
||||
}
|
||||
export const MarketInfoContainer = ({
|
||||
marketId,
|
||||
@@ -73,7 +73,7 @@ export const MarketInfoContainer = ({
|
||||
<AsyncRenderer data={data} loading={loading} error={error} reload={reload}>
|
||||
{data ? (
|
||||
<TinyScroll className="h-full overflow-auto">
|
||||
<Info market={data} onSelect={(id) => onSelect?.(id)} />
|
||||
<Info market={data} onSelect={onSelect} />
|
||||
</TinyScroll>
|
||||
) : (
|
||||
<Splash>
|
||||
@@ -85,7 +85,7 @@ export const MarketInfoContainer = ({
|
||||
};
|
||||
|
||||
export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
const { VEGA_TOKEN_URL, VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { VEGA_TOKEN_URL } = useEnvironment();
|
||||
const headerClassName = 'uppercase text-lg';
|
||||
|
||||
if (!market) return null;
|
||||
@@ -124,6 +124,10 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
title: t('Instrument'),
|
||||
content: <InstrumentInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Oracle'),
|
||||
content: <OracleInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Settlement asset'),
|
||||
content: <SettlementAssetInfoPanel market={market} />,
|
||||
@@ -165,7 +169,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
<LiquidityInfoPanel market={market}>
|
||||
<Link
|
||||
to={`/liquidity/${market.id}`}
|
||||
onClick={() => onSelect(market.id)}
|
||||
onClick={(ev) => onSelect?.(market.id, ev.metaKey)}
|
||||
data-testid="view-liquidity-link"
|
||||
>
|
||||
<UILink>{t('View liquidity provision table')}</UILink>
|
||||
@@ -177,23 +181,6 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
title: t('Liquidity price range'),
|
||||
content: <LiquidityPriceRangeInfoPanel market={market} />,
|
||||
},
|
||||
{
|
||||
title: t('Oracle'),
|
||||
content: (
|
||||
<OracleInfoPanel market={market}>
|
||||
<ExternalLink
|
||||
href={`${VEGA_EXPLORER_URL}/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData.id}`}
|
||||
>
|
||||
{t('View settlement data oracle specification')}
|
||||
</ExternalLink>
|
||||
<ExternalLink
|
||||
href={`${VEGA_EXPLORER_URL}/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForTradingTermination.id}`}
|
||||
>
|
||||
{t('View termination oracle specification')}
|
||||
</ExternalLink>
|
||||
</OracleInfoPanel>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const marketGovPanels = [
|
||||
@@ -236,17 +223,17 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="mb-8">
|
||||
<p className={headerClassName}>{t('Market data')}</p>
|
||||
<h3 className={headerClassName}>{t('Market data')}</h3>
|
||||
<Accordion panels={marketDataPanels} />
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<MarketProposalNotification marketId={market.id} />
|
||||
<p className={headerClassName}>{t('Market specification')}</p>
|
||||
<h3 className={headerClassName}>{t('Market specification')}</h3>
|
||||
<Accordion panels={marketSpecPanels} />
|
||||
</div>
|
||||
{VEGA_TOKEN_URL && market.proposal?.id && (
|
||||
<div>
|
||||
<p className={headerClassName}>{t('Market governance')}</p>
|
||||
<h3 className={headerClassName}>{t('Market governance')}</h3>
|
||||
<Accordion panels={marketGovPanels} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import {
|
||||
ConditionOperator,
|
||||
ConditionOperatorMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { DataSourceProof } from './market-info-panels';
|
||||
|
||||
describe('DataSourceProof', () => {
|
||||
const ORACLE_PUBKEY =
|
||||
'69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f';
|
||||
it('renders correct proof for external data sources', () => {
|
||||
const props = {
|
||||
data: {
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionExternal' as const,
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfiguration' as const,
|
||||
signers: [
|
||||
{
|
||||
__typename: 'Signer' as const,
|
||||
signer: {
|
||||
__typename: 'PubKey' as const,
|
||||
key: ORACLE_PUBKEY,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
providers: [
|
||||
{
|
||||
name: 'Another oracle',
|
||||
url: 'https://zombo.com',
|
||||
description_markdown:
|
||||
'Some markdown describing the oracle provider.\n\nTwitter: @FacesPics2\n',
|
||||
oracle: {
|
||||
status: 'GOOD' as const,
|
||||
status_reason: '',
|
||||
first_verified: '2022-01-01T00:00:00.000Z',
|
||||
last_verified: '2022-12-31T00:00:00.000Z',
|
||||
type: 'public_key' as const,
|
||||
public_key: ORACLE_PUBKEY,
|
||||
},
|
||||
proofs: [
|
||||
{
|
||||
format: 'signed_message' as const,
|
||||
available: true,
|
||||
type: 'public_key' as const,
|
||||
public_key: ORACLE_PUBKEY,
|
||||
message: 'SOMEHEX',
|
||||
},
|
||||
],
|
||||
github_link: `https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/PubKey-${ORACLE_PUBKEY}.toml`,
|
||||
},
|
||||
],
|
||||
type: 'termination' as const,
|
||||
};
|
||||
render(<DataSourceProof {...props} />);
|
||||
expect(screen.getByRole('link')).toHaveAttribute(
|
||||
'href',
|
||||
props.providers[0].github_link
|
||||
);
|
||||
});
|
||||
|
||||
it('renders message if there are no providers', () => {
|
||||
const props = {
|
||||
data: {
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionExternal' as const,
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfiguration' as const,
|
||||
signers: [
|
||||
{
|
||||
__typename: 'Signer' as const,
|
||||
signer: {
|
||||
__typename: 'PubKey' as const,
|
||||
key: ORACLE_PUBKEY,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
providers: [],
|
||||
type: 'termination' as const,
|
||||
};
|
||||
render(<DataSourceProof {...props} />);
|
||||
expect(
|
||||
screen.getByText('No oracle proof for termination')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders message if there are no matching proofs', () => {
|
||||
const props = {
|
||||
data: {
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionExternal' as const,
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfiguration' as const,
|
||||
signers: [
|
||||
{
|
||||
__typename: 'Signer' as const,
|
||||
signer: {
|
||||
__typename: 'PubKey' as const,
|
||||
key: ORACLE_PUBKEY,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
providers: [
|
||||
{
|
||||
name: 'Another oracle',
|
||||
url: 'https://zombo.com',
|
||||
description_markdown:
|
||||
'Some markdown describing the oracle provider.\n\nTwitter: @FacesPics2\n',
|
||||
oracle: {
|
||||
status: 'GOOD' as const,
|
||||
status_reason: '',
|
||||
first_verified: '2022-01-01T00:00:00.000Z',
|
||||
last_verified: '2022-12-31T00:00:00.000Z',
|
||||
type: 'public_key' as const,
|
||||
public_key: 'not-the-pubkey',
|
||||
},
|
||||
proofs: [
|
||||
{
|
||||
format: 'signed_message' as const,
|
||||
available: true,
|
||||
type: 'public_key' as const,
|
||||
public_key: 'not-the-pubkey',
|
||||
message: 'SOMEHEX',
|
||||
},
|
||||
],
|
||||
github_link: `https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/PubKey-${ORACLE_PUBKEY}.toml`,
|
||||
},
|
||||
],
|
||||
type: 'settlementData' as const,
|
||||
};
|
||||
render(<DataSourceProof {...props} />);
|
||||
expect(
|
||||
screen.getByText('No oracle proof for settlement data')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders message if no data source on market', () => {
|
||||
const props = {
|
||||
data: {
|
||||
sourceType: {
|
||||
__typename: 'Invalid',
|
||||
},
|
||||
},
|
||||
providers: [],
|
||||
type: 'termination' as const,
|
||||
};
|
||||
// @ts-ignore types are invalid
|
||||
render(<DataSourceProof {...props} />);
|
||||
expect(screen.getByText('Invalid data source')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders conditions for internal data sources', () => {
|
||||
const condition = {
|
||||
__typename: 'Condition' as const,
|
||||
operator: ConditionOperator.OPERATOR_GREATER_THAN,
|
||||
value: '100',
|
||||
};
|
||||
const props = {
|
||||
data: {
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionInternal' as const,
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfigurationTime' as const,
|
||||
conditions: [condition],
|
||||
},
|
||||
},
|
||||
},
|
||||
providers: [],
|
||||
type: 'termination' as const,
|
||||
};
|
||||
render(<DataSourceProof {...props} />);
|
||||
expect(screen.getByText('Internal conditions')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
`${ConditionOperatorMapping[condition.operator]} ${condition.value}`
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
calcCandleVolume,
|
||||
totalFeesPercentage,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
@@ -21,7 +21,12 @@ import type {
|
||||
MarketInfoWithDataAndCandles,
|
||||
} from './market-info-data-provider';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { DataSourceDefinition, SignerKind } from '@vegaprotocol/types';
|
||||
import { ConditionOperatorMapping } from '@vegaprotocol/types';
|
||||
import { MarketTradingModeMapping } from '@vegaprotocol/types';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { Provider } from '@vegaprotocol/oracles';
|
||||
import { useOracleProofs } from '@vegaprotocol/oracles';
|
||||
|
||||
type PanelProps = Pick<
|
||||
ComponentProps<typeof MarketInfoTable>,
|
||||
@@ -399,7 +404,7 @@ export const LiquidityPriceRangeInfoPanel = ({
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -408,9 +413,156 @@ export const LiquidityPriceRangeInfoPanel = ({
|
||||
export const OracleInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={market.tradableInstrument.instrument.product.dataSourceSpecBinding}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}: MarketInfoProps & PanelProps) => {
|
||||
const product = market.tradableInstrument.instrument.product;
|
||||
const { VEGA_EXPLORER_URL, ORACLE_PROOFS_URL } = useEnvironment();
|
||||
const { data } = useOracleProofs(ORACLE_PROOFS_URL);
|
||||
return (
|
||||
<MarketInfoTable data={product.dataSourceSpecBinding} {...props}>
|
||||
<div
|
||||
className="flex flex-col gap-2 mt-4"
|
||||
data-testid="oracle-proof-links"
|
||||
>
|
||||
<DataSourceProof
|
||||
data={product.dataSourceSpecForSettlementData.data}
|
||||
providers={data}
|
||||
type="settlementData"
|
||||
/>
|
||||
<DataSourceProof
|
||||
data={product.dataSourceSpecForTradingTermination.data}
|
||||
providers={data}
|
||||
type="termination"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2" data-testid="oracle-spec-links">
|
||||
<ExternalLink
|
||||
href={`${VEGA_EXPLORER_URL}/oracles#${product.dataSourceSpecForSettlementData.id}`}
|
||||
>
|
||||
{t('View settlement data specification')}
|
||||
</ExternalLink>
|
||||
<ExternalLink
|
||||
href={`${VEGA_EXPLORER_URL}/oracles#${product.dataSourceSpecForTradingTermination.id}`}
|
||||
>
|
||||
{t('View termination specification')}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</MarketInfoTable>
|
||||
);
|
||||
};
|
||||
|
||||
export const DataSourceProof = ({
|
||||
data,
|
||||
providers,
|
||||
type,
|
||||
}: {
|
||||
data: DataSourceDefinition;
|
||||
providers: Provider[] | undefined;
|
||||
type: 'settlementData' | 'termination';
|
||||
}) => {
|
||||
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
|
||||
const signers = data.sourceType.sourceType.signers || [];
|
||||
|
||||
if (!providers?.length) {
|
||||
return <NoOracleProof type={type} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{signers.map(({ signer }, i) => {
|
||||
return (
|
||||
<OracleLink
|
||||
key={i}
|
||||
providers={providers}
|
||||
signer={signer}
|
||||
type={type}
|
||||
index={i}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.sourceType.__typename === 'DataSourceDefinitionInternal') {
|
||||
return (
|
||||
<div>
|
||||
<h3>{t('Internal conditions')}</h3>
|
||||
{data.sourceType.sourceType.conditions.map((condition, i) => {
|
||||
if (!condition) return null;
|
||||
return (
|
||||
<p key={i}>
|
||||
{ConditionOperatorMapping[condition.operator]} {condition.value}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div>{t('Invalid data source')}</div>;
|
||||
};
|
||||
|
||||
const OracleLink = ({
|
||||
providers,
|
||||
signer,
|
||||
type,
|
||||
index,
|
||||
}: {
|
||||
providers: Provider[];
|
||||
signer: SignerKind;
|
||||
type: 'settlementData' | 'termination';
|
||||
index: number;
|
||||
}) => {
|
||||
const text =
|
||||
type === 'settlementData'
|
||||
? t('View settlement oracle details')
|
||||
: t('View termination oracle details');
|
||||
const textWithCount = index > 0 ? `${text} (${index + 1})` : text;
|
||||
|
||||
const provider = providers.find((p) => {
|
||||
if (signer.__typename === 'PubKey') {
|
||||
if (
|
||||
p.oracle.type === 'public_key' &&
|
||||
p.oracle.public_key === signer.key
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (signer.__typename === 'ETHAddress') {
|
||||
if (
|
||||
p.oracle.type === 'eth_address' &&
|
||||
p.oracle.eth_address === signer.address
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!provider) {
|
||||
return <NoOracleProof type={type} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
<ExternalLink href={provider.github_link}>{textWithCount}</ExternalLink>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const NoOracleProof = ({
|
||||
type,
|
||||
}: {
|
||||
type: 'settlementData' | 'termination';
|
||||
}) => {
|
||||
return (
|
||||
<p>
|
||||
{t(
|
||||
'No oracle proof for %s',
|
||||
type === 'settlementData' ? 'settlement data' : 'termination'
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -133,10 +133,44 @@ export const marketInfoQuery = (
|
||||
dataSourceSpecForSettlementData: {
|
||||
__typename: 'DataSourceSpec',
|
||||
id: 'f028fe5ea7de3890962a05a7163fdde562629af649ed81b8c8902fafb6eef04f',
|
||||
data: {
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionExternal',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfiguration',
|
||||
signers: [
|
||||
{
|
||||
__typename: 'Signer',
|
||||
signer: {
|
||||
__typename: 'PubKey',
|
||||
key: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
__typename: 'DataSourceSpec',
|
||||
id: 'f028fe5ea7de3890962a05a7163fdde562629af649ed81b8c8902fafb6eef04f',
|
||||
data: {
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionExternal',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfiguration',
|
||||
signers: [
|
||||
{
|
||||
__typename: 'Signer',
|
||||
signer: {
|
||||
__typename: 'PubKey',
|
||||
key: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataSourceSpecBinding: {
|
||||
__typename: 'DataSourceSpecToFutureBinding',
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
import {
|
||||
AgGridDynamic as AgGrid,
|
||||
PriceFlashCell,
|
||||
MarketNameCell,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
@@ -24,8 +25,10 @@ export const getRowId = ({ data }: { data: { id: string } }) => data.id;
|
||||
|
||||
export const MarketListTable = forwardRef<
|
||||
AgGridReact,
|
||||
TypedDataAgGrid<MarketMaybeWithData>
|
||||
>((props, ref) => {
|
||||
TypedDataAgGrid<MarketMaybeWithData> & {
|
||||
onMarketClick: (marketId: string, metaKey?: boolean) => void;
|
||||
}
|
||||
>(({ onMarketClick, ...props }, ref) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
return (
|
||||
<AgGrid
|
||||
@@ -41,22 +44,14 @@ export const MarketListTable = forwardRef<
|
||||
filterParams: { buttons: ['reset'] },
|
||||
}}
|
||||
suppressCellFocus={true}
|
||||
components={{ PriceFlashCell }}
|
||||
components={{ PriceFlashCell, MarketNameCell }}
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn
|
||||
headerName={t('Market')}
|
||||
field="tradableInstrument.instrument.code"
|
||||
cellRenderer={({
|
||||
value,
|
||||
data,
|
||||
}: VegaICellRendererParams<
|
||||
MarketMaybeWithData,
|
||||
'tradableInstrument.instrument.code'
|
||||
>) => {
|
||||
if (!data) return null;
|
||||
return <span data-testid={`market-${data.id}`}>{value}</span>;
|
||||
}}
|
||||
cellRenderer="MarketNameCell"
|
||||
cellRendererParams={{ onMarketClick }}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Description')}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { MouseEvent } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { MarketListTable } from './market-list-table';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import type { RowClickedEvent } from 'ag-grid-community';
|
||||
import type { CellClickedEvent } from 'ag-grid-community';
|
||||
import { marketsWithDataProvider as dataProvider } from '../../markets-provider';
|
||||
import type { MarketMaybeWithData } from '../../markets-provider';
|
||||
|
||||
interface MarketsContainerProps {
|
||||
onSelect: (marketId: string) => void;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
}
|
||||
|
||||
export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
|
||||
@@ -21,16 +23,23 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
|
||||
rowData={error ? [] : data}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
onRowClicked={(rowEvent: RowClickedEvent) => {
|
||||
const { data, event } = rowEvent;
|
||||
// filters out clicks on the symbol column because it should display asset details
|
||||
onCellClicked={(cellEvent: CellClickedEvent) => {
|
||||
const { data, column, event } = cellEvent;
|
||||
const colId = column.getColId();
|
||||
if (
|
||||
(event?.target as HTMLElement).tagName.toUpperCase() === 'BUTTON'
|
||||
[
|
||||
'tradableInstrument.instrument.code',
|
||||
'tradableInstrument.instrument.product.settlementAsset',
|
||||
].includes(colId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
onSelect((data as MarketMaybeWithData).id);
|
||||
onSelect(
|
||||
(data as MarketMaybeWithData).id,
|
||||
(event as unknown as MouseEvent)?.metaKey
|
||||
);
|
||||
}}
|
||||
onMarketClick={onSelect}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nrwl/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# oracles
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test oracles` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'oracles',
|
||||
preset: '../../jest.preset.js',
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/oracles',
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "@vegaprotocol/oracles",
|
||||
"version": "0.0.1"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/oracles/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/web:rollup",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/oracles",
|
||||
"tsConfig": "libs/oracles/tsconfig.lib.json",
|
||||
"project": "libs/oracles/package.json",
|
||||
"entryFile": "libs/oracles/src/index.ts",
|
||||
"external": ["react/jsx-runtime"],
|
||||
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
|
||||
"compiler": "babel",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "libs/oracles/README.md",
|
||||
"input": ".",
|
||||
"output": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nrwl/linter:eslint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/oracles/**/*.{ts,tsx,js,jsx}"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nrwl/jest:jest",
|
||||
"outputs": ["coverage/libs/oracles"],
|
||||
"options": {
|
||||
"jestConfig": "libs/oracles/jest.config.ts",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './lib/oracle-schema';
|
||||
export * from './lib/use-oracle-proofs';
|
||||
@@ -0,0 +1,74 @@
|
||||
import z from 'zod';
|
||||
|
||||
export type Provider = z.infer<typeof providerSchema>;
|
||||
export type Oracle = z.infer<typeof oracleSchema>;
|
||||
export type Proof = z.infer<typeof proofSchema>;
|
||||
export type Status = z.infer<typeof statusSchema>;
|
||||
|
||||
const statusSchema = z.enum([
|
||||
'UNKNOWN',
|
||||
'GOOD',
|
||||
'SUSPICIOUS',
|
||||
'MALICIOUS',
|
||||
'RETIRED',
|
||||
'COMPROMISED',
|
||||
]);
|
||||
|
||||
const baseProofSchema = z.object({
|
||||
format: z.enum(['url', 'signed_message']),
|
||||
available: z.boolean(),
|
||||
});
|
||||
|
||||
const proofSchema = z.discriminatedUnion('type', [
|
||||
baseProofSchema.extend({
|
||||
type: z.literal('public_key'),
|
||||
public_key: z.string().min(64),
|
||||
message: z.string().min(1),
|
||||
}),
|
||||
baseProofSchema.extend({
|
||||
type: z.literal('eth_address'),
|
||||
eth_address: z.string().min(42),
|
||||
message: z.string().min(1),
|
||||
}),
|
||||
baseProofSchema.extend({
|
||||
type: z.literal('web'),
|
||||
url: z.string().url(),
|
||||
}),
|
||||
baseProofSchema.extend({
|
||||
type: z.literal('github'),
|
||||
url: z.string().url(),
|
||||
}),
|
||||
baseProofSchema.extend({
|
||||
type: z.literal('twitter'),
|
||||
url: z.string().url(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const baseOracleSchema = z.object({
|
||||
status: statusSchema,
|
||||
status_reason: z.string(),
|
||||
first_verified: z.string(),
|
||||
last_verified: z.string(),
|
||||
});
|
||||
|
||||
const oracleSchema = z.discriminatedUnion('type', [
|
||||
baseOracleSchema.extend({
|
||||
type: z.literal('public_key'),
|
||||
public_key: z.string().min(64),
|
||||
}),
|
||||
baseOracleSchema.extend({
|
||||
type: z.literal('eth_address'),
|
||||
eth_address: z.string().min(42),
|
||||
}),
|
||||
]);
|
||||
|
||||
const providerSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
url: z.string().url(),
|
||||
description_markdown: z.string(),
|
||||
oracle: oracleSchema,
|
||||
proofs: z.array(proofSchema),
|
||||
github_link: z.string().url(),
|
||||
});
|
||||
|
||||
export const providersSchema = z.array(providerSchema);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user