Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c93793a93 | ||
|
|
e265a14e69 | ||
|
|
3189fc1b37 | ||
|
|
9715b681ea | ||
|
|
c7c42045a5 | ||
|
|
fbecf0bffd | ||
|
|
0413ef13f3 | ||
|
|
d28d238574 | ||
|
|
937f4beeb0 |
+3
-3
@@ -1,5 +1,5 @@
|
||||
apps/**/dist/*
|
||||
apps/**/node_modules/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
tmp/*
|
||||
.dockerignore
|
||||
dockerfiles
|
||||
Dockerfile
|
||||
|
||||
+3
-3
@@ -51,7 +51,8 @@
|
||||
"ul": ["list"],
|
||||
"ol": ["list"]
|
||||
}
|
||||
]
|
||||
],
|
||||
"curly": "warn"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -74,8 +75,7 @@
|
||||
"prefer": "type-imports",
|
||||
"disallowTypeAnnotations": true
|
||||
}
|
||||
],
|
||||
"curly": ["error", "multi-line"]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
name: "\U0001F41B Bug"
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: "\U0001F41B bug"
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## Steps to Reproduce
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
## Expected behavior
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## Screenshots
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
## Device details
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context about the problem here.
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: Chore
|
||||
about:
|
||||
A template to capture non feature / enhancement work we need to do like work
|
||||
to support other functions or housekeeping etc.
|
||||
title: ''
|
||||
labels: chore
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## The Chore
|
||||
|
||||
What we need to achieve and who for
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] What do we need to do first
|
||||
- [ ] and then what?
|
||||
- [ ] Etc.
|
||||
|
||||
## Additional details / background info
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
name: User Story
|
||||
about:
|
||||
A template to describe a features or enhancements we want to make from a users
|
||||
perspective to ensure we understand what is needed and the value it would add
|
||||
title: ''
|
||||
labels: feature
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Story
|
||||
|
||||
As a
|
||||
I want
|
||||
So that
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] I can
|
||||
- [ ] I can
|
||||
- [ ] I can
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Explore and sketch
|
||||
- [ ] Team and stakeholder review
|
||||
- [ ] Visual Design
|
||||
- [ ] Team review
|
||||
- [ ] Etc.
|
||||
|
||||
## Sketch
|
||||
|
||||
## Additional details / background info
|
||||
@@ -1,10 +0,0 @@
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Start nomad
|
||||
shell: bash
|
||||
run: vegacapsule nomad &
|
||||
|
||||
- name: Bootstrap network
|
||||
shell: bash
|
||||
run: vegacapsule network bootstrap --config-path=./frontend-monorepo/vegacapsule/config.hcl --force
|
||||
@@ -1,38 +0,0 @@
|
||||
inputs:
|
||||
passphrase:
|
||||
description: 'Wallet password'
|
||||
outputs:
|
||||
token:
|
||||
description: 'api-token of wallet'
|
||||
value: ${{ steps.generate-api-token.outputs.api-token }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Initialize wallet
|
||||
shell: bash
|
||||
run: vega wallet init -f --home ~/.vegacapsule/testnet/wallet
|
||||
|
||||
- name: Import wallet
|
||||
shell: bash
|
||||
run: vega wallet import -w capsule_wallet --recovery-phrase-file ./frontend-monorepo/vegacapsule/recovery -p ./frontend-monorepo/vegacapsule/passphrase --home ~/.vegacapsule/testnet/wallet
|
||||
|
||||
- name: Generate second public key
|
||||
shell: bash
|
||||
run: vega wallet key generate -w capsule_wallet -p ./frontend-monorepo/vegacapsule/passphrase --home ~/.vegacapsule/testnet/wallet
|
||||
|
||||
- name: Import network
|
||||
shell: bash
|
||||
run: vega wallet network import --force --from-file ./frontend-monorepo/vegacapsule/wallet-config.toml --home ~/.vegacapsule/testnet/wallet
|
||||
|
||||
- name: Init api-token
|
||||
shell: bash
|
||||
run: vega wallet api-token init --home ~/.vegacapsule/testnet/wallet --passphrase-file ./frontend-monorepo/vegacapsule/passphrase
|
||||
|
||||
- name: Generate api-token
|
||||
id: generate-api-token
|
||||
shell: bash
|
||||
run: echo api-token=$(vega wallet api-token generate --wallet-name capsule_wallet --tokens-passphrase-file ./frontend-monorepo/vegacapsule/passphrase --wallet-passphrase-file ./frontend-monorepo/vegacapsule/passphrase --home ~/.vegacapsule/testnet/wallet | grep -Eo '[a-zA-Z0-9]{64}') >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Start service using capsule network
|
||||
shell: bash
|
||||
run: vega wallet service run -n DV --load-tokens --tokens-passphrase-file ./frontend-monorepo/vegacapsule/passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet &
|
||||
@@ -1,48 +0,0 @@
|
||||
inputs:
|
||||
recovery:
|
||||
description: 'Recovery phrase'
|
||||
passphrase:
|
||||
description: 'Wallet password'
|
||||
outputs:
|
||||
token:
|
||||
description: 'api-token of wallet'
|
||||
value: ${{ steps.generate-api-token.outputs.api-token }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Create passphrase
|
||||
shell: bash
|
||||
run: echo "${{ inputs.passphrase }}" > ./passphrase
|
||||
|
||||
- name: Create recovery
|
||||
shell: bash
|
||||
run: echo "${{ inputs.recovery }}" > ./recovery
|
||||
|
||||
- name: Initialize wallet
|
||||
shell: bash
|
||||
run: vega wallet init -f --home ~/.vegacapsule/testnet/wallet
|
||||
|
||||
- name: Import wallet
|
||||
shell: bash
|
||||
run: vega wallet import -w UI_Trading_Test --recovery-phrase-file ./recovery -p ./passphrase --home ~/.vegacapsule/testnet/wallet
|
||||
|
||||
- name: Create public key 2
|
||||
shell: bash
|
||||
run: vega wallet key generate -w UI_Trading_Test -p ./passphrase --home ~/.vegacapsule/testnet/wallet
|
||||
|
||||
- name: Import network
|
||||
shell: bash
|
||||
run: vega wallet network import --from-url="https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml" --force --home ~/.vegacapsule/testnet/wallet
|
||||
|
||||
- name: Init api-token
|
||||
shell: bash
|
||||
run: vega wallet api-token init --home ~/.vegacapsule/testnet/wallet --passphrase-file passphrase
|
||||
|
||||
- name: Generate api-token
|
||||
id: generate-api-token
|
||||
shell: bash
|
||||
run: echo api-token=$(vega wallet api-token generate --wallet-name UI_Trading_Test --tokens-passphrase-file passphrase --wallet-passphrase-file passphrase --home ~/.vegacapsule/testnet/wallet | grep -Eo '[a-zA-Z0-9]{64}') >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Start service using stagnet3 network
|
||||
shell: bash
|
||||
run: vega wallet service run -n stagnet3 --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet &
|
||||
@@ -1,27 +1,25 @@
|
||||
---
|
||||
name: 'Add Issues To Project Board'
|
||||
name: Auto Assign Issue to New Project
|
||||
|
||||
'on':
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_NEW_CARD_TO_PROJECT }}
|
||||
PROJECT_ID: ${{ secrets.FRONT_END_PROJECT_ID }}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
USER: ${{ github.actor }}
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
add_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Add issue to project board'
|
||||
- name: Add Issue to New Front End Project Board
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_NEW_CARD_TO_PROJECT }}
|
||||
PROJECT_ID: ${{ secrets.FRONT_END_PROJECT_ID }}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
mutation($user:String!, $project:ID!, $issue:ID!) {
|
||||
addProjectV2ItemById(input: {clientMutationId: $user, projectId: $project, contentId: $issue}) {
|
||||
item {
|
||||
mutation($project:ID!, $issue:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID -f user=$USER
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Cypress tests -- manual trigger
|
||||
name: Capsule tests -- manual trigger
|
||||
|
||||
# This workflow runs the frontend tests against chosen branch
|
||||
|
||||
@@ -10,43 +10,141 @@ on:
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- console-lite-e2e
|
||||
- explorer-e2e
|
||||
- liquidity-provision-dashboard-e2e
|
||||
- simple-trading-app-e2e
|
||||
- stats-e2e
|
||||
- token-e2e
|
||||
- trading-e2e
|
||||
tags:
|
||||
description: 'Test tags to run'
|
||||
includeFlows:
|
||||
description: 'Include long flows (if any)?'
|
||||
required: true
|
||||
type: string
|
||||
default: '@smoke, @regression, @slow'
|
||||
skip-nx-cache:
|
||||
description: 'Add --skip-nx-cache to cypress test'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
manual:
|
||||
name: Run Cypress tests -- manual trigger
|
||||
name: Run capsule tests -- manual trigger
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO111MODULE: 'on'
|
||||
GOPROXY: ${{ secrets.GO_PROXY }}
|
||||
steps:
|
||||
- name: Set tags
|
||||
run: echo TAGS="--env.grepTags '[ ${{ github.event.inputs.tags }} ]'" >> $GITHUB_ENV
|
||||
#######
|
||||
## Setup langs
|
||||
#######
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
id: go
|
||||
with:
|
||||
go-version: 1.18
|
||||
- name: Set up Node 16
|
||||
uses: actions/setup-node@v2
|
||||
id: npm
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Set --skip-nx-cache flag
|
||||
if: ${{ github.event.inputs.skip-nx-cache == 'true' }}
|
||||
run: echo SKIP_NX_CACHE="--skip-nx-cache" >> $GITHUB_ENV
|
||||
#######
|
||||
## Checkout capsule
|
||||
#######
|
||||
|
||||
outputs:
|
||||
skip-cache: ${{env.SKIP_NX_CACHE}}
|
||||
tags: ${{env.TAGS}}
|
||||
# Checkout capsule to build local network
|
||||
- name: Checkout capsule
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: vegaprotocol/vegacapsule
|
||||
ref: v0.2.1
|
||||
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
path: './capsule'
|
||||
|
||||
dispatch:
|
||||
needs: manual
|
||||
uses: ./.github/workflows/tests-dispatcher.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
project: ${{ inputs.project }}
|
||||
skip-cache: ${{needs.manual.outputs.skip-cache}}
|
||||
tags: ${{needs.manual.outputs.tags}}
|
||||
#######
|
||||
## Build binaries
|
||||
#######
|
||||
- name: Build capsule
|
||||
run: go install
|
||||
working-directory: capsule
|
||||
|
||||
- name: Set GOBIN
|
||||
run: echo GOBIN=$(go env GOPATH)/bin >> $GITHUB_ENV
|
||||
|
||||
- name: Install binaries
|
||||
run: vegacapsule install-bins
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
GOBIN: ${{ env.GOBIN }}
|
||||
|
||||
- name: Checkout Vega
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: vegaprotocol/vega
|
||||
ref: tm35.9
|
||||
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
path: './vega'
|
||||
|
||||
- name: Install binary from Vega repo
|
||||
run: go install ./cmd/vega
|
||||
working-directory: vega
|
||||
|
||||
######
|
||||
## Start capsule
|
||||
######
|
||||
- name: Login to docker
|
||||
run: echo -n ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }} | docker login https://ghcr.io -u vega-ci-bot --password-stdin
|
||||
|
||||
- name: Start nomad
|
||||
run: vegacapsule nomad &
|
||||
|
||||
- name: Bootstrap network
|
||||
run: vegacapsule network bootstrap --config-path=./net_confs/config.hcl --force
|
||||
working-directory: capsule
|
||||
|
||||
######
|
||||
## Setup a Vega wallet for our user
|
||||
######
|
||||
- name: Create passphrase
|
||||
run: echo "${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}" > ./passphrase
|
||||
- name: Create recovery
|
||||
run: echo "${{ secrets.TRADING_TEST_VEGA_WALLET_RECOVERY }}" > ./recovery
|
||||
|
||||
- name: Initialize wallet
|
||||
run: vegawallet init -f --home ~/.vegacapsule/testnet/wallet
|
||||
- name: Import wallet
|
||||
run: vegawallet import -w UI_Trading_Test --recovery-phrase-file ./recovery -p ./passphrase --home ~/.vegacapsule/testnet/wallet
|
||||
- name: Create public key 2
|
||||
run: vegawallet key generate -w UI_Trading_Test -p ./passphrase --home ~/.vegacapsule/testnet/wallet
|
||||
- name: Start service
|
||||
run: vegawallet service run --network DV --automatic-consent --home ~/.vegacapsule/testnet/wallet &
|
||||
|
||||
######
|
||||
## Run some tests
|
||||
######
|
||||
|
||||
# Checkout front ends
|
||||
- name: Checkout frontend mono repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install root dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Run Cypress tests
|
||||
run: yarn nx run ${{ github.event.inputs.project }}:e2e --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome
|
||||
env:
|
||||
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPESS_ETH_WALLET_MNEMONIC }}
|
||||
CYPRESS_INCLUDE_FLOWS: ${{ github.event.inputs.includeFlows }}
|
||||
|
||||
######
|
||||
## Upload logs
|
||||
######
|
||||
|
||||
- name: Logs
|
||||
if: ${{ always() }}
|
||||
run: vegacapsule network logs > vega-capsule-logs.txt
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: logs
|
||||
path: ./vega-capsule-logs.txt
|
||||
|
||||
@@ -1,17 +1,135 @@
|
||||
name: Cypress tests -- night run
|
||||
name: Capsule tests -- night run
|
||||
|
||||
# This workflow runs the frontend tests against latest develop of the core to preempt breaking changes
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
nightly:
|
||||
uses: ./.github/workflows/tests-dispatcher.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
project: '[console-lite-e2e, explorer-e2e, liquidity-provision-dashboard-e2e, stats-e2e, token-e2e, trading-e2e]'
|
||||
tags: --env.grepTags '[ @smoke, @regression, @slow ]'
|
||||
night-run: true
|
||||
name: Run capsule tests -- nightly
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO111MODULE: 'on'
|
||||
steps:
|
||||
#######
|
||||
## Setup langs
|
||||
#######
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
id: go
|
||||
with:
|
||||
go-version: 1.18
|
||||
- name: Set up Node 16
|
||||
uses: actions/setup-node@v2
|
||||
id: npm
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
#######
|
||||
## Checkout capsule
|
||||
#######
|
||||
|
||||
# Checkout capsule to build local network
|
||||
- name: Checkout capsule
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: vegaprotocol/vegacapsule
|
||||
ref: v0.2.1
|
||||
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
path: './capsule'
|
||||
|
||||
#######
|
||||
## Build binaries
|
||||
#######
|
||||
- name: Build capsule
|
||||
run: go install
|
||||
working-directory: capsule
|
||||
|
||||
- name: Set GOBIN
|
||||
run: echo GOBIN=$(go env GOPATH)/bin >> $GITHUB_ENV
|
||||
|
||||
- name: Install binaries
|
||||
run: vegacapsule install-bins
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
GOBIN: ${{ env.GOBIN }}
|
||||
|
||||
- name: Checkout Vega
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: vegaprotocol/vega
|
||||
ref: tm35.9
|
||||
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
path: './vega'
|
||||
|
||||
- name: Install binary from Vega repo
|
||||
run: go install ./cmd/vega
|
||||
working-directory: vega
|
||||
|
||||
######
|
||||
## Start capsule
|
||||
######
|
||||
- name: Login to docker
|
||||
run: echo -n ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }} | docker login https://ghcr.io -u vega-ci-bot --password-stdin
|
||||
|
||||
- name: Start nomad
|
||||
run: vegacapsule nomad &
|
||||
|
||||
- name: Bootstrap network
|
||||
run: vegacapsule network bootstrap --config-path=./net_confs/config.hcl --force
|
||||
working-directory: capsule
|
||||
|
||||
######
|
||||
## Setup a Vega wallet for our user
|
||||
######
|
||||
- name: Create passphrase
|
||||
run: echo "${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}" > ./passphrase
|
||||
- name: Create recovery
|
||||
run: echo "${{ secrets.TRADING_TEST_VEGA_WALLET_RECOVERY }}" > ./recovery
|
||||
|
||||
- name: Initialize wallet
|
||||
run: vegawallet init -f --home ~/.vegacapsule/testnet/wallet
|
||||
- name: Import wallet
|
||||
run: vegawallet import -w UI_Trading_Test --recovery-phrase-file ./recovery -p ./passphrase --home ~/.vegacapsule/testnet/wallet
|
||||
- name: Create public key 2
|
||||
run: vegawallet key generate -w UI_Trading_Test -p ./passphrase --home ~/.vegacapsule/testnet/wallet
|
||||
- name: Start service
|
||||
run: vegawallet service run --network DV --automatic-consent --home ~/.vegacapsule/testnet/wallet &
|
||||
|
||||
######
|
||||
## Run some tests
|
||||
######
|
||||
|
||||
# Checkout front ends
|
||||
- name: Checkout frontend mono repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install root dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Run Cypress tests
|
||||
run: yarn nx run-many --target=e2e --all --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome
|
||||
env:
|
||||
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPESS_ETH_WALLET_MNEMONIC }}
|
||||
CYPRESS_NIGHTLY_RUN: true
|
||||
CYPRESS_INCLUDE_FLOWS: true
|
||||
|
||||
######
|
||||
## Upload logs
|
||||
######
|
||||
|
||||
- name: Logs
|
||||
if: ${{ always() }}
|
||||
run: vegacapsule network logs > vega-capsule-logs.txt
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: logs
|
||||
path: ./vega-capsule-logs.txt
|
||||
|
||||
@@ -1,61 +1,142 @@
|
||||
name: Cypress tests - PR
|
||||
name: Capsule tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- main
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
pr:
|
||||
name: Run capsule tests - PR
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
env:
|
||||
GO111MODULE: 'on'
|
||||
steps:
|
||||
#######
|
||||
## Setup langs
|
||||
#######
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
id: go
|
||||
with:
|
||||
go-version: 1.18
|
||||
- name: Set up Node 16
|
||||
uses: actions/setup-node@v2
|
||||
id: npm
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
#######
|
||||
## Checkout capsule
|
||||
#######
|
||||
|
||||
# Checkout capsule to build local network
|
||||
- name: Checkout capsule
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: vegaprotocol/vegacapsule
|
||||
ref: v0.2.1
|
||||
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
path: './capsule'
|
||||
|
||||
#######
|
||||
## Build binaries
|
||||
#######
|
||||
- name: Build capsule
|
||||
run: go install
|
||||
working-directory: capsule
|
||||
|
||||
- name: Set GOBIN
|
||||
run: echo GOBIN=$(go env GOPATH)/bin >> $GITHUB_ENV
|
||||
|
||||
- name: Install binaries
|
||||
run: vegacapsule install-bins
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
GOBIN: ${{ env.GOBIN }}
|
||||
|
||||
- name: Checkout Vega
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: vegaprotocol/vega
|
||||
ref: tm35.9
|
||||
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
|
||||
path: './vega'
|
||||
|
||||
- name: Install binary from Vega repo
|
||||
run: go install ./cmd/vega
|
||||
working-directory: vega
|
||||
|
||||
######
|
||||
## Start capsule
|
||||
######
|
||||
- name: Login to docker
|
||||
run: echo -n ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }} | docker login https://ghcr.io -u vega-ci-bot --password-stdin
|
||||
|
||||
- name: Start nomad
|
||||
run: vegacapsule nomad &
|
||||
|
||||
- name: Bootstrap network
|
||||
run: vegacapsule network bootstrap --config-path=./net_confs/config.hcl --force
|
||||
working-directory: capsule
|
||||
|
||||
######
|
||||
## Setup a Vega wallet for our user
|
||||
######
|
||||
- name: Create passphrase
|
||||
run: echo "${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}" > ./passphrase
|
||||
- name: Create recovery
|
||||
run: echo "${{ secrets.TRADING_TEST_VEGA_WALLET_RECOVERY }}" > ./recovery
|
||||
|
||||
- name: Initialize wallet
|
||||
run: vegawallet init -f --home ~/.vegacapsule/testnet/wallet
|
||||
- name: Import wallet
|
||||
run: vegawallet import -w UI_Trading_Test --recovery-phrase-file ./recovery -p ./passphrase --home ~/.vegacapsule/testnet/wallet
|
||||
- name: Create public key 2
|
||||
run: vegawallet key generate -w UI_Trading_Test -p ./passphrase --home ~/.vegacapsule/testnet/wallet
|
||||
- name: Start service
|
||||
run: vegawallet service run --network DV --automatic-consent --home ~/.vegacapsule/testnet/wallet &
|
||||
|
||||
######
|
||||
## Run some tests
|
||||
######
|
||||
|
||||
# Checkout front ends
|
||||
- name: Checkout frontend mono repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
path: './frontend-monorepo'
|
||||
|
||||
# Restore node_modules from cache if possible
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
frontend-monorepo/node_modules
|
||||
/home/runner/.cache/Cypress
|
||||
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
|
||||
|
||||
# Install frontend dependencies
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
# Check SHAs
|
||||
- name: Derive appropriate SHAs for base and head for `nx affected` commands
|
||||
uses: nrwl/nx-set-shas@v2
|
||||
with:
|
||||
working-directory: frontend-monorepo
|
||||
main-branch-name: ${{ github.base_ref || github.ref_name }}
|
||||
set-environment-variables-for-job: true
|
||||
main-branch-name: master
|
||||
|
||||
# See affected projects
|
||||
- name: See affected apps
|
||||
run: echo AFFECTED=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects) >> $GITHUB_ENV
|
||||
working-directory: frontend-monorepo
|
||||
- name: Install root dependencies
|
||||
run: yarn install
|
||||
|
||||
outputs:
|
||||
projects: ${{ env.AFFECTED }}
|
||||
- name: Run Cypress tests
|
||||
run: npx nx affected:e2e --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome
|
||||
env:
|
||||
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPESS_ETH_WALLET_MNEMONIC }}
|
||||
CYPRESS_INCLUDE_FLOWS: false
|
||||
|
||||
dispatch:
|
||||
needs: pr
|
||||
uses: ./.github/workflows/tests-dispatcher.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
project: ${{ needs.pr.outputs.projects }}
|
||||
tags: "--env.grepTags='[ @smoke, @regression ]'"
|
||||
######
|
||||
## Upload logs
|
||||
######
|
||||
|
||||
- name: Logs
|
||||
if: ${{ always() }}
|
||||
run: vegacapsule network logs > vega-capsule-logs.txt
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: logs
|
||||
path: ./vega-capsule-logs.txt
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
name: Cypress - console-lite
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
trigger:
|
||||
required: true
|
||||
type: string
|
||||
default: 'false'
|
||||
skip-cache:
|
||||
required: false
|
||||
type: string
|
||||
tags:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
console-lite-e2e:
|
||||
if: ${{ inputs.trigger == 'true' }}
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Vega version
|
||||
run: vega version
|
||||
|
||||
# Checkout front ends
|
||||
- name: Checkout frontend mono repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
path: './frontend-monorepo'
|
||||
|
||||
# Restore node_modules from cache if possible
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
frontend-monorepo/node_modules
|
||||
/home/runner/.cache/Cypress
|
||||
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
|
||||
|
||||
# Install frontend dependencies
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
######
|
||||
## Setup a Vega wallet for our user
|
||||
######
|
||||
|
||||
- name: Set up Vegawallet
|
||||
id: setup-vega
|
||||
uses: ./frontend-monorepo/.github/actions/setup-vegawallet
|
||||
with:
|
||||
recovery: ${{ secrets.TRADING_TEST_VEGA_WALLET_RECOVERY }}
|
||||
passphrase: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
|
||||
|
||||
# To make sure that all Cypress binaries are installed properly
|
||||
- name: Install cypress bins
|
||||
run: yarn cypress install
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
- name: Run Cypress tests
|
||||
run: npx nx run console-lite-e2e:e2e ${{ inputs.skip-cache }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome ${{ inputs.tags }}
|
||||
working-directory: frontend-monorepo
|
||||
env:
|
||||
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPRESS_ETH_WALLET_MNEMONIC }}
|
||||
CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }}
|
||||
@@ -1,106 +0,0 @@
|
||||
name: Cypress - explorer
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
trigger:
|
||||
required: true
|
||||
type: string
|
||||
default: 'false'
|
||||
skip-cache:
|
||||
required: false
|
||||
type: string
|
||||
tags:
|
||||
required: false
|
||||
type: string
|
||||
night-run:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
explorer-e2e:
|
||||
if: ${{ inputs.trigger == 'true' }}
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Vega version
|
||||
run: vega version
|
||||
|
||||
- name: Vegacapsule version
|
||||
run: vegacapsule version
|
||||
|
||||
# Checkout front ends
|
||||
- name: Checkout frontend mono repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
path: './frontend-monorepo'
|
||||
|
||||
# Restore node_modules from cache if possible
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
frontend-monorepo/node_modules
|
||||
/home/runner/.cache/Cypress
|
||||
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
|
||||
|
||||
# Install frontend dependencies
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
#######
|
||||
## Build and run Vegacapsule network
|
||||
#######
|
||||
|
||||
- name: Build and run Vegacapsule network
|
||||
uses: ./frontend-monorepo/.github/actions/run-vegacapsule
|
||||
|
||||
######
|
||||
## Setup a Vega wallet for our user
|
||||
######
|
||||
|
||||
- name: Set up Vegawallet for docker
|
||||
id: setup-vega
|
||||
uses: ./frontend-monorepo/.github/actions/setup-vegawallet-docker
|
||||
with:
|
||||
passphrase: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
|
||||
|
||||
######
|
||||
## Run some tests
|
||||
######
|
||||
|
||||
# To make sure that all Cypress binaries are installed properly
|
||||
- name: Install cypress bins
|
||||
run: yarn cypress install
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
- name: Run Cypress tests
|
||||
run: npx nx run explorer-e2e:e2e ${{ inputs.skip-cache }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome ${{ inputs.tags }}
|
||||
working-directory: frontend-monorepo
|
||||
env:
|
||||
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPRESS_ETH_WALLET_MNEMONIC }}
|
||||
CYPRESS_NIGHTLY_RUN: ${{ inputs.night-run }}
|
||||
CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }}
|
||||
|
||||
######
|
||||
## Upload logs
|
||||
######
|
||||
|
||||
# Artifact path is not valid: /ganache-1/capsule-logscolletor.stderr-2022-12-22T10:20:30Z.log. Contains the following character: Colon :
|
||||
- name: Rename files to allow archive
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
while read -r file; do
|
||||
mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
|
||||
done< <(find /home/runner/.vegacapsule/testnet/logs -type f)
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: logs
|
||||
path: /home/runner/.vegacapsule/testnet/logs
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Cypress - liquidity provision dashboard
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
trigger:
|
||||
required: true
|
||||
type: string
|
||||
default: 'false'
|
||||
|
||||
jobs:
|
||||
liquidity-provision-dashboard-e2e:
|
||||
timeout-minutes: 10
|
||||
if: ${{ inputs.trigger == 'true' }}
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
# Checkout front ends
|
||||
- name: Checkout frontend mono repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
path: './frontend-monorepo'
|
||||
|
||||
# Restore node_modules from cache if possible
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
frontend-monorepo/node_modules
|
||||
/home/runner/.cache/Cypress
|
||||
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
|
||||
|
||||
# Install frontend dependencies
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
# To make sure that all Cypress binaries are installed properly
|
||||
- name: Install cypress bins
|
||||
run: yarn cypress install
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
- name: Run Cypress tests
|
||||
run: npx nx run liquidity-provision-dashboard-e2e:e2e --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome
|
||||
working-directory: frontend-monorepo
|
||||
env:
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
@@ -1,31 +0,0 @@
|
||||
name: Cypress Console tests -- live environment
|
||||
|
||||
# This workflow runs using provided url
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
url:
|
||||
description: 'Url'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
cypress-run:
|
||||
name: Run Cypress Trading tests -- live environment
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run Cypress tests
|
||||
uses: cypress-io/github-action@v4
|
||||
with:
|
||||
browser: chrome
|
||||
record: true
|
||||
project: ./apps/trading-e2e
|
||||
config: baseUrl=${{ github.event.inputs.url }}
|
||||
env: grepTags=@live
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Cypress - stats
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
trigger:
|
||||
required: true
|
||||
type: string
|
||||
default: 'false'
|
||||
|
||||
jobs:
|
||||
stats-e2e:
|
||||
runs-on: self-hosted
|
||||
if: ${{ inputs.trigger == 'true' }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
# Checkout front ends
|
||||
- name: Checkout frontend mono repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
path: './frontend-monorepo'
|
||||
|
||||
# Restore node_modules from cache if possible
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
frontend-monorepo/node_modules
|
||||
/home/runner/.cache/Cypress
|
||||
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
|
||||
|
||||
# Install frontend dependencies
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
# To make sure that all Cypress binaries are installed properly
|
||||
- name: Install cypress bins
|
||||
run: yarn cypress install
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
- name: Run Cypress tests
|
||||
run: npx nx run stats-e2e:e2e --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome
|
||||
working-directory: frontend-monorepo
|
||||
env:
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
@@ -1,101 +0,0 @@
|
||||
name: Cypress - token
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
trigger:
|
||||
required: true
|
||||
type: string
|
||||
default: 'false'
|
||||
skip-cache:
|
||||
required: false
|
||||
type: string
|
||||
tags:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
token-e2e:
|
||||
if: ${{ inputs.trigger == 'true' }}
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Vega version
|
||||
run: vega version
|
||||
|
||||
- name: Vegacapsule version
|
||||
run: vegacapsule version
|
||||
|
||||
# Checkout front ends
|
||||
- name: Checkout frontend mono repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
path: './frontend-monorepo'
|
||||
|
||||
# Restore node_modules from cache if possible
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
frontend-monorepo/node_modules
|
||||
/home/runner/.cache/Cypress
|
||||
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
|
||||
|
||||
# Install frontend dependencies
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
#######
|
||||
## Build and run Vegacapsule network
|
||||
#######
|
||||
|
||||
- name: Build and run Vegacapsule network
|
||||
uses: ./frontend-monorepo/.github/actions/run-vegacapsule
|
||||
|
||||
######
|
||||
## Setup a Vega wallet for our user
|
||||
######
|
||||
|
||||
- name: Set up Vegawallet for docker
|
||||
id: setup-vega
|
||||
uses: ./frontend-monorepo/.github/actions/setup-vegawallet-docker
|
||||
with:
|
||||
passphrase: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
|
||||
|
||||
######
|
||||
## Run some tests
|
||||
######
|
||||
|
||||
# To make sure that all Cypress binaries are installed properly
|
||||
- name: Install cypress bins
|
||||
run: yarn cypress install
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
- name: Run Cypress tests
|
||||
run: npx nx run token-e2e:e2e ${{ inputs.skip-cache }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome ${{ inputs.tags }}
|
||||
working-directory: frontend-monorepo
|
||||
env:
|
||||
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPRESS_ETH_WALLET_MNEMONIC }}
|
||||
CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }}
|
||||
|
||||
######
|
||||
## Upload logs
|
||||
######
|
||||
|
||||
# Artifact path is not valid: /ganache-1/capsule-logscolletor.stderr-2022-12-22T10:20:30Z.log. Contains the following character: Colon :
|
||||
- name: Rename files to allow archive
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
while read -r file; do
|
||||
mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
|
||||
done< <(find /home/runner/.vegacapsule/testnet/logs -type f)
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: logs
|
||||
path: /home/runner/.vegacapsule/testnet/logs
|
||||
@@ -1,70 +0,0 @@
|
||||
name: Cypress - trading
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
trigger:
|
||||
required: true
|
||||
type: string
|
||||
default: 'false'
|
||||
skip-cache:
|
||||
required: false
|
||||
type: string
|
||||
tags:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
trading-e2e:
|
||||
if: ${{ inputs.trigger == 'true' }}
|
||||
timeout-minutes: 30
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Vega version
|
||||
run: vega version
|
||||
|
||||
# Checkout front ends
|
||||
- name: Checkout frontend mono repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
path: './frontend-monorepo'
|
||||
|
||||
# Restore node_modules from cache if possible
|
||||
- name: Restore node_modules from cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
frontend-monorepo/node_modules
|
||||
/home/runner/.cache/Cypress
|
||||
key: node_modules_cypress-${{ hashFiles('frontend-monorepo/yarn.lock') }}
|
||||
|
||||
# Install frontend dependencies
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
######
|
||||
## Setup a Vega wallet for our user
|
||||
######
|
||||
|
||||
- name: Set up Vegawallet
|
||||
id: setup-vega
|
||||
uses: ./frontend-monorepo/.github/actions/setup-vegawallet
|
||||
with:
|
||||
recovery: ${{ secrets.TRADING_TEST_VEGA_WALLET_RECOVERY }}
|
||||
passphrase: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
|
||||
|
||||
# To make sure that all Cypress binaries are installed properly
|
||||
- name: Install cypress bins
|
||||
run: yarn cypress install
|
||||
working-directory: frontend-monorepo
|
||||
|
||||
- name: Run Cypress tests
|
||||
run: npx nx run trading-e2e:e2e ${{ inputs.skip-cache }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome ${{ inputs.tags }}
|
||||
working-directory: frontend-monorepo
|
||||
env:
|
||||
CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE: ${{ secrets.CYPRESS_TRADING_TEST_VEGA_WALLET_PASSPHRASE }}
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
CYPRESS_ETH_WALLET_MNEMONIC: ${{ secrets.CYPRESS_ETH_WALLET_MNEMONIC }}
|
||||
CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }}
|
||||
@@ -3,7 +3,7 @@ name: Generate queries
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
|
||||
jobs:
|
||||
master:
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Deploy affected projects to IPFS
|
||||
|
||||
'on':
|
||||
# Triggers the workflow on push to main branch
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch: null
|
||||
|
||||
jobs:
|
||||
master:
|
||||
name: Publish NX Affected
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
- name: Use Node.js 16
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.14.0
|
||||
- name: Install root dependencies
|
||||
run: yarn install
|
||||
- name: Run deploy for affected sites
|
||||
env:
|
||||
FLEEK_API_KEY: ${{ secrets.FLEEK_API_KEY }}
|
||||
run: ./tools/ipfs-deploy.js
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: Verify PR title
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review, reopened, edited, synchronize]
|
||||
jobs:
|
||||
lint_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Use Node.js 16
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.14.0
|
||||
- name: Install commitlint cli and config
|
||||
run: npm install @commitlint/cli @commitlint/config-conventional
|
||||
- name: Create config
|
||||
run: echo "module.exports = {extends:['@commitlint/config-conventional']};" > commitlint.config.js
|
||||
- name: Check PR title
|
||||
run: echo "${{ github.event.pull_request.title }}" | npx commitlint
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Publish NextJs containers
|
||||
|
||||
'on':
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-pre[0-9]+'
|
||||
|
||||
jobs:
|
||||
master:
|
||||
strategy:
|
||||
matrix:
|
||||
build-platform: ['linux/amd64']
|
||||
app: ['trading']
|
||||
name: Build the ${{ inputs.image_name }} image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set output
|
||||
id: vars
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
- name: Check output
|
||||
env:
|
||||
RELEASE_VERSION: ${{ steps.vars.outputs.tag }}
|
||||
run: |
|
||||
echo $RELEASE_VERSION
|
||||
echo ${{ steps.vars.outputs.tag }}
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
id: quemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Collect images data
|
||||
id: tags
|
||||
run: |
|
||||
echo '::set-output name=current::vegaprotocol/${{ matrix.app }}:${{ steps.vars.outputs.tag }}'
|
||||
echo '::set-output name=latest::vegaprotocol/${{ matrix.app }}:latest'
|
||||
|
||||
- name: Echo tags
|
||||
run: |
|
||||
echo ${{ steps.tags.outputs.latest }}
|
||||
echo ${{ steps.tags.outputs.current }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: '.'
|
||||
file: './Dockerfile.next'
|
||||
build-args: 'APP=${{ matrix.app }}'
|
||||
platforms: ${{ matrix.build-platform }}
|
||||
tags: ${{ steps.tags.outputs.latest }},${{ steps.tags.outputs.current }}
|
||||
push: true
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Publish CRA containers
|
||||
|
||||
'on':
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-pre[0-9]+'
|
||||
|
||||
jobs:
|
||||
master:
|
||||
strategy:
|
||||
matrix:
|
||||
build-platform: ['linux/amd64']
|
||||
app: ['explorer', 'token']
|
||||
name: Build the ${{ inputs.image_name }} image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set output
|
||||
id: vars
|
||||
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
|
||||
- name: Check output
|
||||
env:
|
||||
RELEASE_VERSION: ${{ steps.vars.outputs.tag }}
|
||||
run: |
|
||||
echo $RELEASE_VERSION
|
||||
echo ${{ steps.vars.outputs.tag }}
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
id: quemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Collect images data
|
||||
id: tags
|
||||
run: |
|
||||
echo '::set-output name=current::vegaprotocol/${{ matrix.app }}:${{ steps.vars.outputs.tag }}'
|
||||
echo '::set-output name=latest::vegaprotocol/${{ matrix.app }}:latest'
|
||||
|
||||
- name: Echo tags
|
||||
run: |
|
||||
echo ${{ steps.tags.outputs.latest }}
|
||||
echo ${{ steps.tags.outputs.current }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: '.'
|
||||
file: './Dockerfile.cra'
|
||||
build-args: 'APP=${{ matrix.app }}'
|
||||
platforms: ${{ matrix.build-platform }}
|
||||
tags: ${{ steps.tags.outputs.latest }},${{ steps.tags.outputs.current }}
|
||||
push: true
|
||||
@@ -1,126 +0,0 @@
|
||||
name: Publish docker containers
|
||||
|
||||
'on':
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-*'
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
description: 'Publish tag to Docker Hub & GitHub Registry'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
tag:
|
||||
description: 'Git Tag to build and publish'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
apps:
|
||||
description: 'Applications to build and publish'
|
||||
required: false
|
||||
type: choice
|
||||
options:
|
||||
- '["explorer", "token", "trading"]'
|
||||
- '["explorer"]'
|
||||
- '["token"]'
|
||||
- '["trading"]'
|
||||
archs:
|
||||
description: 'Architecture to build and publish'
|
||||
required: false
|
||||
type: choice
|
||||
options:
|
||||
- linux/amd64, linux/arm64
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
|
||||
jobs:
|
||||
master:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
app: ${{ fromJson(inputs.apps || '["explorer", "token", "trading"]') }}
|
||||
name: Build the ${{ matrix.app }} image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Set up QEMU
|
||||
id: quemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.qemu.outputs.platforms }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: ${{ inputs.publish || startsWith(github.ref, 'refs/tags/') }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Determine Docker Image tag
|
||||
id: tags
|
||||
run: |
|
||||
hash=$(git rev-parse HEAD|cut -b1-8)
|
||||
versionTag=${{ inputs.tag || startsWith(github.ref, 'refs/tags/') && github.ref_name || '${hash}' }}
|
||||
echo ::set-output name=version::${versionTag}
|
||||
echo ::set-output name=npmVersion::$(cat dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }} | grep FROM | head -n 1 | awk '{print $2}' | cut -d ':' -f 2 | cut -d '-' -f 1 )
|
||||
|
||||
- name: Print config
|
||||
run: |
|
||||
git rev-parse --verify HEAD
|
||||
git status
|
||||
echo "inputs.tag=${{ inputs.tag }}"
|
||||
echo "inputs.publish=${{ inputs.publish }}"
|
||||
echo "inputs.apps=${{ inputs.apps }}"
|
||||
echo "inputs.archs=${{ inputs.archs }}"
|
||||
echo "steps.tags.outputs.version=${{ steps.tags.outputs.version }}"
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ steps.tags.outputs.npmVersion }}
|
||||
|
||||
- name: Build frontend dists
|
||||
run: |
|
||||
yarn --verbose --pure-lockfile
|
||||
yarn nx ${{ matrix.app =='trading' && 'export' || 'build' }} ${{ matrix.app }} --pure-lockfile
|
||||
|
||||
- name: Build and export to local Docker
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
file: dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }}.dist
|
||||
build-args: APP=${{ matrix.app }}
|
||||
load: true
|
||||
tags: vegaprotocol/${{ matrix.app }}:local
|
||||
|
||||
- name: Sanity check docker image
|
||||
run: |
|
||||
docker run --rm vegaprotocol/${{ matrix.app }}:local cat .env
|
||||
docker run --rm vegaprotocol/${{ matrix.app }}:local ls -lah
|
||||
|
||||
- name: Build and push to DockerHub
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: ${{ inputs.publish || startsWith(github.ref, 'refs/tags/') }}
|
||||
file: dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }}.dist
|
||||
build-args: APP=${{ matrix.app }}
|
||||
platforms: ${{ inputs.archs || 'linux/amd64, linux/arm64' }}
|
||||
tags: |
|
||||
vegaprotocol/${{ matrix.app }}:latest
|
||||
vegaprotocol/${{ matrix.app }}:${{ steps.tags.outputs.version }}
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
+38
-17
@@ -3,37 +3,60 @@ name: Unit tests & build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
pr:
|
||||
name: Test and lint - PR
|
||||
master:
|
||||
name: Test and lint - main
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: 'read'
|
||||
actions: 'read'
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
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 }}
|
||||
main-branch-name: master
|
||||
- name: Use Node.js 16
|
||||
id: Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.14.0
|
||||
- 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
|
||||
run: yarn install
|
||||
- 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
|
||||
pr:
|
||||
name: Test and lint - PR
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
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: master
|
||||
- name: Use Node.js 16
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.14.0
|
||||
- name: Install root dependencies
|
||||
run: yarn install
|
||||
- name: Check formatting
|
||||
run: yarn nx format:check
|
||||
- name: Lint affected
|
||||
@@ -42,5 +65,3 @@ jobs:
|
||||
run: yarn nx affected:test
|
||||
- name: Build affected
|
||||
run: yarn nx affected:build
|
||||
- name: Build affected spec
|
||||
run: yarn nx affected --target=build-spec
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
project:
|
||||
required: true
|
||||
type: string
|
||||
skip-cache:
|
||||
required: false
|
||||
type: string
|
||||
tags:
|
||||
required: false
|
||||
type: string
|
||||
night-run:
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
run-console-lite-e2e:
|
||||
uses: ./.github/workflows/cypress-console-lite-e2e.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
trigger: ${{ contains(inputs.project, 'console-lite-e2e') || contains(inputs.project, 'console-lite') }}
|
||||
skip-cache: ${{ inputs.skip-cache }}
|
||||
tags: ${{ inputs.tags }}
|
||||
|
||||
run-explorer-e2e:
|
||||
uses: ./.github/workflows/cypress-explorer-e2e.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
trigger: ${{ contains(inputs.project, 'explorer-e2e') || contains(inputs.project, 'explorer') }}
|
||||
skip-cache: ${{ inputs.skip-cache }}
|
||||
tags: ${{ inputs.tags }}
|
||||
night-run: ${{ inputs.night-run }}
|
||||
|
||||
run-liquidity-e2e:
|
||||
uses: ./.github/workflows/cypress-liquidity-provision-dashboard-e2e.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
trigger: ${{ contains(inputs.project, 'liquidity-provision-dashboard-e2e') || contains(inputs.project, 'liquidity-provision-dashboard') }}
|
||||
|
||||
run-stats-e2e:
|
||||
uses: ./.github/workflows/cypress-stats-e2e.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
trigger: ${{ contains(inputs.project, 'stats-e2e') || contains(inputs.project, 'stats') }}
|
||||
|
||||
run-token-e2e:
|
||||
uses: ./.github/workflows/cypress-token-e2e.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
trigger: ${{ contains(inputs.project, 'token-e2e') || contains(inputs.project, 'token') }}
|
||||
skip-cache: ${{ inputs.skip-cache }}
|
||||
tags: ${{ inputs.tags }}
|
||||
|
||||
run-trading-e2e:
|
||||
uses: ./.github/workflows/cypress-trading-e2e.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
trigger: ${{ contains(inputs.project, 'trading-e2e') || contains(inputs.project, 'trading') }}
|
||||
skip-cache: ${{ inputs.skip-cache }}
|
||||
tags: ${{ inputs.tags }}
|
||||
@@ -43,6 +43,3 @@ Thumbs.db
|
||||
.local.env
|
||||
.env.local
|
||||
cypress.env.json
|
||||
|
||||
# Next.js
|
||||
.next
|
||||
|
||||
@@ -6,3 +6,7 @@ yarn commitlint --edit "${1}"
|
||||
|
||||
# Lint all staged files
|
||||
yarn lint-staged
|
||||
|
||||
# Build & test affected apps
|
||||
yarn nx affected:build --uncommitted
|
||||
yarn nx affected:test --uncommitted
|
||||
|
||||
+2
-2
@@ -3,9 +3,9 @@
|
||||
/dist
|
||||
/coverage
|
||||
__generated__
|
||||
__generated___
|
||||
|
||||
apps/static/src/assets/devnet-tranches.json
|
||||
apps/static/src/assets/mainnet-tranches.json
|
||||
apps/static/src/assets/stagnet3-tranches.json
|
||||
apps/static/src/assets/stagnet1-tranches.json
|
||||
apps/static/src/assets/stagnet2-tranches.json
|
||||
apps/static/src/assets/testnet-tranches.json
|
||||
|
||||
+1
-19
@@ -1,24 +1,6 @@
|
||||
module.exports = {
|
||||
stories: [],
|
||||
addons: [
|
||||
'@storybook/addon-actions',
|
||||
'@storybook/addon-viewport',
|
||||
{
|
||||
name: '@storybook/addon-docs',
|
||||
options: {
|
||||
configureJSX: true,
|
||||
babelOptions: {},
|
||||
sourceLoaderOptions: null,
|
||||
transcludeMarkdown: true,
|
||||
},
|
||||
},
|
||||
'@storybook/addon-controls',
|
||||
'@storybook/addon-backgrounds',
|
||||
'@storybook/addon-toolbars',
|
||||
'@storybook/addon-measure',
|
||||
'@storybook/addon-outline',
|
||||
'@storybook/addon-a11y',
|
||||
],
|
||||
addons: ['@storybook/addon-essentials', '@storybook/addon-a11y'],
|
||||
// uncomment the property below if you want to apply some webpack config globally
|
||||
// webpackFinal: async (config, { configType }) => {
|
||||
// // Make whatever fine-grained changes you need that should apply to all storybook configs
|
||||
|
||||
Vendored
+1
-2
@@ -4,7 +4,6 @@
|
||||
"esbenp.prettier-vscode",
|
||||
"firsttris.vscode-jest-runner",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stevejpurves.cucumber",
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
"stevejpurves.cucumber"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
* @vegaprotocol/frontend
|
||||
*.graphql @vegaprotocol/core
|
||||
|
||||
@@ -8,8 +8,8 @@ COPY package.json ./
|
||||
COPY yarn.lock ./
|
||||
COPY . ./
|
||||
RUN apk add python3 make gcc g++
|
||||
RUN yarn --network-timeout 100000 --verbose --pure-lockfile
|
||||
RUN yarn nx build $APP --network-timeout 100000 --verbose --pure-lockfile
|
||||
RUN yarn
|
||||
RUN yarn nx build $APP
|
||||
|
||||
# Production environment
|
||||
FROM nginx:stable-alpine
|
||||
@@ -8,8 +8,8 @@ COPY package.json ./
|
||||
COPY yarn.lock ./
|
||||
COPY . ./
|
||||
RUN apk add python3 make gcc g++
|
||||
RUN yarn --network-timeout 100000 --verbose --pure-lockfile
|
||||
RUN yarn nx export $APP --network-timeout 100000 --verbose --pure-lockfile
|
||||
RUN yarn
|
||||
RUN yarn nx export $APP
|
||||
|
||||
# Production environment
|
||||
FROM nginx:stable-alpine
|
||||
Vendored
-20
@@ -1,20 +0,0 @@
|
||||
@Library('vega-shared-library') _
|
||||
|
||||
def commitHash = 'UNKNOWN'
|
||||
|
||||
pipeline {
|
||||
agent any
|
||||
options {
|
||||
skipDefaultCheckout true
|
||||
parallelsAlwaysFailFast()
|
||||
}
|
||||
stages {
|
||||
stage('approbation') {
|
||||
steps {
|
||||
sh 'printenv'
|
||||
checkout scm
|
||||
runApprobation ignoreFailure: false, frontendBranch: env.BRANCH_NAME, type: 'frontend'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,6 @@ An application for the status of the Vega network. Showing block height and othe
|
||||
|
||||
Hosting for static content being shared across apps, for example fonts.
|
||||
|
||||
### [Multisig-signer](./apps/multisig-signer)
|
||||
|
||||
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
|
||||
|
||||
# 🧱 Libraries in this repo
|
||||
|
||||
### [UI toolkit](./libs/ui-toolkit)
|
||||
@@ -73,10 +69,10 @@ Run `nx serve my-app` for a dev server. Navigate to the port specified in `app/<
|
||||
|
||||
### Using Apollo GraphQL and Generate Types
|
||||
|
||||
In order to generate the schemas for your GraphQL queries, you can run `GRAPHQL_SCHEMA_PATH=[YOUR SCHEMA FILE / API URL HERE] nx run types:generate`.
|
||||
In order to generate the schemas for your GraphQL queries, you can run `NX_VEGA_URL=[YOUR URL HERE] nx run types:generate`.
|
||||
|
||||
```bash
|
||||
export GRAPHQL_SCHEMA_PATH=https://api.n11.testnet.vega.xyz/graphql
|
||||
export NX_VEGA_URL=https://lb.testnet.vega.xyz/query
|
||||
yarn nx run types:generate
|
||||
```
|
||||
|
||||
@@ -86,14 +82,13 @@ Run `yarn nx run <my-app>-e2e:e2e` to execute the e2e tests with [cypress](https
|
||||
|
||||
Run `nx test my-app` to execute the unit tests with [Jest](https://jestjs.io), or `nx affected:test` to execute just unit tests affected by a change. You can also use `--watch` with these test to run jest in watch mode, see [Jest executor](https://nx.dev/packages/jest/executors/jest) for all CLI flags.
|
||||
|
||||
### Using wallet
|
||||
#### Trading app E2E tests
|
||||
|
||||
To run tests locally using your own wallets make sure you have generated at least two public keys and update the following environment variables in `cypress.config.js` to match your wallet.
|
||||
To run tests locally using your own wallets you can add the following environment variables to `cypress.json`
|
||||
|
||||
1. Set `VEGA_PUBLIC_KEY` and `TRUNCATED_VEGA_PUBLIC_KEY` to your first public key.
|
||||
2. Set `VEGA_PUBLIC_KEY2` and `TRUNCATED_VEGA_PUBLIC_KEY2` to your second public key.
|
||||
3. Set `TRADING_TEST_VEGA_WALLET_PASSPHRASE` as your wallet passphrase
|
||||
4. Add `ETH_WALLET_MNEMONIC` as your Ethereum wallet mnemonic
|
||||
1. Change `TRADING_TEST_VEGA_WALLET_NAME` to your Vega wallet name
|
||||
2. Add `TRADING_TEST_VEGA_WALLET_PASSPHRASE` as your wallet passphrase
|
||||
3. Add `ETH_WALLET_MNEMONIC` as your Ethereum wallet mnemonic
|
||||
|
||||
### Formatting
|
||||
|
||||
@@ -111,12 +106,12 @@ Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more
|
||||
|
||||
## Docker
|
||||
|
||||
The [Dockerfile](./dockerfiles) for running the frontends is pretty basic, merely building the application with the APP arg that is passed in and serving the application from [nginx](./nginx/nginx.conf). The only complexity that exists is that there is a script which allows the passing of run time environment variables to the containers. See configuration below for how to do this.
|
||||
The [Dockerfile](./Dockerfile) for running the frontends is pretty basic, merely building the application with the APP arg that is passed in and serving the application from [nginx](./nginx/nginx.conf). The only complexity that exists is that there is a script which allows the passing of run time environment variables to the containers. See configuration below for how to do this.
|
||||
|
||||
You can build any of the containers locally with the following command:
|
||||
|
||||
```bash
|
||||
docker build --dockerfile dockerfiles/Dockerfile.cra . --build-arg APP=[YOUR APP] --tag=[TAG]
|
||||
docker build . --build-arg APP=[YOUR APP] --tag=[TAG]
|
||||
```
|
||||
|
||||
In order to run a container:
|
||||
@@ -125,8 +120,6 @@ In order to run a container:
|
||||
docker run -p 3000:80 [TAG]
|
||||
```
|
||||
|
||||
Images ending with `.dist` are to pack locally created transpiled HTML files into nginx container for non-compatible with yarn architectures like M1 Mac
|
||||
|
||||
## Config
|
||||
|
||||
As environment variables are build time and not run time in frontend applications. We have built a system which allows for passing run time environment variables, this generates a JSON file that will override the default environment variables that the container was built with (which is always testnet, using the default .env files).
|
||||
@@ -134,7 +127,7 @@ As environment variables are build time and not run time in frontend application
|
||||
In order to override specific environment variables you can pass these to the container like this:
|
||||
|
||||
```bash
|
||||
docker run -e NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql -p 3000:80 [TAG]
|
||||
docker run -e NX_VEGA_URL=https://n04.d.vega.xyz/query -p 3000:80 [TAG]
|
||||
```
|
||||
|
||||
Which will now point the app to use a devnet data node. To see a list of all possible config properties see the readme.md for each app in the app directory.
|
||||
@@ -145,34 +138,6 @@ Coming soon! You will be able to run the containers within Vega Capsule.
|
||||
|
||||
You can run against a local instance of Vega Capsule today by using the .env.capsule present in the apps.
|
||||
|
||||
If you wish to run E2E tests for Token and Block Explorer (other areas to be added soon)
|
||||
|
||||
- Vegacapsule must be used in order for these tests to succeed, the vegacapsule repo README.md file contains the steps required to set this up, it must be installed globally.
|
||||
- However we start the capsule network a little differently to how it is laid out in those instructions:
|
||||
|
||||
In order to run the bootstrap command to generate and start a new network, we must do so using the following:
|
||||
|
||||
```bash
|
||||
vegacapsule network bootstrap --config-path=../frontend-monorepo/vegacapsule/config.hcl
|
||||
```
|
||||
|
||||
In order to setup and run vegawallet for e2e capsule tests, in a separate terminal window:
|
||||
|
||||
1. cd into `./vegacapsule`
|
||||
2. run:
|
||||
|
||||
```bash
|
||||
bash setup-vegawallet.sh
|
||||
```
|
||||
|
||||
3. copy generated `api-token` and paste the token into `CYPRESS_VEGA_WALLET_API_TOKEN` environment variable in either `apps/token-e2e/.env` or `apps/explorer-e2e/.env` depending on which project needs testing.
|
||||
|
||||
Note: The script is only needed if capsule was built for first time or fresh. To run existing wallet service for capsule:
|
||||
|
||||
```bash
|
||||
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
|
||||
```
|
||||
|
||||
# 📑 License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
||||
NX_VEGA_URL=https://api.stagnet3.vega.xyz/graphql
|
||||
NX_VEGA_ENV=STAGNET3
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_EXPLORER_URL=https://stagnet3.explorer.vega.xyz
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
CYPRESS_VEGA_ENV=STAGNET3
|
||||
CYPRESS_VEGA_WALLET_API_TOKEN=
|
||||
-1
@@ -1 +0,0 @@
|
||||
declare module '*.scss';
|
||||
Vendored
-20
@@ -1,20 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare namespace Cypress {
|
||||
// specify additional properties in the TestConfig object
|
||||
// in our case we will add "tags" property
|
||||
interface SuiteConfigOverrides {
|
||||
/**
|
||||
* List of tags for this test
|
||||
* @example a single tag
|
||||
* it('logs in', { tags: '@smoke' }, () => { ... })
|
||||
* @example multiple tags
|
||||
* it('works', { tags: ['@smoke', '@slow'] }, () => { ... })
|
||||
*/
|
||||
tags?: string | string[];
|
||||
}
|
||||
|
||||
interface Cypress {
|
||||
grep?: (grep?: string, tags?: string, burn?: string) => void;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "apps/console-lite-e2e/src",
|
||||
"projectType": "application",
|
||||
"targets": {
|
||||
"e2e": {
|
||||
"executor": "@nrwl/cypress:cypress",
|
||||
"options": {
|
||||
"cypressConfig": "apps/console-lite-e2e/cypress.config.js",
|
||||
"devServerTarget": "console-lite:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "console-lite:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nrwl/linter:eslint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["apps/console-lite-e2e/**/*.{js,ts}"]
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"executor": "@nrwl/workspace:run-commands",
|
||||
"outputs": [],
|
||||
"options": {
|
||||
"command": "yarn tsc --project ./apps/console-lite-e2e/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [],
|
||||
"implicitDependencies": ["console-lite"]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
describe('simple trading app', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockConsole();
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('render', () => {
|
||||
cy.get('#root').should('exist');
|
||||
});
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
const marketId = 'market-0';
|
||||
const marketName = 'ACTIVE MARKET';
|
||||
|
||||
describe('market selector', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockConsole();
|
||||
cy.visit(`/trading/${marketId}`);
|
||||
cy.connectVegaWallet();
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
it('should be properly rendered', () => {
|
||||
cy.get('input[placeholder="Search"]').should('have.value', marketName);
|
||||
cy.getByTestId('arrow-button').click();
|
||||
cy.getByTestId('market-pane').should('be.visible');
|
||||
cy.getByTestId('market-pane')
|
||||
.children()
|
||||
.find('[role="button"]')
|
||||
.first()
|
||||
.should('contain.text', marketName);
|
||||
cy.getByTestId('market-pane')
|
||||
.children()
|
||||
.find('[role="button"]')
|
||||
.first()
|
||||
.click();
|
||||
cy.getByTestId('market-pane').should('not.be.visible');
|
||||
});
|
||||
|
||||
it('typing should change list', () => {
|
||||
cy.get('input[placeholder="Search"]').type('{backspace}');
|
||||
cy.getByTestId('market-pane')
|
||||
.children()
|
||||
.find('[role="button"]')
|
||||
.should('have.length.at.least', 1);
|
||||
cy.get('input[placeholder="Search"]').clear();
|
||||
cy.get('input[placeholder="Search"]').type('ACTIVE');
|
||||
cy.getByTestId('market-pane')
|
||||
.children()
|
||||
.find('[role="button"]')
|
||||
.should('have.length', 1);
|
||||
cy.getByTestId('market-pane')
|
||||
.children()
|
||||
.find('[role="button"]')
|
||||
.last()
|
||||
.click();
|
||||
cy.location('pathname').should('eq', `/trading/${marketId}`);
|
||||
cy.get('input[placeholder="Search"]').should('have.value', marketName);
|
||||
});
|
||||
// constantly failing on ci
|
||||
it.skip('keyboard navigation should work well', () => {
|
||||
cy.get('input[placeholder="Search"]').type('{backspace}');
|
||||
cy.get('input[placeholder="Search"]').clear();
|
||||
cy.focused().realPress('ArrowDown');
|
||||
cy.focused().should('contain.text', marketName);
|
||||
cy.focused().realPress('ArrowDown');
|
||||
cy.focused().should('contain.text', 'ETHBTC').realPress('Enter');
|
||||
cy.location('pathname').should('eq', '/trading/ethbtc-quaterly');
|
||||
|
||||
cy.get('input[placeholder="Search"]').type('{backspace}');
|
||||
cy.get('input[placeholder="Search"]').clear();
|
||||
cy.getByTestId('market-pane').should('be.visible');
|
||||
cy.get('body').realPress('ArrowDown');
|
||||
cy.get('body').realPress('Tab');
|
||||
cy.getByTestId('market-pane').should('not.be.visible');
|
||||
});
|
||||
|
||||
it('mobile view', () => {
|
||||
cy.viewport('iphone-xr');
|
||||
cy.visit(`/trading/${marketId}`);
|
||||
cy.connectVegaWallet();
|
||||
cy.get('[role="dialog"]').should('not.exist');
|
||||
cy.getByTestId('arrow-button').click();
|
||||
cy.get('[role="dialog"]').should('be.visible');
|
||||
cy.get('input[placeholder="Search"]').then((search) => {
|
||||
cy.wrap(search).clear();
|
||||
});
|
||||
cy.getByTestId('market-pane')
|
||||
.children()
|
||||
.find('[role="button"]')
|
||||
.should('have.length', 3);
|
||||
cy.get('div[role="dialog"]').should('have.class', 'w-screen');
|
||||
cy.getByTestId('dialog-close').click();
|
||||
cy.get('input[placeholder="Search"]').should('have.value', marketName);
|
||||
});
|
||||
});
|
||||
@@ -1,278 +0,0 @@
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import { marketQuery, marketsQuery } from '@vegaprotocol/mock';
|
||||
|
||||
const marketId =
|
||||
marketsQuery().marketsConnection?.edges[1].node.id || 'market-1';
|
||||
const marketOverride = {
|
||||
market: {
|
||||
depth: {
|
||||
lastTrade: {
|
||||
price: '9893006',
|
||||
},
|
||||
},
|
||||
id: marketId,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
settlementAsset: {
|
||||
id: 'asset-id-2',
|
||||
name: 'DAI Name',
|
||||
symbol: 'tDAI',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('Market trade with wallet disconnected', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.mockConsole();
|
||||
cy.visit(`/trading/${marketId}`);
|
||||
cy.wait('@Market');
|
||||
console.log('marketId', marketId);
|
||||
});
|
||||
it('should not display steps', () => {
|
||||
cy.getByTestId('trading-connect-wallet')
|
||||
.find('h3')
|
||||
.should('have.text', 'Please connect your Vega wallet to make a trade');
|
||||
cy.getByTestId('trading-connect-wallet')
|
||||
.find('button')
|
||||
.should('have.text', 'Connect Vega wallet');
|
||||
cy.getByTestId('trading-connect-wallet')
|
||||
.find('a')
|
||||
.should('have.text', 'https://vega.xyz/wallet');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Market trade', { tags: '@regression' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockConsole();
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'Market', marketQuery(marketOverride));
|
||||
});
|
||||
cy.visit(`/trading/${marketId}`);
|
||||
cy.connectVegaWallet();
|
||||
cy.wait('@Market');
|
||||
});
|
||||
|
||||
it('side selector should work well', () => {
|
||||
cy.get('#step-1-control [aria-label^="Selected value"]').should(
|
||||
'have.text',
|
||||
'Long'
|
||||
);
|
||||
cy.get('#step-1-control [aria-label^="Selected value"]').click();
|
||||
cy.get('button[aria-label="Open short position"]').click();
|
||||
cy.get('#step-2-control').click();
|
||||
cy.get('#step-1-control [aria-label^="Selected value"]').should(
|
||||
'have.text',
|
||||
'Short'
|
||||
);
|
||||
});
|
||||
|
||||
it('side selector mobile view should work well', () => {
|
||||
cy.viewport('iphone-xr');
|
||||
cy.getByTestId('next-button').scrollIntoView().click();
|
||||
|
||||
cy.get('button[aria-label="Open long position"]').should(
|
||||
'have.class',
|
||||
'selected'
|
||||
);
|
||||
cy.get('button[aria-label="Open short position"]').should(
|
||||
'not.have.class',
|
||||
'selected'
|
||||
);
|
||||
|
||||
cy.get('button[aria-label="Open short position"]').click();
|
||||
cy.get('button[aria-label="Open long position"]').should(
|
||||
'not.have.class',
|
||||
'selected'
|
||||
);
|
||||
cy.get('button[aria-label="Open short position"]').should(
|
||||
'have.class',
|
||||
'selected'
|
||||
);
|
||||
cy.getByTestId('next-button').scrollIntoView().click();
|
||||
cy.get('#step-1-control').should(
|
||||
'contain.html',
|
||||
'aria-label="Selected value Short"'
|
||||
);
|
||||
});
|
||||
|
||||
it('size slider should work well', () => {
|
||||
cy.get('#step-1-control [aria-label^="Selected value"]').click();
|
||||
cy.get('button[aria-label="Open short position"]').click();
|
||||
cy.get('#step-2-control').click();
|
||||
cy.get('#step-2-panel')
|
||||
.find('dd')
|
||||
.eq(0)
|
||||
.find('button')
|
||||
.should('have.text', '1');
|
||||
cy.get('#step-2-panel').find('[role="slider"]').type('{rightarrow}');
|
||||
|
||||
cy.get('#step-2-panel')
|
||||
.find('dd')
|
||||
.eq(0)
|
||||
.find('button')
|
||||
.should('have.text', '2');
|
||||
});
|
||||
|
||||
it('percentage selection should work well', () => {
|
||||
cy.get('#step-1-control [aria-label^="Selected value"]').click();
|
||||
cy.get('button[aria-label="Open short position"]').click();
|
||||
cy.get('#step-2-control').click();
|
||||
cy.get('#step-2-panel')
|
||||
.find('dd')
|
||||
.eq(0)
|
||||
.find('button')
|
||||
.should('have.text', '1');
|
||||
|
||||
cy.getByTestId('max-label').should('have.text', '10');
|
||||
|
||||
cy.getByTestId('percentage-selector')
|
||||
.find('button')
|
||||
.contains('Max')
|
||||
.click();
|
||||
cy.get('#step-2-panel')
|
||||
.find('dd')
|
||||
.eq(0)
|
||||
.find('button')
|
||||
.should('have.text', '10');
|
||||
});
|
||||
|
||||
it('size input should work well', () => {
|
||||
cy.get('#step-1-control [aria-label^="Selected value"]').click();
|
||||
cy.get('button[aria-label="Open short position"]').click();
|
||||
cy.get('#step-2-control').click();
|
||||
cy.get('#step-2-panel')
|
||||
.find('dd')
|
||||
.eq(0)
|
||||
.find('button')
|
||||
.should('have.text', '1');
|
||||
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
|
||||
cy.get('#step-2-panel').find('dd').eq(0).find('input').type('{backspace}2');
|
||||
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
|
||||
cy.get('#step-2-panel')
|
||||
.find('dd')
|
||||
.eq(0)
|
||||
.find('button')
|
||||
.should('have.text', '2');
|
||||
cy.get('button').contains('Max').click();
|
||||
});
|
||||
|
||||
it('slippage value should be displayed', () => {
|
||||
cy.get('#step-1-control [aria-label^="Selected value"]').click();
|
||||
cy.get('button[aria-label="Open short position"]').click();
|
||||
cy.get('#step-2-control').click();
|
||||
cy.get('button').contains('Max').click();
|
||||
cy.get('#step-2-panel')
|
||||
.find('dl')
|
||||
.eq(2)
|
||||
.find('dd')
|
||||
.should('have.text', '0.01%');
|
||||
});
|
||||
|
||||
it('allow slippage value to be adjusted', () => {
|
||||
cy.get('#step-1-control [aria-label^="Selected value"]').click();
|
||||
cy.get('button[aria-label="Open short position"]').click();
|
||||
cy.get('#step-2-control').click();
|
||||
cy.get('button').contains('Max').click();
|
||||
cy.get('#step-2-panel')
|
||||
.find('dl')
|
||||
.eq(2)
|
||||
.find('dd')
|
||||
.should('have.text', '0.01%');
|
||||
cy.get('#step-2-panel').find('dl').eq(2).find('button').click();
|
||||
cy.get('#input-order-slippage')
|
||||
.focus()
|
||||
.type('{backspace}{backspace}{backspace}1');
|
||||
|
||||
cy.getByTestId('slippage-dialog').find('button').click();
|
||||
|
||||
cy.get('#step-2-panel')
|
||||
.find('dl')
|
||||
.eq(2)
|
||||
.find('dd')
|
||||
.should('have.text', '1%');
|
||||
});
|
||||
|
||||
it('notional position size should be present', () => {
|
||||
cy.get('#step-1-control [aria-label^="Selected value"]').click();
|
||||
cy.get('button[aria-label="Open short position"]').click();
|
||||
cy.get('#step-2-control').click();
|
||||
cy.get('#step-2-panel')
|
||||
.find('dd')
|
||||
.eq(0)
|
||||
.find('button')
|
||||
.should('have.text', '1');
|
||||
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
|
||||
cy.get('#step-2-panel').find('dd').eq(0).find('input').type('{backspace}2');
|
||||
cy.get('#step-2-panel').find('dd').eq(0).find('button').click();
|
||||
cy.get('#step-2-panel')
|
||||
.find('dt')
|
||||
.eq(2)
|
||||
.should('have.text', 'Est. Position Size (tDAI)');
|
||||
cy.get('#step-2-panel').find('dd').eq(2).should('have.text', '197.86012');
|
||||
});
|
||||
|
||||
it('total fees should be displayed', () => {
|
||||
cy.get('#step-1-control [aria-label^="Selected value"]').click();
|
||||
cy.get('button[aria-label="Open short position"]').click();
|
||||
cy.get('#step-2-control').click();
|
||||
cy.get('#step-2-panel')
|
||||
.find('dt')
|
||||
.eq(3)
|
||||
.should('have.text', 'Est. Fees (tDAI)');
|
||||
cy.get('#step-2-panel')
|
||||
.find('dd')
|
||||
.eq(3)
|
||||
.should('have.text', '3.00 (3.03%)');
|
||||
});
|
||||
|
||||
it('order review should display proper calculations', () => {
|
||||
cy.get('#step-3-control').click();
|
||||
|
||||
cy.getByTestId('review-trade')
|
||||
.get('#contracts_tooltip_trigger')
|
||||
.trigger('click')
|
||||
.realTouch();
|
||||
|
||||
cy.get('[data-radix-popper-content-wrapper]').contains(
|
||||
'The number of contracts determines'
|
||||
);
|
||||
cy.get('#step-3-panel').find('dd').eq(1).should('have.text', '1');
|
||||
|
||||
cy.get('#step-3-panel').find('dd').eq(2).should('have.text', '98.93006');
|
||||
|
||||
cy.get('#step-3-panel')
|
||||
.find('dd')
|
||||
.eq(3)
|
||||
.should('have.text', '3.00 (3.03%)');
|
||||
|
||||
cy.get('#step-3-panel')
|
||||
.find('dd')
|
||||
.eq(4)
|
||||
.should('have.text', '45,126.90058');
|
||||
|
||||
cy.getByTestId('place-order').should('be.enabled').click();
|
||||
});
|
||||
|
||||
it('info tooltip on mobile view should work well', () => {
|
||||
cy.viewport('iphone-xr');
|
||||
cy.get('#step-3-control').click();
|
||||
|
||||
// Start from the bottom tooltip to ensure the tooltip above
|
||||
// can be interacted with
|
||||
cy.getByTestId('review-trade').get('div.cursor-help').eq(1).realTouch();
|
||||
cy.get('[data-radix-popper-content-wrapper]').contains(
|
||||
'The notional size represents the position size'
|
||||
);
|
||||
|
||||
cy.getByTestId('review-trade')
|
||||
.get('#contracts_tooltip_trigger')
|
||||
.realTouch();
|
||||
cy.get('[data-radix-popper-content-wrapper]').contains(
|
||||
'The number of contracts determines'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,194 +0,0 @@
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import {
|
||||
accountsQuery,
|
||||
assetsQuery,
|
||||
chainIdQuery,
|
||||
fillsQuery,
|
||||
marginsQuery,
|
||||
marketsDataQuery,
|
||||
marketsQuery,
|
||||
ordersQuery,
|
||||
positionsQuery,
|
||||
statisticsQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
|
||||
describe('Portfolio page - wallet', { tags: '@smoke' }, () => {
|
||||
it('button for wallet connect should work', () => {
|
||||
cy.visit('/');
|
||||
cy.get('[href="/portfolio"]').eq(0).click();
|
||||
cy.getByTestId('trading-connect-wallet').should('be.visible');
|
||||
cy.connectVegaWallet();
|
||||
cy.getByTestId('trading-connect-wallet').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Portfolio page tabs', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'ChainId', chainIdQuery());
|
||||
aliasGQLQuery(req, 'Statistics', statisticsQuery());
|
||||
aliasGQLQuery(req, 'Positions', positionsQuery());
|
||||
aliasGQLQuery(req, 'Margins', marginsQuery());
|
||||
aliasGQLQuery(req, 'Markets', marketsQuery());
|
||||
aliasGQLQuery(req, 'MarketsData', marketsDataQuery());
|
||||
aliasGQLQuery(req, 'Accounts', accountsQuery());
|
||||
aliasGQLQuery(req, 'Assets', assetsQuery());
|
||||
});
|
||||
});
|
||||
|
||||
it('certain tabs should exist', () => {
|
||||
cy.visit('/portfolio');
|
||||
cy.connectVegaWallet();
|
||||
|
||||
cy.getByTestId('assets').click();
|
||||
cy.location('pathname').should('eq', '/portfolio/assets');
|
||||
|
||||
cy.getByTestId('positions').click();
|
||||
cy.location('pathname').should('eq', '/portfolio/positions');
|
||||
|
||||
cy.getByTestId('orders').click();
|
||||
cy.location('pathname').should('eq', '/portfolio/orders');
|
||||
|
||||
cy.getByTestId('fills').click();
|
||||
cy.location('pathname').should('eq', '/portfolio/fills');
|
||||
|
||||
cy.getByTestId('deposits').click();
|
||||
cy.location('pathname').should('eq', '/portfolio/deposits');
|
||||
});
|
||||
|
||||
describe('Assets view', () => {
|
||||
before(() => {
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'ChainId', chainIdQuery());
|
||||
aliasGQLQuery(req, 'Statistics', statisticsQuery());
|
||||
aliasGQLQuery(req, 'Positions', positionsQuery());
|
||||
aliasGQLQuery(req, 'Margins', marginsQuery());
|
||||
aliasGQLQuery(req, 'Markets', marketsQuery());
|
||||
aliasGQLQuery(req, 'MarketsData', marketsDataQuery());
|
||||
aliasGQLQuery(req, 'Accounts', accountsQuery());
|
||||
aliasGQLQuery(req, 'Assets', assetsQuery());
|
||||
});
|
||||
cy.visit('/portfolio/assets');
|
||||
cy.connectVegaWallet();
|
||||
});
|
||||
|
||||
it('data should be properly rendered', () => {
|
||||
cy.get('.ag-center-cols-container .ag-row').should('have.length', 5);
|
||||
cy.get(
|
||||
'.ag-center-cols-container [row-id="ACCOUNT_TYPE_GENERAL-asset-id-null"]'
|
||||
)
|
||||
.find('button')
|
||||
.click();
|
||||
cy.getByTestId('dialog-title').should(
|
||||
'have.text',
|
||||
'Asset details - tEURO'
|
||||
);
|
||||
cy.getByTestId('dialog-close').click();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Positions view', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'ChainId', chainIdQuery());
|
||||
aliasGQLQuery(req, 'Statistics', statisticsQuery());
|
||||
aliasGQLQuery(req, 'Positions', positionsQuery());
|
||||
aliasGQLQuery(req, 'Margins', marginsQuery());
|
||||
aliasGQLQuery(req, 'Markets', marketsQuery());
|
||||
aliasGQLQuery(req, 'MarketsData', marketsDataQuery());
|
||||
aliasGQLQuery(req, 'Accounts', accountsQuery());
|
||||
aliasGQLQuery(req, 'Assets', assetsQuery());
|
||||
});
|
||||
cy.visit('/portfolio/positions');
|
||||
cy.connectVegaWallet();
|
||||
});
|
||||
|
||||
it('data should be properly rendered', () => {
|
||||
cy.get('.ag-center-cols-container .ag-row').should('have.length', 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Orders view', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'ChainId', chainIdQuery());
|
||||
aliasGQLQuery(req, 'Statistics', statisticsQuery());
|
||||
aliasGQLQuery(req, 'Orders', ordersQuery());
|
||||
aliasGQLQuery(req, 'Markets', marketsQuery());
|
||||
});
|
||||
cy.visit('/portfolio/orders');
|
||||
cy.connectVegaWallet();
|
||||
});
|
||||
|
||||
it('data should be properly rendered', () => {
|
||||
cy.get('.ag-center-cols-container .ag-row').should('have.length', 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fills view', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'ChainId', chainIdQuery());
|
||||
aliasGQLQuery(req, 'Statistics', statisticsQuery());
|
||||
aliasGQLQuery(req, 'Orders', ordersQuery());
|
||||
aliasGQLQuery(req, 'Markets', marketsQuery());
|
||||
aliasGQLQuery(req, 'Fills', fillsQuery());
|
||||
});
|
||||
cy.visit('/portfolio/fills');
|
||||
cy.connectVegaWallet();
|
||||
});
|
||||
|
||||
it('data should be properly rendered', () => {
|
||||
cy.get('.ag-center-cols-container .ag-row').should('have.length', 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Empty views', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'ChainId', chainIdQuery());
|
||||
aliasGQLQuery(req, 'Statistics', statisticsQuery());
|
||||
aliasGQLQuery(req, 'Positions', { party: null });
|
||||
aliasGQLQuery(req, 'Accounts', { party: null });
|
||||
aliasGQLQuery(req, 'Orders', { party: null });
|
||||
aliasGQLQuery(req, 'Fills', { party: null });
|
||||
aliasGQLQuery(req, 'Markets', {
|
||||
marketsConnection: { edges: [], __typename: 'MarketConnection' },
|
||||
});
|
||||
aliasGQLQuery(req, 'Assets', {
|
||||
assetsConnection: { edges: null, __typename: 'AssetsConnection' },
|
||||
});
|
||||
aliasGQLQuery(req, 'Margins', marginsQuery());
|
||||
aliasGQLQuery(req, 'MarketsData', marketsDataQuery());
|
||||
});
|
||||
cy.visit('/portfolio');
|
||||
cy.connectVegaWallet();
|
||||
});
|
||||
|
||||
it('"No data to display" should be always displayed', () => {
|
||||
cy.getByTestId('assets').click();
|
||||
cy.get('div.flex.items-center.justify-center').should(
|
||||
'contain.text',
|
||||
'No data to display'
|
||||
);
|
||||
|
||||
cy.getByTestId('positions').click();
|
||||
cy.get('div.flex.items-center.justify-center').should(
|
||||
'contain.text',
|
||||
'No data to display'
|
||||
);
|
||||
|
||||
cy.getByTestId('orders').click();
|
||||
cy.get('div.flex.items-center.justify-center').should(
|
||||
'contain.text',
|
||||
'No data to display'
|
||||
);
|
||||
|
||||
cy.getByTestId('fills').click();
|
||||
cy.get('div.flex.items-center.justify-center').should(
|
||||
'contain.text',
|
||||
'No data to display'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import type { CyHttpMessages } from 'cypress/types/net-stubbing';
|
||||
import {
|
||||
accountsQuery,
|
||||
assetsQuery,
|
||||
chainIdQuery,
|
||||
estimateOrderQuery,
|
||||
fillsQuery,
|
||||
marginsQuery,
|
||||
marketDataQuery,
|
||||
marketDepthQuery,
|
||||
marketQuery,
|
||||
marketsCandlesQuery,
|
||||
marketsDataQuery,
|
||||
marketsQuery,
|
||||
ordersQuery,
|
||||
positionsQuery,
|
||||
statisticsQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Chainable<Subject> {
|
||||
mockConsole(): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mockPage = (req: CyHttpMessages.IncomingHttpRequest) => {
|
||||
aliasGQLQuery(req, 'ChainId', chainIdQuery());
|
||||
aliasGQLQuery(req, 'Statistics', statisticsQuery());
|
||||
aliasGQLQuery(req, 'Markets', marketsQuery());
|
||||
aliasGQLQuery(req, 'MarketsCandles', marketsCandlesQuery());
|
||||
aliasGQLQuery(req, 'MarketsData', marketsDataQuery());
|
||||
aliasGQLQuery(req, 'MarketData', marketDataQuery());
|
||||
aliasGQLQuery(req, 'Market', marketQuery());
|
||||
aliasGQLQuery(req, 'MarketTags', {});
|
||||
aliasGQLQuery(req, 'EstimateOrder', estimateOrderQuery());
|
||||
aliasGQLQuery(req, 'MarketNames', {});
|
||||
aliasGQLQuery(req, 'MarketDepth', marketDepthQuery());
|
||||
aliasGQLQuery(req, 'Positions', positionsQuery());
|
||||
aliasGQLQuery(req, 'Margins', marginsQuery());
|
||||
aliasGQLQuery(req, 'Accounts', accountsQuery());
|
||||
aliasGQLQuery(req, 'Assets', assetsQuery());
|
||||
aliasGQLQuery(req, 'SimpleMarkets', marketsQuery());
|
||||
aliasGQLQuery(req, 'Orders', ordersQuery());
|
||||
aliasGQLQuery(req, 'Fills', fillsQuery());
|
||||
};
|
||||
|
||||
export const addMockConsole = () => {
|
||||
Cypress.Commands.add('mockConsole', () => {
|
||||
cy.mockGQL(mockPage);
|
||||
});
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import '@vegaprotocol/cypress';
|
||||
import 'cypress-real-events/support';
|
||||
import registerCypressGrep from '@cypress/grep';
|
||||
import { addMockConsole } from './console-mock';
|
||||
|
||||
registerCypressGrep();
|
||||
addMockConsole();
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"sourceMap": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"allowJs": true,
|
||||
"types": ["cypress", "node", "cypress-real-events", "@cypress/grep"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "./declaration.d.ts", "index.d.ts"]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
|
||||
NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql
|
||||
NX_VEGA_ENV=DEVNET
|
||||
NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
|
||||
@@ -1,8 +0,0 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
|
||||
NX_VEGA_URL=https://api.vega.xyz/query
|
||||
NX_VEGA_ENV=MAINNET
|
||||
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
|
||||
NX_ETHEREUM_PROVIDER_URL=https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://etherscan.io
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
|
||||
@@ -1,7 +0,0 @@
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/sandbox-network.json
|
||||
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://static.vega.xyz/assets/sandbox-network.json
|
||||
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
@@ -1,10 +0,0 @@
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
|
||||
NX_VEGA_ENV=STAGNET1
|
||||
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
|
||||
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://stagnet1.console.vega.xyz\",\"STAGNET3\":\"https://stagnet3.console.vega.xyz\"}
|
||||
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
|
||||
NX_VEGA_URL=https://api.n00.stagnet1.vega.xyz/graphql
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
@@ -1,7 +0,0 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
||||
NX_VEGA_URL=https://api.stagnet3.vega.xyz/graphql
|
||||
NX_VEGA_ENV=STAGNET3
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_EXPLORER_URL=https://stagnet3.explorer.vega.xyz
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*", "__generated__", "__generated___"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import 'jest-canvas-mock';
|
||||
import { defaultFallbackInView } from 'react-intersection-observer';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
defaultFallbackInView(true);
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
global.DOMRect = class DOMRect {
|
||||
bottom = 0;
|
||||
left = 0;
|
||||
right = 0;
|
||||
top = 0;
|
||||
|
||||
constructor(
|
||||
public x = 0,
|
||||
public y = 0,
|
||||
public width = 0,
|
||||
public height = 0
|
||||
) {}
|
||||
static fromRect(other?: DOMRectInit): DOMRect {
|
||||
return new DOMRect(other?.x, other?.y, other?.width, other?.height);
|
||||
}
|
||||
toJSON() {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // Deprecated
|
||||
removeListener: jest.fn(), // Deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
jest.setTimeout(30000);
|
||||
@@ -1,77 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
|
||||
import {
|
||||
VegaConnectDialog,
|
||||
VegaManageDialog,
|
||||
VegaWalletProvider,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { Connectors } from './lib/vega-connectors';
|
||||
import '../styles.scss';
|
||||
import { AppLoader } from './components/app-loader';
|
||||
import Header from './components/header';
|
||||
import { Main } from './components/main';
|
||||
import LocalContext from './context/local-context';
|
||||
import useLocalValues from './hooks/use-local-values';
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
|
||||
function App() {
|
||||
const localValues = useLocalValues();
|
||||
const {
|
||||
vegaWalletDialog,
|
||||
menu: { setMenuOpen },
|
||||
} = localValues;
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
setMenuOpen(false);
|
||||
}, [location, setMenuOpen]);
|
||||
|
||||
const cacheConfig: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
Market: {
|
||||
merge: true,
|
||||
},
|
||||
Party: {
|
||||
merge: true,
|
||||
},
|
||||
Query: {},
|
||||
Account: {
|
||||
keyFields: false,
|
||||
fields: {
|
||||
balanceFormatted: {},
|
||||
},
|
||||
},
|
||||
Node: {
|
||||
keyFields: false,
|
||||
},
|
||||
Instrument: {
|
||||
keyFields: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<EnvironmentProvider>
|
||||
<NetworkLoader cache={cacheConfig}>
|
||||
<VegaWalletProvider>
|
||||
<LocalContext.Provider value={localValues}>
|
||||
<AppLoader>
|
||||
<div className="max-h-full min-h-full dark:bg-lite-black dark:text-neutral-200 bg-white text-neutral-800 grid grid-rows-[min-content,1fr]">
|
||||
<Header />
|
||||
<Main />
|
||||
<VegaConnectDialog connectors={Connectors} />
|
||||
<VegaManageDialog
|
||||
dialogOpen={vegaWalletDialog.manage}
|
||||
setDialogOpen={vegaWalletDialog.setManage}
|
||||
/>
|
||||
</div>
|
||||
</AppLoader>
|
||||
</LocalContext.Provider>
|
||||
</VegaWalletProvider>
|
||||
</NetworkLoader>
|
||||
</EnvironmentProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,109 +0,0 @@
|
||||
import React, { forwardRef, useCallback, useMemo, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
useScreenDimensions,
|
||||
useThemeSwitcher,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
GridOptions,
|
||||
GetRowIdParams,
|
||||
TabToNextCellParams,
|
||||
CellKeyDownEvent,
|
||||
FullWidthCellKeyDownEvent,
|
||||
IGetRowsParams,
|
||||
IDatasource,
|
||||
} from 'ag-grid-community';
|
||||
import { NO_DATA_MESSAGE } from '../../constants';
|
||||
import * as constants from '../simple-market-list/constants';
|
||||
|
||||
export interface GetRowsParams<T>
|
||||
extends Omit<IGetRowsParams, 'successCallback'> {
|
||||
successCallback(rowsThisBlock: T[], lastRow?: number): void;
|
||||
}
|
||||
|
||||
export interface Datasource<T> extends IDatasource {
|
||||
getRows(params: GetRowsParams<T>): void;
|
||||
}
|
||||
interface Props<T> extends GridOptions {
|
||||
rowData?: T[];
|
||||
datasource?: Datasource<T>;
|
||||
handleRowClicked?: (event: { data: T }) => void;
|
||||
components?: Record<string, unknown>;
|
||||
classNamesParam?: string | string[];
|
||||
}
|
||||
|
||||
const ConsoleLiteGrid = <T extends { id?: string }>(
|
||||
{ handleRowClicked, getRowId, classNamesParam, ...props }: Props<T>,
|
||||
ref?: React.Ref<AgGridReact>
|
||||
) => {
|
||||
const { isMobile, screenSize } = useScreenDimensions();
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const { theme } = useThemeSwitcher();
|
||||
const handleOnGridReady = useCallback(() => {
|
||||
(
|
||||
(ref as React.RefObject<AgGridReact>) || gridRef
|
||||
).current?.api?.sizeColumnsToFit();
|
||||
}, [gridRef, ref]);
|
||||
const onTabToNextCell = useCallback((params: TabToNextCellParams) => {
|
||||
const {
|
||||
api,
|
||||
previousCellPosition: { rowIndex },
|
||||
} = params;
|
||||
const rowCount = api.getDisplayedRowCount();
|
||||
if (rowCount <= rowIndex + 1) {
|
||||
return null;
|
||||
}
|
||||
return { ...params.previousCellPosition, rowIndex: rowIndex + 1 };
|
||||
}, []);
|
||||
const getRowIdLocal = useCallback(({ data }: GetRowIdParams) => data.id, []);
|
||||
const onCellKeyDown = useCallback(
|
||||
(
|
||||
params: (CellKeyDownEvent | FullWidthCellKeyDownEvent) & {
|
||||
event: KeyboardEvent;
|
||||
}
|
||||
) => {
|
||||
const { event: { key = '' } = {}, data } = params;
|
||||
if (key === 'Enter') {
|
||||
handleRowClicked?.({ data });
|
||||
}
|
||||
},
|
||||
[handleRowClicked]
|
||||
);
|
||||
const shouldSuppressHorizontalScroll = useMemo(() => {
|
||||
return !isMobile && constants.LARGE_SCREENS.includes(screenSize);
|
||||
}, [isMobile, screenSize]);
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
className={classNames(classNamesParam)}
|
||||
rowHeight={60}
|
||||
customThemeParams={
|
||||
theme === 'dark'
|
||||
? constants.agGridDarkVariables
|
||||
: constants.agGridLightVariables
|
||||
}
|
||||
onGridReady={handleOnGridReady}
|
||||
onRowClicked={handleRowClicked}
|
||||
rowClass={isMobile ? 'mobile' : ''}
|
||||
rowClassRules={constants.ROW_CLASS_RULES}
|
||||
ref={ref || gridRef}
|
||||
overlayNoRowsTemplate={NO_DATA_MESSAGE}
|
||||
suppressContextMenu
|
||||
getRowId={getRowId || getRowIdLocal}
|
||||
suppressMovableColumns
|
||||
suppressRowTransform
|
||||
onCellKeyDown={onCellKeyDown}
|
||||
tabToNextCell={onTabToNextCell}
|
||||
suppressHorizontalScroll={shouldSuppressHorizontalScroll}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ConsoleLiteGridForwarder = forwardRef(ConsoleLiteGrid) as <T>(
|
||||
p: Props<T> & { ref?: React.Ref<AgGridReact> }
|
||||
) => React.ReactElement;
|
||||
|
||||
export default ConsoleLiteGridForwarder;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as ConsoleLiteGrid } from './console-lite-grid';
|
||||
@@ -1,11 +0,0 @@
|
||||
query MarketTags($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
metadata {
|
||||
tags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type MarketTagsQueryVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type MarketTagsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null } } } } | null };
|
||||
|
||||
|
||||
export const MarketTagsDocument = gql`
|
||||
query MarketTags($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
metadata {
|
||||
tags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useMarketTagsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useMarketTagsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useMarketTagsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useMarketTagsQuery({
|
||||
* variables: {
|
||||
* marketId: // value for 'marketId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useMarketTagsQuery(baseOptions: Apollo.QueryHookOptions<MarketTagsQuery, MarketTagsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<MarketTagsQuery, MarketTagsQueryVariables>(MarketTagsDocument, options);
|
||||
}
|
||||
export function useMarketTagsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MarketTagsQuery, MarketTagsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<MarketTagsQuery, MarketTagsQueryVariables>(MarketTagsDocument, options);
|
||||
}
|
||||
export type MarketTagsQueryHookResult = ReturnType<typeof useMarketTagsQuery>;
|
||||
export type MarketTagsLazyQueryHookResult = ReturnType<typeof useMarketTagsLazyQuery>;
|
||||
export type MarketTagsQueryResult = Apollo.QueryResult<MarketTagsQuery, MarketTagsQueryVariables>;
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { MarketDealTicketAsset } from '@vegaprotocol/market-list';
|
||||
import { DealTicketBalance } from './deal-ticket-balance';
|
||||
|
||||
jest.mock('@vegaprotocol/wallet', () => ({
|
||||
...jest.requireActual('@vegaprotocol/wallet'),
|
||||
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
|
||||
}));
|
||||
|
||||
const tDAI: MarketDealTicketAsset = {
|
||||
__typename: 'Asset',
|
||||
id: '1',
|
||||
symbol: 'tDAI',
|
||||
name: 'TDAI',
|
||||
decimals: 2,
|
||||
};
|
||||
|
||||
let mockAccountBalance: {
|
||||
accountBalance: string;
|
||||
accountDecimals: number | null;
|
||||
} = { accountBalance: '1000000', accountDecimals: 2 };
|
||||
jest.mock('@vegaprotocol/accounts', () => ({
|
||||
...jest.requireActual('@vegaprotocol/accounts'),
|
||||
useAccountBalance: jest.fn(() => mockAccountBalance),
|
||||
}));
|
||||
|
||||
describe('DealTicketBalance', function () {
|
||||
it('should render the balance', () => {
|
||||
const { getByText, getByRole } = render(
|
||||
<DealTicketBalance settlementAsset={tDAI} isWalletConnected />,
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
|
||||
expect(getByRole('complementary')).toHaveAccessibleName('tDAI Balance');
|
||||
expect(getByText('10,000.00')).toBeInTheDocument();
|
||||
expect(getByText('tDAI')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should prompt to connect wallet', () => {
|
||||
const { getByText } = render(
|
||||
<DealTicketBalance settlementAsset={tDAI} isWalletConnected={false} />,
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
|
||||
expect(
|
||||
getByText('Please connect your Vega wallet to see your balance')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display zero balance', () => {
|
||||
mockAccountBalance = { accountBalance: '', accountDecimals: null };
|
||||
const { getByText } = render(
|
||||
<DealTicketBalance settlementAsset={tDAI} isWalletConnected={true} />,
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
|
||||
expect(getByText('No tDAI left to trade')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
t,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type { MarketDealTicket } from '@vegaprotocol/market-list';
|
||||
import { useAccountBalance } from '@vegaprotocol/accounts';
|
||||
|
||||
interface DealTicketBalanceProps {
|
||||
settlementAsset: MarketDealTicket['tradableInstrument']['instrument']['product']['settlementAsset'];
|
||||
isWalletConnected: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DealTicketBalance = ({
|
||||
settlementAsset,
|
||||
isWalletConnected,
|
||||
className = '',
|
||||
}: DealTicketBalanceProps) => {
|
||||
const settlementAssetId = settlementAsset?.id;
|
||||
const settlementAssetSymbol = settlementAsset?.symbol;
|
||||
|
||||
const { accountBalance, accountDecimals } =
|
||||
useAccountBalance(settlementAssetId) || {};
|
||||
const settlementBalance = toBigNum(accountBalance || 0, accountDecimals || 0);
|
||||
|
||||
const formattedNumber =
|
||||
accountBalance &&
|
||||
accountDecimals &&
|
||||
addDecimalsFormatNumber(accountBalance, accountDecimals);
|
||||
|
||||
const balance = (
|
||||
<p className="text-blue text-lg font-semibold">
|
||||
{!settlementBalance.isZero()
|
||||
? t(`${formattedNumber}`)
|
||||
: `No ${settlementAssetSymbol} left to trade`}
|
||||
</p>
|
||||
);
|
||||
|
||||
const connectWallet = (
|
||||
<p>{t('Please connect your Vega wallet to see your balance')}</p>
|
||||
);
|
||||
|
||||
const ariaLabel = t(`${settlementAssetSymbol} Balance`);
|
||||
|
||||
return (
|
||||
<aside
|
||||
aria-label={ariaLabel}
|
||||
className={classNames('text-right', className)}
|
||||
>
|
||||
<div className="inline-block">
|
||||
<span className="text-blue">{settlementAssetSymbol}</span>
|
||||
{isWalletConnected ? balance : connectWallet}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { DealTicketManager } from '@vegaprotocol/deal-ticket';
|
||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { DealTicketSteps } from './deal-ticket-steps';
|
||||
import { DealTicketBalance } from './deal-ticket-balance';
|
||||
import Baubles from './baubles-decor';
|
||||
import ConnectWallet from '../wallet-connector';
|
||||
import { useMemo } from 'react';
|
||||
import type {
|
||||
MarketDataUpdateFieldsFragment,
|
||||
MarketDealTicket,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import { marketDealTicketProvider } from '@vegaprotocol/market-list';
|
||||
|
||||
const tempEmptyText = (
|
||||
<p>{t('Please select a market from the markets page')}</p>
|
||||
);
|
||||
|
||||
export const DealTicketContainer = () => {
|
||||
const { marketId } = useParams<{ marketId: string }>();
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
marketId: marketId || '',
|
||||
}),
|
||||
[marketId]
|
||||
);
|
||||
const { data, loading } = useDataProvider<
|
||||
MarketDealTicket,
|
||||
MarketDataUpdateFieldsFragment
|
||||
>({
|
||||
dataProvider: marketDealTicketProvider,
|
||||
variables,
|
||||
skip: !marketId,
|
||||
});
|
||||
|
||||
const loader = <Loader />;
|
||||
if (marketId && data) {
|
||||
const balance = (
|
||||
<DealTicketBalance
|
||||
className="mb-4"
|
||||
settlementAsset={
|
||||
data.tradableInstrument.instrument.product?.settlementAsset
|
||||
}
|
||||
isWalletConnected={!!pubKey}
|
||||
/>
|
||||
);
|
||||
|
||||
const container = (
|
||||
<DealTicketManager market={data}>
|
||||
{loading ? loader : balance}
|
||||
<DealTicketSteps market={data} />
|
||||
</DealTicketManager>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="flex p-4 md:p-6">
|
||||
<section className="w-full md:w-1/2 md:min-w-[500px]">
|
||||
{pubKey ? container : <ConnectWallet />}
|
||||
</section>
|
||||
<Baubles />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
return marketId ? (
|
||||
<Splash>
|
||||
<p>{t('Could not load market')}</p>
|
||||
</Splash>
|
||||
) : (
|
||||
tempEmptyText
|
||||
);
|
||||
};
|
||||
@@ -1,149 +0,0 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
SliderRoot,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
SliderRange,
|
||||
FormGroup,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { InputSetter } from '../input-setter';
|
||||
|
||||
interface DealTicketSizeInputProps {
|
||||
step: number;
|
||||
min: number;
|
||||
max: number;
|
||||
value: number;
|
||||
onValueChange: (value: number) => void;
|
||||
positionDecimalPlaces: number;
|
||||
}
|
||||
|
||||
const getSizeLabel = (value: number): string => {
|
||||
const MIN_LABEL = 'Min';
|
||||
const MAX_LABEL = 'Max';
|
||||
if (value === 0) {
|
||||
return MIN_LABEL;
|
||||
} else if (value === 100) {
|
||||
return MAX_LABEL;
|
||||
}
|
||||
|
||||
return `${value}%`;
|
||||
};
|
||||
|
||||
export const DealTicketSizeInput = ({
|
||||
value,
|
||||
step,
|
||||
min,
|
||||
max,
|
||||
onValueChange,
|
||||
positionDecimalPlaces,
|
||||
}: DealTicketSizeInputProps) => {
|
||||
const sizeRatios = [0, 25, 50, 75, 100];
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
|
||||
const onInputValueChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let value = parseFloat(event.target.value);
|
||||
const isLessThanMin = value < min;
|
||||
const isMoreThanMax = value > max;
|
||||
if (isLessThanMin) {
|
||||
value = min;
|
||||
} else if (isMoreThanMax) {
|
||||
value = max;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
onValueChange(value);
|
||||
}
|
||||
|
||||
setInputValue(value);
|
||||
},
|
||||
[min, max, onValueChange, setInputValue]
|
||||
);
|
||||
|
||||
const onButtonValueChange = (size: number) => {
|
||||
const newVal = new BigNumber(size)
|
||||
.decimalPlaces(positionDecimalPlaces)
|
||||
.toNumber();
|
||||
onValueChange(newVal);
|
||||
setInputValue(newVal);
|
||||
};
|
||||
|
||||
const onSliderValueChange = useCallback(
|
||||
(value: number[]) => {
|
||||
const val = value[0];
|
||||
setInputValue(val);
|
||||
onValueChange(val);
|
||||
},
|
||||
[onValueChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between text-black dark:text-white mb-2">
|
||||
<span data-testid="min-label">{min}</span>
|
||||
<span data-testid="max-label">{max}</span>
|
||||
</div>
|
||||
<SliderRoot
|
||||
className="mb-2"
|
||||
value={[value]}
|
||||
onValueChange={onSliderValueChange}
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
>
|
||||
<SliderTrack className="bg-lightGrey dark:bg-offBlack">
|
||||
<SliderRange className="!bg-black dark:!bg-white" />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</SliderRoot>
|
||||
|
||||
<div
|
||||
data-testid="percentage-selector"
|
||||
className="flex w-full justify-between text-black dark:text-white mb-6"
|
||||
>
|
||||
{sizeRatios.map((size, index) => {
|
||||
const proportionalSize = size ? (size / 100) * max : min;
|
||||
return (
|
||||
<button
|
||||
className="no-underline hover:underline text-blue"
|
||||
onClick={() => onButtonValueChange(proportionalSize)}
|
||||
type="button"
|
||||
key={index}
|
||||
>
|
||||
{getSizeLabel(size)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<dl className="text-black dark:text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<dt>{t('Contracts')}</dt>
|
||||
<dd className="flex justify-end w-full">
|
||||
<FormGroup
|
||||
hideLabel={true}
|
||||
label="Enter Size"
|
||||
labelFor="trade-size-input"
|
||||
className="mb-1"
|
||||
>
|
||||
<InputSetter
|
||||
id="input-order-size-market"
|
||||
type="number"
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
className="w-full"
|
||||
value={inputValue}
|
||||
onChange={onInputValueChange}
|
||||
>
|
||||
{inputValue}
|
||||
</InputSetter>
|
||||
</FormGroup>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
import { DealTicketEstimates } from '@vegaprotocol/deal-ticket';
|
||||
import { DealTicketSizeInput } from './deal-ticket-size-input';
|
||||
|
||||
interface DealTicketSizeProps {
|
||||
step: number;
|
||||
min: number;
|
||||
max: number;
|
||||
size: number;
|
||||
onSizeChange: (value: number) => void;
|
||||
name: string;
|
||||
quoteName: string;
|
||||
price: string;
|
||||
estCloseOut: string;
|
||||
estMargin: string;
|
||||
fees: string;
|
||||
positionDecimalPlaces: number;
|
||||
notionalSize: string;
|
||||
}
|
||||
|
||||
export const DealTicketSize = ({
|
||||
step,
|
||||
min,
|
||||
max,
|
||||
price,
|
||||
quoteName,
|
||||
size,
|
||||
onSizeChange,
|
||||
estCloseOut,
|
||||
positionDecimalPlaces,
|
||||
fees,
|
||||
notionalSize,
|
||||
}: DealTicketSizeProps) => {
|
||||
return max === 0 ? (
|
||||
<p>Not enough balance to trade</p>
|
||||
) : (
|
||||
<div>
|
||||
<DealTicketSizeInput
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
value={size}
|
||||
onValueChange={onSizeChange}
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
/>
|
||||
<DealTicketEstimates
|
||||
quoteName={quoteName}
|
||||
fees={fees}
|
||||
estCloseOut={estCloseOut}
|
||||
price={price}
|
||||
notionalSize={notionalSize}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
Dialog,
|
||||
Icon,
|
||||
Intent,
|
||||
Tooltip,
|
||||
TrafficLight,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { InputSetter } from '../../components/input-setter';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import {
|
||||
DataTitle,
|
||||
ValueTooltipRow,
|
||||
EST_SLIPPAGE,
|
||||
} from '@vegaprotocol/deal-ticket';
|
||||
|
||||
interface DealTicketSlippageProps {
|
||||
step?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
value: number;
|
||||
onValueChange(value: number): void;
|
||||
}
|
||||
|
||||
export const DealTicketSlippage = ({
|
||||
value,
|
||||
step = 0.01,
|
||||
min = 0,
|
||||
max = 50,
|
||||
onValueChange,
|
||||
}: DealTicketSlippageProps) => {
|
||||
const [isDialogVisible, setIsDialogVisible] = useState(false);
|
||||
|
||||
const onChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
const numericValue = parseFloat(value);
|
||||
onValueChange(numericValue);
|
||||
},
|
||||
[onValueChange]
|
||||
);
|
||||
|
||||
const toggleDialog = useCallback(() => {
|
||||
setIsDialogVisible(!isDialogVisible);
|
||||
}, [isDialogVisible]);
|
||||
|
||||
const formLabel = (
|
||||
<label className="flex items-center mb-1">
|
||||
<span className="mr-1">{t('Adjust slippage tolerance')}</span>
|
||||
<Tooltip align="center" description={EST_SLIPPAGE}>
|
||||
<div className="cursor-help" tabIndex={-1}>
|
||||
<Icon
|
||||
name={IconNames.ISSUE}
|
||||
className="block rotate-180"
|
||||
ariaLabel={EST_SLIPPAGE}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</label>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={isDialogVisible}
|
||||
onChange={toggleDialog}
|
||||
intent={Intent.None}
|
||||
title={t('Transaction Settings')}
|
||||
>
|
||||
<div data-testid="slippage-dialog">
|
||||
{formLabel}
|
||||
<InputSetter
|
||||
id="input-order-slippage"
|
||||
isInputVisible
|
||||
hasError={!value}
|
||||
type="number"
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
className="w-full"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
>
|
||||
{value}%
|
||||
</InputSetter>
|
||||
</div>
|
||||
</Dialog>
|
||||
<dl className="text-black dark:text-white">
|
||||
<div className="flex justify-between mb-2">
|
||||
<DataTitle>{t('Est. Price Impact / Slippage')}</DataTitle>
|
||||
<div className="flex">
|
||||
<div className="mr-1">
|
||||
<ValueTooltipRow description={EST_SLIPPAGE}>
|
||||
<TrafficLight value={value} q1={1} q2={5}>
|
||||
{value}%
|
||||
</TrafficLight>
|
||||
</ValueTooltipRow>
|
||||
</div>
|
||||
<button type="button" onClick={toggleDialog}>
|
||||
<Icon
|
||||
name={IconNames.COG}
|
||||
className="block rotate-180"
|
||||
ariaLabel={t('Override slippage value')}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,342 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { Stepper } from '../stepper';
|
||||
import {
|
||||
getDefaultOrder,
|
||||
useOrderCloseOut,
|
||||
useOrderMargin,
|
||||
useMaximumPositionSize,
|
||||
useCalculateSlippage,
|
||||
validateAmount,
|
||||
} from '@vegaprotocol/deal-ticket';
|
||||
import { InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { MarketSelector } from '@vegaprotocol/deal-ticket';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { VegaTxStatus } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
t,
|
||||
toDecimal,
|
||||
removeDecimal,
|
||||
addDecimalsFormatNumber,
|
||||
addDecimal,
|
||||
formatNumber,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
useOrderSubmit,
|
||||
getOrderDialogTitle,
|
||||
getOrderDialogIntent,
|
||||
getOrderDialogIcon,
|
||||
OrderFeedback,
|
||||
} from '@vegaprotocol/orders';
|
||||
import { DealTicketSize } from './deal-ticket-size';
|
||||
import MarketNameRenderer from '../simple-market-list/simple-market-renderer';
|
||||
import SideSelector, { SIDE_NAMES } from './side-selector';
|
||||
import ReviewTrade from './review-trade';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { DealTicketSlippage } from './deal-ticket-slippage';
|
||||
import { useOrderValidation } from './use-order-validation';
|
||||
import type { MarketDealTicket } from '@vegaprotocol/market-list';
|
||||
|
||||
interface DealTicketMarketProps {
|
||||
market: MarketDealTicket;
|
||||
}
|
||||
|
||||
export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
|
||||
const navigate = useNavigate();
|
||||
const setMarket = useCallback(
|
||||
(marketId: string) => {
|
||||
navigate(`/trading/${marketId}`);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<OrderSubmissionBody['orderSubmission']>({
|
||||
mode: 'onChange',
|
||||
defaultValues: getDefaultOrder(market),
|
||||
});
|
||||
|
||||
const emptyString = ' - ';
|
||||
const step = toDecimal(market.positionDecimalPlaces);
|
||||
const order = watch();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { message: invalidText, isDisabled } = useOrderValidation({
|
||||
market,
|
||||
order,
|
||||
fieldErrors: errors,
|
||||
});
|
||||
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
|
||||
|
||||
const maxTrade = useMaximumPositionSize({
|
||||
marketId: market.id,
|
||||
settlementAssetId:
|
||||
market.tradableInstrument.instrument.product.settlementAsset.id,
|
||||
price: market?.depth?.lastTrade?.price,
|
||||
order,
|
||||
});
|
||||
|
||||
const estCloseOut = useOrderCloseOut({
|
||||
order,
|
||||
market,
|
||||
});
|
||||
const slippage = useCalculateSlippage({ marketId: market.id, order });
|
||||
const [slippageValue, setSlippageValue] = useState(
|
||||
slippage ? parseFloat(slippage) : 0
|
||||
);
|
||||
const transactionStatus =
|
||||
transaction.status === VegaTxStatus.Requested ||
|
||||
transaction.status === VegaTxStatus.Pending
|
||||
? 'pending'
|
||||
: 'default';
|
||||
|
||||
useEffect(() => {
|
||||
setSlippageValue(slippage ? parseFloat(slippage) : 0);
|
||||
}, [slippage]);
|
||||
|
||||
const price = useMemo(() => {
|
||||
if (slippage && market?.depth?.lastTrade?.price) {
|
||||
const isLong = order.side === Schema.Side.SIDE_BUY;
|
||||
const multiplier = new BigNumber(1)[isLong ? 'plus' : 'minus'](
|
||||
parseFloat(slippage) / 100
|
||||
);
|
||||
return new BigNumber(market?.depth?.lastTrade?.price)
|
||||
.multipliedBy(multiplier)
|
||||
.toString();
|
||||
}
|
||||
return undefined;
|
||||
}, [market?.depth?.lastTrade?.price, order.side, slippage]);
|
||||
|
||||
const estMargin = useOrderMargin({
|
||||
order,
|
||||
market,
|
||||
partyId: pubKey || '',
|
||||
derivedPrice: price,
|
||||
});
|
||||
|
||||
const formattedPrice =
|
||||
price && addDecimalsFormatNumber(price, market.decimalPlaces);
|
||||
|
||||
const notionalSize = useMemo(() => {
|
||||
if (price) {
|
||||
const size = new BigNumber(price).multipliedBy(order.size).toNumber();
|
||||
|
||||
return addDecimalsFormatNumber(size, market.decimalPlaces);
|
||||
}
|
||||
return null;
|
||||
}, [market.decimalPlaces, order.size, price]);
|
||||
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
|
||||
const fees = useMemo(() => {
|
||||
if (estMargin?.totalFees && notionalSize) {
|
||||
const percentage = new BigNumber(estMargin?.totalFees)
|
||||
.dividedBy(notionalSize)
|
||||
.multipliedBy(100)
|
||||
.decimalPlaces(2)
|
||||
.toString();
|
||||
|
||||
return `${addDecimalsFormatNumber(
|
||||
estMargin.totalFees,
|
||||
assetDecimals
|
||||
)} (${formatNumber(addDecimal(percentage, assetDecimals), 2)}%)`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [assetDecimals, estMargin?.totalFees, notionalSize]);
|
||||
|
||||
const max = useMemo(() => {
|
||||
return new BigNumber(maxTrade)
|
||||
.decimalPlaces(market.positionDecimalPlaces)
|
||||
.toNumber();
|
||||
}, [market.positionDecimalPlaces, maxTrade]);
|
||||
|
||||
useEffect(() => {
|
||||
setSlippageValue(slippage ? parseFloat(slippage) : 0);
|
||||
}, [slippage]);
|
||||
|
||||
const onSizeChange = useCallback(
|
||||
(value: number) => {
|
||||
const newVal = new BigNumber(value)
|
||||
.decimalPlaces(market.positionDecimalPlaces)
|
||||
.toString();
|
||||
const isValid = validateAmount(step, 'Size')(newVal);
|
||||
if (isValid !== 'step') {
|
||||
setValue('size', newVal);
|
||||
}
|
||||
},
|
||||
[market.positionDecimalPlaces, setValue, step]
|
||||
);
|
||||
|
||||
const onSlippageChange = useCallback(
|
||||
(value: number) => {
|
||||
if (market?.depth?.lastTrade?.price) {
|
||||
if (value) {
|
||||
const isLong = order.side === Schema.Side.SIDE_BUY;
|
||||
const multiplier = new BigNumber(1)[isLong ? 'plus' : 'minus'](
|
||||
value / 100
|
||||
);
|
||||
const bestAskPrice = new BigNumber(market?.depth?.lastTrade?.price)
|
||||
.multipliedBy(multiplier)
|
||||
.decimalPlaces(market.decimalPlaces)
|
||||
.toString();
|
||||
|
||||
setValue('price', bestAskPrice);
|
||||
|
||||
if (order.type === Schema.OrderType.TYPE_MARKET) {
|
||||
setValue('type', Schema.OrderType.TYPE_LIMIT);
|
||||
}
|
||||
} else {
|
||||
setValue('type', Schema.OrderType.TYPE_MARKET);
|
||||
setValue('price', market?.depth?.lastTrade?.price);
|
||||
}
|
||||
}
|
||||
setSlippageValue(value);
|
||||
},
|
||||
[
|
||||
market.decimalPlaces,
|
||||
market?.depth?.lastTrade?.price,
|
||||
order.side,
|
||||
order.type,
|
||||
setSlippageValue,
|
||||
setValue,
|
||||
]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(order: OrderSubmissionBody['orderSubmission']) => {
|
||||
if (transactionStatus !== 'pending') {
|
||||
submit({
|
||||
...order,
|
||||
price:
|
||||
order.price && removeDecimal(order.price, market.decimalPlaces),
|
||||
size: removeDecimal(order.size, market.positionDecimalPlaces),
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
transactionStatus,
|
||||
submit,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces,
|
||||
]
|
||||
);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
label: t('Select Market'),
|
||||
component: (
|
||||
<MarketSelector
|
||||
market={market}
|
||||
setMarket={setMarket}
|
||||
ItemRenderer={MarketNameRenderer}
|
||||
/>
|
||||
),
|
||||
value: market.tradableInstrument.instrument.name,
|
||||
},
|
||||
{
|
||||
label: t('Select Direction'),
|
||||
component: (
|
||||
<Controller
|
||||
name="side"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SideSelector value={field.value} onSelect={field.onChange} />
|
||||
)}
|
||||
/>
|
||||
),
|
||||
value: SIDE_NAMES[order.side] || '',
|
||||
},
|
||||
{
|
||||
label: t('Choose Position Size'),
|
||||
component:
|
||||
max !== null ? (
|
||||
<>
|
||||
<DealTicketSize
|
||||
step={step}
|
||||
min={step}
|
||||
max={max}
|
||||
onSizeChange={onSizeChange}
|
||||
size={new BigNumber(order.size).toNumber()}
|
||||
name="size"
|
||||
price={formattedPrice || emptyString}
|
||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||
quoteName={
|
||||
market.tradableInstrument.instrument.product.settlementAsset
|
||||
.symbol
|
||||
}
|
||||
notionalSize={notionalSize || emptyString}
|
||||
estCloseOut={estCloseOut || emptyString}
|
||||
fees={fees || emptyString}
|
||||
estMargin={estMargin?.margin || emptyString}
|
||||
/>
|
||||
<DealTicketSlippage
|
||||
value={slippageValue}
|
||||
onValueChange={onSlippageChange}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
t('Loading...')
|
||||
),
|
||||
value: order.size,
|
||||
},
|
||||
{
|
||||
label: t('Review Trade'),
|
||||
component: (
|
||||
<div className="mb-8">
|
||||
{invalidText && (
|
||||
<div className="mb-2">
|
||||
<InputError data-testid="dealticket-error-message">
|
||||
{invalidText}
|
||||
</InputError>
|
||||
</div>
|
||||
)}
|
||||
<ReviewTrade
|
||||
market={market}
|
||||
isDisabled={isDisabled}
|
||||
transactionStatus={transactionStatus}
|
||||
order={order}
|
||||
estCloseOut={estCloseOut || emptyString}
|
||||
estMargin={estMargin?.margin || emptyString}
|
||||
price={formattedPrice || emptyString}
|
||||
quoteName={
|
||||
market.tradableInstrument.instrument.product.settlementAsset
|
||||
.symbol
|
||||
}
|
||||
notionalSize={notionalSize || emptyString}
|
||||
fees={fees || emptyString}
|
||||
slippage={slippageValue}
|
||||
/>
|
||||
<Dialog
|
||||
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
||||
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback
|
||||
transaction={transaction}
|
||||
order={finalizedOrder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="px-2 py-4">
|
||||
<Stepper steps={steps} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,105 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
Button,
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { DealTicketEstimates } from '@vegaprotocol/deal-ticket';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { SIDE_NAMES } from './side-selector';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { MarketExpires } from '@vegaprotocol/market-info';
|
||||
import type { MarketDealTicket } from '@vegaprotocol/market-list';
|
||||
import { useMarketTagsQuery } from './__generated__/MarketTags';
|
||||
|
||||
interface Props {
|
||||
market: MarketDealTicket;
|
||||
isDisabled: boolean;
|
||||
transactionStatus?: string;
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
estCloseOut: string;
|
||||
estMargin: string;
|
||||
quoteName: string;
|
||||
price: string;
|
||||
fees: string;
|
||||
notionalSize: string;
|
||||
slippage: number;
|
||||
}
|
||||
|
||||
export default ({
|
||||
isDisabled,
|
||||
market,
|
||||
order,
|
||||
transactionStatus,
|
||||
estCloseOut,
|
||||
quoteName,
|
||||
fees,
|
||||
price,
|
||||
notionalSize,
|
||||
slippage,
|
||||
}: Props) => {
|
||||
const { data: tagsData } = useMarketTagsQuery({
|
||||
variables: { marketId: market.id },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mb-8 text-black dark:text-white" data-testid="review-trade">
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow noBorder>
|
||||
<div className="flex flex-none gap-x-2 items-center">
|
||||
<div
|
||||
className={classNames(
|
||||
{
|
||||
'buyButton dark:buyButtonDark':
|
||||
order.side === Schema.Side.SIDE_BUY,
|
||||
'sellButton dark:sellButtonDark':
|
||||
order.side === Schema.Side.SIDE_SELL,
|
||||
},
|
||||
'px-2 py-1 inline text-ui-small'
|
||||
)}
|
||||
>
|
||||
{SIDE_NAMES[order.side]}
|
||||
</div>
|
||||
<div>{market.tradableInstrument.instrument.product.quoteName}</div>
|
||||
<div>
|
||||
{tagsData?.market?.tradableInstrument.instrument.metadata
|
||||
.tags && (
|
||||
<MarketExpires
|
||||
tags={
|
||||
tagsData?.market.tradableInstrument.instrument.metadata.tags
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-blue">
|
||||
{`@ ${price} `}
|
||||
<span className="text-ui-small inline">(EST)</span>
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
|
||||
<DealTicketEstimates
|
||||
size={order.size}
|
||||
quoteName={quoteName}
|
||||
fees={fees}
|
||||
estCloseOut={estCloseOut}
|
||||
notionalSize={notionalSize}
|
||||
slippage={slippage.toString()}
|
||||
/>
|
||||
|
||||
<div className="mt-12 max-w-sm">
|
||||
<Button
|
||||
fill={true}
|
||||
type="submit"
|
||||
disabled={transactionStatus === 'pending' || isDisabled}
|
||||
data-testid="place-order"
|
||||
rightIcon="arrow-top-right"
|
||||
>
|
||||
{transactionStatus === 'pending' ? t('Pending...') : t('Submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,415 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { ValidationProps } from './use-order-validation';
|
||||
import { marketTranslations, useOrderValidation } from './use-order-validation';
|
||||
import type { MarketDealTicket } from '@vegaprotocol/market-list';
|
||||
import * as DealTicket from '@vegaprotocol/deal-ticket';
|
||||
|
||||
jest.mock('@vegaprotocol/wallet');
|
||||
jest.mock('@vegaprotocol/deal-ticket', () => {
|
||||
return {
|
||||
...jest.requireActual('@vegaprotocol/deal-ticket'),
|
||||
useOrderMarginValidation: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
type SettlementAsset =
|
||||
MarketDealTicket['tradableInstrument']['instrument']['product']['settlementAsset'];
|
||||
const asset: SettlementAsset = {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-id',
|
||||
symbol: 'asset-symbol',
|
||||
name: 'asset-name',
|
||||
decimals: 2,
|
||||
};
|
||||
|
||||
const market: MarketDealTicket = {
|
||||
id: 'market-id',
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 1,
|
||||
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
state: Schema.MarketState.STATE_ACTIVE,
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
id: 'instrument-id',
|
||||
name: 'instrument-name',
|
||||
code: 'instriment-code',
|
||||
metadata: {
|
||||
tags: [],
|
||||
},
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'quote-name',
|
||||
settlementAsset: asset,
|
||||
dataSourceSpecForTradingTermination: {
|
||||
id: 'dataSource-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
depth: {
|
||||
__typename: 'MarketDepth',
|
||||
lastTrade: {
|
||||
__typename: 'Trade',
|
||||
price: '100',
|
||||
},
|
||||
},
|
||||
fees: {
|
||||
__typename: 'Fees',
|
||||
factors: {
|
||||
__typename: 'FeeFactors',
|
||||
makerFee: '1',
|
||||
infrastructureFee: '2',
|
||||
liquidityFee: '3',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
__typename: 'MarketData',
|
||||
bestBidPrice: '1605489971',
|
||||
bestOfferPrice: '1606823730',
|
||||
markPrice: '1606823730',
|
||||
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
|
||||
staticMidPrice: '1606156850',
|
||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
marketState: Schema.MarketState.STATE_ACTIVE,
|
||||
indicativeVolume: '0',
|
||||
indicativePrice: '0',
|
||||
bestStaticBidPrice: '1605489971',
|
||||
bestStaticOfferPrice: '1606823730',
|
||||
targetStake: '8561302732',
|
||||
suppliedStake: '727654170336',
|
||||
auctionStart: null,
|
||||
auctionEnd: null,
|
||||
market: { __typename: 'Market', id: 'market-id' },
|
||||
},
|
||||
marketTimestamps: {
|
||||
__typename: 'MarketTimestamps',
|
||||
close: null,
|
||||
open: null,
|
||||
},
|
||||
};
|
||||
|
||||
const defaultWalletContext = {
|
||||
pubKey: '111111__111111',
|
||||
pubKeys: [],
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
selectPubKey: jest.fn(),
|
||||
connector: null,
|
||||
};
|
||||
|
||||
const order = {
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
marketId: 'market-id',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
size: '0.1',
|
||||
};
|
||||
|
||||
const defaultOrder = {
|
||||
market: { ...market },
|
||||
step: 0.1,
|
||||
order: {
|
||||
...order,
|
||||
},
|
||||
};
|
||||
|
||||
const ERROR = {
|
||||
KEY_MISSING: 'No public key selected',
|
||||
KEY_TAINTED: 'Selected public key has been tainted',
|
||||
MARKET_SUSPENDED: 'Market is currently suspended',
|
||||
MARKET_INACTIVE: 'Market is no longer active',
|
||||
MARKET_WAITING: 'Market is not active yet',
|
||||
MARKET_CONTINUOUS_LIMIT:
|
||||
'Only limit orders are permitted when market is in auction',
|
||||
MARKET_CONTINUOUS_TIF:
|
||||
'Until the auction ends, you can only place GFA, GTT, or GTC limit orders',
|
||||
FIELD_SIZE_REQ: 'You need to provide a size',
|
||||
FIELD_SIZE_MIN: `Size cannot be lower than "${defaultOrder.step}"`,
|
||||
FIELD_PRICE_REQ: 'You need to provide a price',
|
||||
FIELD_PRICE_MIN: 'The price cannot be negative',
|
||||
FIELD_PRICE_STEP_NULL: 'Order sizes must be in whole numbers for this market',
|
||||
FIELD_PRICE_STEP_DECIMAL: `The size field accepts up to ${market.positionDecimalPlaces} decimal places`,
|
||||
};
|
||||
|
||||
function setup(
|
||||
props?: Partial<ValidationProps>,
|
||||
context?: Partial<VegaWalletContextShape>
|
||||
) {
|
||||
const mockUseVegaWallet = useVegaWallet as jest.Mock;
|
||||
mockUseVegaWallet.mockReturnValue({ ...defaultWalletContext, context });
|
||||
return renderHook(() => useOrderValidation({ ...defaultOrder, ...props }), {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
}
|
||||
|
||||
describe('useOrderValidation', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Returns empty string when given valid data', () => {
|
||||
jest.spyOn(DealTicket, 'useOrderMarginValidation').mockReturnValue({
|
||||
balance: '0',
|
||||
margin: '100',
|
||||
balanceError: false,
|
||||
});
|
||||
|
||||
const { result } = setup();
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: false,
|
||||
message: ``,
|
||||
section: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns an error message when no keypair found', () => {
|
||||
jest.spyOn(DealTicket, 'useOrderMarginValidation').mockReturnValue({
|
||||
balance: '0',
|
||||
margin: '100',
|
||||
balanceError: false,
|
||||
});
|
||||
const { result } = setup(defaultOrder, { pubKey: null });
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: false,
|
||||
message: ``,
|
||||
section: '',
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
state
|
||||
${Schema.MarketState.STATE_SETTLED}
|
||||
${Schema.MarketState.STATE_REJECTED}
|
||||
${Schema.MarketState.STATE_TRADING_TERMINATED}
|
||||
${Schema.MarketState.STATE_CLOSED}
|
||||
${Schema.MarketState.STATE_CANCELLED}
|
||||
`(
|
||||
'Returns an error message for market state when not accepting orders',
|
||||
({ state }) => {
|
||||
const market = {
|
||||
...defaultOrder.market,
|
||||
data: { ...defaultOrder.market.data, marketState: state },
|
||||
};
|
||||
const { result } = setup({ market });
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: true,
|
||||
message: `This market is ${marketTranslations(
|
||||
state
|
||||
)} and not accepting orders`,
|
||||
section: 'sec-summary',
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
state
|
||||
${Schema.MarketState.STATE_PENDING}
|
||||
${Schema.MarketState.STATE_PROPOSED}
|
||||
`(
|
||||
'Returns an error message for market state suspended or pending',
|
||||
({ state }) => {
|
||||
jest.spyOn(DealTicket, 'useOrderMarginValidation').mockReturnValue({
|
||||
balance: '0',
|
||||
margin: '100',
|
||||
balanceError: false,
|
||||
});
|
||||
|
||||
const market = {
|
||||
...defaultOrder.market,
|
||||
data: {
|
||||
...defaultOrder.market.data,
|
||||
marketState: state,
|
||||
marketTradingMode:
|
||||
Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
|
||||
},
|
||||
};
|
||||
const { result } = setup({
|
||||
market,
|
||||
order: {
|
||||
...order,
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
|
||||
},
|
||||
});
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: false,
|
||||
message: `This market is ${Schema.MarketStateMapping[
|
||||
state as Schema.MarketState
|
||||
].toLowerCase()} and only accepting liquidity commitment orders`,
|
||||
section: 'sec-summary',
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
tradingMode | errorMessage
|
||||
${Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
||||
${Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
||||
${Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
||||
`(
|
||||
`Returns an error message when trying to submit a non-limit order for a "$tradingMode" market`,
|
||||
({ tradingMode, errorMessage }) => {
|
||||
const market = {
|
||||
...defaultOrder.market,
|
||||
data: { ...defaultOrder.market.data, marketTradingMode: tradingMode },
|
||||
};
|
||||
const { result } = setup({
|
||||
market,
|
||||
order: {
|
||||
...order,
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
},
|
||||
});
|
||||
expect(result.current.isDisabled).toBeTruthy();
|
||||
expect(result.current.message).toBe(errorMessage);
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
tradingMode | orderTimeInForce | errorMessage
|
||||
${Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION} | ${Schema.OrderTimeInForce.TIME_IN_FORCE_GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
`(
|
||||
`Returns an error message when submitting a limit order with a "$orderTimeInForce" value to a "$tradingMode" market`,
|
||||
({ tradingMode, orderTimeInForce, errorMessage }) => {
|
||||
const market = {
|
||||
...defaultOrder.market,
|
||||
data: { ...defaultOrder.market.data, marketTradingMode: tradingMode },
|
||||
};
|
||||
const { result } = setup({
|
||||
market,
|
||||
order: {
|
||||
...order,
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
timeInForce: orderTimeInForce,
|
||||
},
|
||||
});
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: true,
|
||||
message: errorMessage,
|
||||
section: 'sec-force',
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
fieldName | errorType | section | errorMessage
|
||||
${`size`} | ${`required`} | ${'sec-size'} | ${ERROR.FIELD_SIZE_REQ}
|
||||
${`size`} | ${`min`} | ${'sec-size'} | ${ERROR.FIELD_SIZE_MIN}
|
||||
${`price`} | ${`required`} | ${'sec-price'} | ${ERROR.FIELD_PRICE_REQ}
|
||||
${`price`} | ${`min`} | ${'sec-price'} | ${ERROR.FIELD_PRICE_MIN}
|
||||
`(
|
||||
`Returns an error message when the order $fieldName "$errorType" validation fails`,
|
||||
({ fieldName, errorType, section, errorMessage }) => {
|
||||
const { result } = setup({
|
||||
fieldErrors: { [fieldName]: { type: errorType } },
|
||||
order: {
|
||||
...order,
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
},
|
||||
});
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: true,
|
||||
message: errorMessage,
|
||||
section,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('Returns an error message when the order size incorrectly has decimal values', () => {
|
||||
const { result } = setup({
|
||||
market: { ...market, positionDecimalPlaces: 0 },
|
||||
fieldErrors: {
|
||||
size: { type: `validate`, message: DealTicket.ERROR_SIZE_DECIMAL },
|
||||
},
|
||||
});
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: true,
|
||||
message: ERROR.FIELD_PRICE_STEP_NULL,
|
||||
section: 'sec-size',
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns an error message when the order size has more decimals than allowed', () => {
|
||||
const { result } = setup({
|
||||
fieldErrors: {
|
||||
size: { type: `validate`, message: DealTicket.ERROR_SIZE_DECIMAL },
|
||||
},
|
||||
});
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: true,
|
||||
message: ERROR.FIELD_PRICE_STEP_DECIMAL,
|
||||
section: 'sec-size',
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns an error message when the estimated margin is higher than collateral', async () => {
|
||||
const invalidatedMockValue = {
|
||||
balance: '100',
|
||||
margin: '200',
|
||||
balanceError: true,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(DealTicket, 'useOrderMarginValidation')
|
||||
.mockReturnValue(invalidatedMockValue);
|
||||
|
||||
const { result } = setup({});
|
||||
|
||||
expect(result.current.isDisabled).toBe(false);
|
||||
|
||||
const testElement = (
|
||||
<DealTicket.MarginWarning
|
||||
margin={invalidatedMockValue.margin}
|
||||
balance={invalidatedMockValue.balance}
|
||||
asset={asset}
|
||||
/>
|
||||
);
|
||||
expect((result.current.message as React.ReactElement)?.props).toEqual(
|
||||
testElement.props
|
||||
);
|
||||
expect((result.current.message as React.ReactElement)?.type).toEqual(
|
||||
testElement.type
|
||||
);
|
||||
});
|
||||
|
||||
it.each`
|
||||
state
|
||||
${Schema.MarketState.STATE_PENDING}
|
||||
${Schema.MarketState.STATE_PROPOSED}
|
||||
`(
|
||||
'Returns error when market state is pending and size is wrong',
|
||||
({ state }) => {
|
||||
const market = {
|
||||
...defaultOrder.market,
|
||||
data: { ...defaultOrder.market.data, marketState: state },
|
||||
};
|
||||
const { result } = setup({
|
||||
fieldErrors: {
|
||||
size: { type: `validate`, message: DealTicket.ERROR_SIZE_DECIMAL },
|
||||
},
|
||||
market,
|
||||
});
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: true,
|
||||
message: ERROR.FIELD_PRICE_STEP_DECIMAL,
|
||||
section: 'sec-size',
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -1,375 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type { FieldErrors } from 'react-hook-form';
|
||||
import { useMemo } from 'react';
|
||||
import { DataGrid, t, toDecimal } from '@vegaprotocol/react-helpers';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
compileGridData,
|
||||
MarginWarning,
|
||||
isMarketInAuction,
|
||||
ERROR_SIZE_DECIMAL,
|
||||
useOrderMarginValidation,
|
||||
} from '@vegaprotocol/deal-ticket';
|
||||
import type { MarketDealTicket } from '@vegaprotocol/market-list';
|
||||
|
||||
export const DEAL_TICKET_SECTION = {
|
||||
TYPE: 'sec-type',
|
||||
SIZE: 'sec-size',
|
||||
PRICE: 'sec-price',
|
||||
FORCE: 'sec-force',
|
||||
EXPIRY: 'sec-expiry',
|
||||
SUMMARY: 'sec-summary',
|
||||
};
|
||||
|
||||
export const ERROR_EXPIRATION_IN_THE_PAST = 'ERROR_EXPIRATION_IN_THE_PAST';
|
||||
|
||||
export type ValidationProps = {
|
||||
step?: number;
|
||||
market: MarketDealTicket;
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
fieldErrors?: FieldErrors<OrderSubmissionBody['orderSubmission']>;
|
||||
};
|
||||
|
||||
export const marketTranslations = (marketState: Schema.MarketState) => {
|
||||
switch (marketState) {
|
||||
case Schema.MarketState.STATE_TRADING_TERMINATED:
|
||||
return t('terminated');
|
||||
default:
|
||||
return t(Schema.MarketStateMapping[marketState]).toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
export type DealTicketSection =
|
||||
| ''
|
||||
| typeof DEAL_TICKET_SECTION[keyof typeof DEAL_TICKET_SECTION];
|
||||
|
||||
export const useOrderValidation = ({
|
||||
market,
|
||||
fieldErrors,
|
||||
order,
|
||||
}: ValidationProps): {
|
||||
message: ReactNode | string;
|
||||
isDisabled: boolean;
|
||||
section: DealTicketSection;
|
||||
} => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const minSize = toDecimal(market.positionDecimalPlaces);
|
||||
const isInvalidOrderMargin = useOrderMarginValidation({ market, order });
|
||||
|
||||
const fieldErrorChecking = useMemo<{
|
||||
message: ReactNode | string;
|
||||
isDisabled: boolean;
|
||||
section: DealTicketSection;
|
||||
} | null>(() => {
|
||||
if (fieldErrors?.size?.type || fieldErrors?.price?.type) {
|
||||
if (fieldErrors?.size?.type === 'required') {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t('You need to provide a size'),
|
||||
section: DEAL_TICKET_SECTION.SIZE,
|
||||
};
|
||||
}
|
||||
|
||||
if (fieldErrors?.size?.type === 'min') {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(`Size cannot be lower than "${minSize}"`),
|
||||
section: DEAL_TICKET_SECTION.SIZE,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
fieldErrors?.price?.type === 'required' &&
|
||||
order.type !== Schema.OrderType.TYPE_MARKET
|
||||
) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t('You need to provide a price'),
|
||||
section: DEAL_TICKET_SECTION.PRICE,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
fieldErrors?.price?.type === 'min' &&
|
||||
order.type !== Schema.OrderType.TYPE_MARKET
|
||||
) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(`The price cannot be negative`),
|
||||
section: DEAL_TICKET_SECTION.PRICE,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
fieldErrors?.size?.type === 'validate' &&
|
||||
fieldErrors?.size?.message === ERROR_SIZE_DECIMAL
|
||||
) {
|
||||
if (market.positionDecimalPlaces === 0) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t('Order sizes must be in whole numbers for this market'),
|
||||
section: DEAL_TICKET_SECTION.SIZE,
|
||||
};
|
||||
}
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(
|
||||
`The size field accepts up to ${market.positionDecimalPlaces} decimal places`
|
||||
),
|
||||
section: DEAL_TICKET_SECTION.SIZE,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
fieldErrors?.expiresAt?.type === 'validate' &&
|
||||
fieldErrors?.expiresAt.message === ERROR_EXPIRATION_IN_THE_PAST
|
||||
) {
|
||||
return {
|
||||
isDisabled: false,
|
||||
message: t(
|
||||
'The expiry date that you have entered appears to be in the past'
|
||||
),
|
||||
section: DEAL_TICKET_SECTION.EXPIRY,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
fieldErrors?.size?.type,
|
||||
fieldErrors?.size?.message,
|
||||
fieldErrors?.price?.type,
|
||||
fieldErrors?.expiresAt?.type,
|
||||
fieldErrors?.expiresAt?.message,
|
||||
order.type,
|
||||
minSize,
|
||||
market.positionDecimalPlaces,
|
||||
]);
|
||||
|
||||
const { message, isDisabled, section } = useMemo<{
|
||||
message: ReactNode | string;
|
||||
isDisabled: boolean;
|
||||
section: DealTicketSection;
|
||||
}>(() => {
|
||||
if (!pubKey) {
|
||||
return {
|
||||
message: t('No public key selected'),
|
||||
isDisabled: true,
|
||||
section: DEAL_TICKET_SECTION.SUMMARY,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
Schema.MarketState.STATE_SETTLED,
|
||||
Schema.MarketState.STATE_REJECTED,
|
||||
Schema.MarketState.STATE_TRADING_TERMINATED,
|
||||
Schema.MarketState.STATE_CANCELLED,
|
||||
Schema.MarketState.STATE_CLOSED,
|
||||
].includes(market.data.marketState)
|
||||
) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(
|
||||
`This market is ${marketTranslations(
|
||||
market.data.marketState
|
||||
)} and not accepting orders`
|
||||
),
|
||||
section: DEAL_TICKET_SECTION.SUMMARY,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
Schema.MarketState.STATE_PROPOSED,
|
||||
Schema.MarketState.STATE_PENDING,
|
||||
].includes(market.data.marketState)
|
||||
) {
|
||||
if (fieldErrorChecking) {
|
||||
return fieldErrorChecking;
|
||||
}
|
||||
return {
|
||||
isDisabled: false,
|
||||
message: t(
|
||||
`This market is ${marketTranslations(
|
||||
market.data.marketState
|
||||
)} and only accepting liquidity commitment orders`
|
||||
),
|
||||
section: DEAL_TICKET_SECTION.SUMMARY,
|
||||
};
|
||||
}
|
||||
|
||||
if (isMarketInAuction(market)) {
|
||||
if (order.type === Schema.OrderType.TYPE_MARKET) {
|
||||
if (
|
||||
market.data.marketTradingMode ===
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
|
||||
market.data.trigger ===
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
|
||||
) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: (
|
||||
<span>
|
||||
{t('This market is in auction until it reaches')}{' '}
|
||||
<Tooltip
|
||||
description={<DataGrid grid={compileGridData(market)} />}
|
||||
>
|
||||
<span>{t('sufficient liquidity')}</span>
|
||||
</Tooltip>
|
||||
{'. '}
|
||||
{t('Only limit orders are permitted when market is in auction')}
|
||||
</span>
|
||||
),
|
||||
section: DEAL_TICKET_SECTION.TYPE,
|
||||
};
|
||||
}
|
||||
if (
|
||||
market.data.marketTradingMode ===
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
|
||||
market.data.trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_PRICE
|
||||
) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: (
|
||||
<span>
|
||||
{t('This market is in auction due to')}{' '}
|
||||
<Tooltip
|
||||
description={<DataGrid grid={compileGridData(market)} />}
|
||||
>
|
||||
<span>{t('high price volatility')}</span>
|
||||
</Tooltip>
|
||||
{'. '}
|
||||
{t('Only limit orders are permitted when market is in auction')}
|
||||
</span>
|
||||
),
|
||||
section: DEAL_TICKET_SECTION.TYPE,
|
||||
};
|
||||
}
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(
|
||||
'Only limit orders are permitted when market is in auction'
|
||||
),
|
||||
section: DEAL_TICKET_SECTION.SUMMARY,
|
||||
};
|
||||
}
|
||||
if (
|
||||
order.type === Schema.OrderType.TYPE_LIMIT &&
|
||||
[
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GFN,
|
||||
].includes(order.timeInForce)
|
||||
) {
|
||||
if (
|
||||
market.data.marketTradingMode ===
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
|
||||
market.data.trigger ===
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
|
||||
) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: (
|
||||
<span>
|
||||
{t('This market is in auction until it reaches')}{' '}
|
||||
<Tooltip
|
||||
description={<DataGrid grid={compileGridData(market)} />}
|
||||
>
|
||||
<span>{t('sufficient liquidity')}</span>
|
||||
</Tooltip>
|
||||
{'. '}
|
||||
{t(
|
||||
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
section: DEAL_TICKET_SECTION.FORCE,
|
||||
};
|
||||
}
|
||||
if (
|
||||
market.data.marketTradingMode ===
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
|
||||
market.data.trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_PRICE
|
||||
) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: (
|
||||
<span>
|
||||
{t('This market is in auction due to')}{' '}
|
||||
<Tooltip
|
||||
description={<DataGrid grid={compileGridData(market)} />}
|
||||
>
|
||||
<span>{t('high price volatility')}</span>
|
||||
</Tooltip>
|
||||
{'. '}
|
||||
{t(
|
||||
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
section: DEAL_TICKET_SECTION.FORCE,
|
||||
};
|
||||
}
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(
|
||||
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
|
||||
),
|
||||
section: DEAL_TICKET_SECTION.FORCE,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldErrorChecking) {
|
||||
return fieldErrorChecking;
|
||||
}
|
||||
|
||||
if (isInvalidOrderMargin.balanceError) {
|
||||
return {
|
||||
isDisabled: false,
|
||||
message: (
|
||||
<MarginWarning
|
||||
margin={isInvalidOrderMargin.margin}
|
||||
balance={isInvalidOrderMargin.balance}
|
||||
asset={market.tradableInstrument.instrument.product.settlementAsset}
|
||||
/>
|
||||
),
|
||||
section: DEAL_TICKET_SECTION.PRICE,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
].includes(market.data.marketTradingMode)
|
||||
) {
|
||||
return {
|
||||
isDisabled: false,
|
||||
message: t(
|
||||
'Any orders placed now will not trade until the auction ends'
|
||||
),
|
||||
section: DEAL_TICKET_SECTION.SUMMARY,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isDisabled: false,
|
||||
message: '',
|
||||
section: '',
|
||||
};
|
||||
}, [
|
||||
pubKey,
|
||||
market,
|
||||
fieldErrorChecking,
|
||||
isInvalidOrderMargin,
|
||||
order.type,
|
||||
order.timeInForce,
|
||||
]);
|
||||
|
||||
return { message, isDisabled, section };
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { DepositDialog, useDepositDialog } from '@vegaprotocol/deposits';
|
||||
|
||||
/**
|
||||
* Fetches data required for the Deposit page
|
||||
*/
|
||||
export const DepositContainer = () => {
|
||||
const openDepositDialog = useDepositDialog((state) => state.open);
|
||||
return (
|
||||
<div>
|
||||
<DepositDialog />
|
||||
<Button size="sm" onClick={() => openDepositDialog()}>
|
||||
{t('Make deposit')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './deposits';
|
||||
@@ -1,32 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import Logo from './logo';
|
||||
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
|
||||
import LocalContext from '../../context/local-context';
|
||||
|
||||
const Header = () => {
|
||||
const { updateVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
updateVegaWalletDialog: store.updateVegaWalletDialog,
|
||||
}));
|
||||
const {
|
||||
vegaWalletDialog: { setManage },
|
||||
} = useContext(LocalContext);
|
||||
return (
|
||||
<div
|
||||
className="dark flex items-stretch pr-6 py-6 bg-black text-neutral-400"
|
||||
data-testid="header"
|
||||
>
|
||||
<Logo />
|
||||
<div className="flex items-center gap-2 ml-auto relative z-10">
|
||||
<VegaWalletConnectButton
|
||||
setConnectDialog={updateVegaWalletDialog}
|
||||
setManageDialog={setManage}
|
||||
/>
|
||||
<ThemeSwitcher className="-my-4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -1,103 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export interface Item {
|
||||
name: string;
|
||||
id: string;
|
||||
url?: string;
|
||||
isActive?: boolean;
|
||||
color?: string;
|
||||
cssClass?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
active?: string;
|
||||
cssClass?: string[];
|
||||
items: Item[];
|
||||
'data-testid'?: string;
|
||||
'aria-label'?: string;
|
||||
}
|
||||
|
||||
const MenuItem = ({ id, name, url, isActive, cssClass }: Item): JSX.Element => {
|
||||
if (!url) {
|
||||
return <span data-testid={id}>{name}</span>;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
to={url}
|
||||
aria-label={name}
|
||||
className={classNames('pl-0 hover:opacity-75', cssClass, {
|
||||
active: isActive,
|
||||
})}
|
||||
data-testid={id}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const findActive = (active?: string, items?: Item[]) => {
|
||||
if (!active && items?.length) {
|
||||
return 0;
|
||||
}
|
||||
return items?.length
|
||||
? items.findIndex((item) => item.id === active) || 0
|
||||
: -1;
|
||||
};
|
||||
|
||||
export const HorizontalMenu = ({
|
||||
items,
|
||||
active,
|
||||
'data-testid': dataTestId,
|
||||
'aria-label': ariaLabel,
|
||||
}: Props) => {
|
||||
const [activeNumber, setActiveNumber] = useState<number>(
|
||||
findActive(active, items)
|
||||
);
|
||||
const slideContRef = useRef<HTMLUListElement | null>(null);
|
||||
const [sliderStyles, setSliderStyles] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
setActiveNumber(findActive(active, items));
|
||||
}, [active, items]);
|
||||
|
||||
useEffect(() => {
|
||||
const contStyles = (
|
||||
slideContRef.current as HTMLUListElement
|
||||
).getBoundingClientRect();
|
||||
const selectedStyles = (slideContRef.current as HTMLUListElement).children[
|
||||
activeNumber
|
||||
]?.getBoundingClientRect();
|
||||
const styles: Record<string, string> = selectedStyles
|
||||
? {
|
||||
backgroundColor: items[activeNumber].color || '',
|
||||
width: `${selectedStyles.width}px`,
|
||||
left: `${selectedStyles.left - contStyles.left}px`,
|
||||
}
|
||||
: {};
|
||||
setSliderStyles(styles);
|
||||
}, [activeNumber, slideContRef, items]);
|
||||
|
||||
return items.length ? (
|
||||
<ul
|
||||
ref={slideContRef}
|
||||
className="grid grid-flow-col auto-cols-min gap-4 relative pb-2 mb-2"
|
||||
data-testid={dataTestId}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<li key={item.id} className="md:mr-2 whitespace-nowrap">
|
||||
<MenuItem {...item} isActive={i === activeNumber} />
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
className="absolute bottom-0 h-[2px] transition-left duration-300 dark:bg-white bg-black"
|
||||
key="slider"
|
||||
style={sliderStyles}
|
||||
/>
|
||||
</ul>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default HorizontalMenu;
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as HorizontalMenu } from './horizontal-menu';
|
||||
export type { Item as HorizontalMenuItem } from './horizontal-menu';
|
||||
@@ -1 +0,0 @@
|
||||
export * from './input-setter';
|
||||
@@ -1,64 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render, waitFor, fireEvent } from '@testing-library/react';
|
||||
import { InputSetter } from './index';
|
||||
|
||||
describe('InputSetter Component', () => {
|
||||
it('should show the correct value and visibility based on props', async () => {
|
||||
const value = 'Hello';
|
||||
const isInputVisible = true;
|
||||
const { getByRole } = await render(
|
||||
<InputSetter
|
||||
id="input-order-size-market"
|
||||
type="number"
|
||||
className="w-full"
|
||||
value={value}
|
||||
isInputVisible={isInputVisible}
|
||||
onChange={() => false}
|
||||
/>
|
||||
);
|
||||
const input = getByRole('spinbutton');
|
||||
const btn = getByRole('button');
|
||||
expect(input).toHaveAttribute('value', value);
|
||||
expect(btn).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should toggle the visibility of the input when the button is clicked', async () => {
|
||||
const value = 'Hello';
|
||||
const isInputVisible = true;
|
||||
const { getByRole } = await render(
|
||||
<InputSetter
|
||||
id="input-order-size-market"
|
||||
type="number"
|
||||
className="w-full"
|
||||
value={value}
|
||||
isInputVisible={isInputVisible}
|
||||
onChange={() => false}
|
||||
/>
|
||||
);
|
||||
const btn = getByRole('button');
|
||||
fireEvent.click(btn);
|
||||
await waitFor(() => getByRole('button'));
|
||||
expect(getByRole('button')).toHaveTextContent(value);
|
||||
});
|
||||
|
||||
it('should toggle the visibility of the input when the enter key is pressed', async () => {
|
||||
const value = 'Hello';
|
||||
const isInputVisible = true;
|
||||
const { getByRole } = await render(
|
||||
<InputSetter
|
||||
id="input-order-size-market"
|
||||
name="input-order-size-market"
|
||||
type="number"
|
||||
className="w-full"
|
||||
value={value}
|
||||
isInputVisible={isInputVisible}
|
||||
onChange={() => false}
|
||||
/>
|
||||
);
|
||||
fireEvent.keyDown(getByRole('spinbutton'), {
|
||||
key: 'Enter',
|
||||
});
|
||||
await waitFor(() => getByRole('button'));
|
||||
expect(getByRole('button')).toHaveTextContent(value);
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Input } from '@vegaprotocol/ui-toolkit';
|
||||
import type { InputProps } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface InputSetterProps {
|
||||
buttonLabel?: string;
|
||||
isInputVisible?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const InputSetter = ({
|
||||
buttonLabel = t('set'),
|
||||
isInputVisible = false,
|
||||
children,
|
||||
...props
|
||||
}: InputSetterProps & InputProps) => {
|
||||
const [isInputToggled, setIsInputToggled] = useState(isInputVisible);
|
||||
|
||||
const toggleInput = useCallback(() => {
|
||||
setIsInputToggled(!isInputToggled);
|
||||
}, [isInputToggled]);
|
||||
|
||||
const onInputEnter = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.stopPropagation();
|
||||
toggleInput();
|
||||
}
|
||||
},
|
||||
[toggleInput]
|
||||
);
|
||||
|
||||
return isInputToggled ? (
|
||||
<div className="flex items-center">
|
||||
<Input {...props} onKeyDown={onInputEnter} />
|
||||
<button
|
||||
type="button"
|
||||
className="no-underline hover:underline text-blue ml-2"
|
||||
onClick={toggleInput}
|
||||
>
|
||||
{buttonLabel}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="no-underline hover:underline text-blue py-1.5"
|
||||
onClick={toggleInput}
|
||||
>
|
||||
{children || props.value}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { routerConfig } from '../../routes';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { NavItem } from './nav-item';
|
||||
import React from 'react';
|
||||
|
||||
interface NavProps {
|
||||
className?: string;
|
||||
tabs?: boolean;
|
||||
}
|
||||
|
||||
export const Nav = ({ className, tabs = false }: NavProps) => {
|
||||
return (
|
||||
<nav role={tabs ? 'tablist' : 'menu'} className={className}>
|
||||
{routerConfig
|
||||
.filter((r) => r.isNavItem)
|
||||
.map((r) => (
|
||||
<NavLink
|
||||
role={tabs ? 'tab' : 'menuitem'}
|
||||
key={r.name}
|
||||
to={r.path}
|
||||
className={({ isActive }) =>
|
||||
`text-base block md:mb-10 px-12 md:text-black md:dark:text-white ${
|
||||
isActive && 'text-white md:text-blue md:dark:text-blue'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<NavItem iconName={r.icon} label={r.text} />
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { useMemo, useRef, useCallback } from 'react';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { PriceCell, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import type { AccountFields } from '@vegaprotocol/accounts';
|
||||
import { aggregatedAccountsDataProvider, getId } from '@vegaprotocol/accounts';
|
||||
import type { IGetRowsParams } from 'ag-grid-community';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { NO_DATA_MESSAGE } from '../../../constants';
|
||||
import { ConsoleLiteGrid } from '../../console-lite-grid';
|
||||
import { useAccountColumnDefinitions } from '.';
|
||||
|
||||
const AccountsManager = () => {
|
||||
const { partyId = '' } = useOutletContext<{ partyId: string }>();
|
||||
const { isOpen, id, setOpen } = useAssetDetailsDialogStore();
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const dataRef = useRef<AccountFields[] | null>(null);
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
const update = useCallback(
|
||||
({ data }: { data: AccountFields[] | null }) => {
|
||||
dataRef.current = data;
|
||||
gridRef.current?.api?.refreshInfiniteCache();
|
||||
return true;
|
||||
},
|
||||
[gridRef]
|
||||
);
|
||||
const { data, error, loading } = useDataProvider<AccountFields[], never>({
|
||||
dataProvider: aggregatedAccountsDataProvider,
|
||||
update,
|
||||
variables,
|
||||
});
|
||||
const getRows = async ({
|
||||
successCallback,
|
||||
startRow,
|
||||
endRow,
|
||||
}: IGetRowsParams) => {
|
||||
const rowsThisBlock = dataRef.current
|
||||
? dataRef.current.slice(startRow, endRow)
|
||||
: [];
|
||||
const lastRow = dataRef.current?.length ?? -1;
|
||||
successCallback(rowsThisBlock, lastRow);
|
||||
};
|
||||
const { columnDefs, defaultColDef } = useAccountColumnDefinitions();
|
||||
console.log(data, loading);
|
||||
return (
|
||||
<>
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data?.length ? data : null}
|
||||
noDataMessage={NO_DATA_MESSAGE}
|
||||
>
|
||||
<ConsoleLiteGrid<AccountFields>
|
||||
rowData={data?.length ? undefined : []}
|
||||
rowModelType={data?.length ? 'infinite' : 'clientSide'}
|
||||
ref={gridRef}
|
||||
datasource={{ getRows }}
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
components={{ PriceCell }}
|
||||
getRowId={({ data }) => getId(data)}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
<AssetDetailsDialog
|
||||
assetId={id}
|
||||
open={isOpen}
|
||||
onChange={(open) => setOpen(open)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountsManager;
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as AccountManager } from './accounts';
|
||||
export { default as useAccountColumnDefinitions } from './use-column-definitions';
|
||||
@@ -1,80 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
|
||||
import type { AccountFields } from '@vegaprotocol/accounts';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import type { ColDef, GroupCellRendererParams } from 'ag-grid-community';
|
||||
import type { VegaValueFormatterParams } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const useAccountColumnDefinitions = () => {
|
||||
const { open } = useAssetDetailsDialogStore();
|
||||
const columnDefs: ColDef[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
colId: 'account-asset',
|
||||
headerName: t('Asset'),
|
||||
field: 'asset.symbol',
|
||||
headerClass: 'uppercase justify-start',
|
||||
cellClass: 'uppercase flex h-full items-center md:pl-4',
|
||||
cellRenderer: ({ value, data }: GroupCellRendererParams) =>
|
||||
value && value.length > 0 ? (
|
||||
<div className="md:pl-4 grid h-full items-center" title={value}>
|
||||
<div className="truncate min-w-0">
|
||||
<button
|
||||
className="hover:underline"
|
||||
onClick={() => {
|
||||
open(data.asset.id);
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
},
|
||||
{
|
||||
colId: 'deposited',
|
||||
headerName: t('Deposited'),
|
||||
field: 'deposited',
|
||||
cellRenderer: 'PriceCell',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<AccountFields, 'deposited'>) => {
|
||||
if (value && data) {
|
||||
return addDecimalsFormatNumber(value, data.asset.decimals);
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'used',
|
||||
headerName: t('Used'),
|
||||
field: 'used',
|
||||
cellRenderer: 'PriceCell',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<AccountFields, 'used'>) => {
|
||||
if (value && data) {
|
||||
return addDecimalsFormatNumber(value, data.asset.decimals);
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [open]);
|
||||
|
||||
const defaultColDef = useMemo(() => {
|
||||
return {
|
||||
sortable: true,
|
||||
unSortIcon: true,
|
||||
headerClass: 'uppercase',
|
||||
editable: false,
|
||||
};
|
||||
}, []);
|
||||
return { columnDefs, defaultColDef };
|
||||
};
|
||||
|
||||
export default useAccountColumnDefinitions;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const PORTFOLIO_ASSETS = 'assets';
|
||||
export const PORTFOLIO_POSITIONS = 'positions';
|
||||
export const PORTFOLIO_ORDERS = 'orders';
|
||||
export const PORTFOLIO_FILLS = 'fills';
|
||||
export const PORTFOLIO_DEPOSITS = 'deposits';
|
||||
|
||||
export const PORTFOLIO_ITEMS = [
|
||||
{
|
||||
name: t('Assets'),
|
||||
id: PORTFOLIO_ASSETS,
|
||||
url: `/portfolio/${PORTFOLIO_ASSETS}`,
|
||||
},
|
||||
{
|
||||
name: t('Positions'),
|
||||
id: PORTFOLIO_POSITIONS,
|
||||
url: `/portfolio/${PORTFOLIO_POSITIONS}`,
|
||||
},
|
||||
{
|
||||
name: t('Orders'),
|
||||
id: PORTFOLIO_ORDERS,
|
||||
url: `/portfolio/${PORTFOLIO_ORDERS}`,
|
||||
},
|
||||
{
|
||||
name: t('Fills'),
|
||||
id: PORTFOLIO_FILLS,
|
||||
url: `/portfolio/${PORTFOLIO_FILLS}`,
|
||||
},
|
||||
{
|
||||
name: t('Deposits'),
|
||||
id: PORTFOLIO_DEPOSITS,
|
||||
url: `/portfolio/${PORTFOLIO_DEPOSITS}`,
|
||||
},
|
||||
];
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useRef } from 'react';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { Trade } from '@vegaprotocol/fills';
|
||||
import { useFillsList } from '@vegaprotocol/fills';
|
||||
import type { BodyScrollEndEvent, BodyScrollEvent } from 'ag-grid-community';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { ConsoleLiteGrid } from '../../console-lite-grid';
|
||||
import { NO_DATA_MESSAGE } from '../../../constants';
|
||||
import useColumnDefinitions from './use-column-definitions';
|
||||
|
||||
const FillsManager = () => {
|
||||
const { partyId } = useOutletContext<{ partyId: string }>();
|
||||
const { columnDefs, defaultColDef } = useColumnDefinitions({ partyId });
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const scrolledToTop = useRef(true);
|
||||
const { data, error, loading, addNewRows, getRows } = useFillsList({
|
||||
partyId,
|
||||
gridRef,
|
||||
scrolledToTop,
|
||||
});
|
||||
|
||||
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||
if (event.top === 0) {
|
||||
addNewRows();
|
||||
}
|
||||
};
|
||||
|
||||
const onBodyScroll = (event: BodyScrollEvent) => {
|
||||
scrolledToTop.current = event.top <= 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data?.length ? data : null}
|
||||
noDataMessage={NO_DATA_MESSAGE}
|
||||
>
|
||||
<ConsoleLiteGrid<Trade>
|
||||
ref={gridRef}
|
||||
rowModelType="infinite"
|
||||
datasource={{ getRows }}
|
||||
onBodyScrollEnd={onBodyScrollEnd}
|
||||
onBodyScroll={onBodyScroll}
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FillsManager;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as FillsManager } from './fills';
|
||||
@@ -1,190 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { ColDef, ValueFormatterParams } from 'ag-grid-community';
|
||||
import type { Trade } from '@vegaprotocol/fills';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
addDecimal,
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
getDateTimeFormat,
|
||||
negativeClassNames,
|
||||
positiveClassNames,
|
||||
t,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
interface Props {
|
||||
partyId: string;
|
||||
}
|
||||
|
||||
const useColumnDefinitions = ({ partyId }: Props) => {
|
||||
const columnDefs: ColDef[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
colId: 'market',
|
||||
headerName: t('Market'),
|
||||
field: 'market.tradableInstrument.instrument.name',
|
||||
cellClass: '!flex h-full items-center !md:pl-4',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
colId: 'size',
|
||||
headerName: t('Size'),
|
||||
headerClass: 'uppercase',
|
||||
field: 'size',
|
||||
width: 100,
|
||||
cellClass: ({ data }: { data: Trade }) => {
|
||||
return classNames('!flex h-full items-center justify-center', {
|
||||
[positiveClassNames]: data?.buyer.id === partyId,
|
||||
[negativeClassNames]: data?.seller.id,
|
||||
});
|
||||
},
|
||||
valueFormatter: ({ value, data }: ValueFormatterParams) => {
|
||||
if (value && data?.market) {
|
||||
let prefix;
|
||||
if (data.buyer.id === partyId) {
|
||||
prefix = '+';
|
||||
} else if (data.seller.id) {
|
||||
prefix = '-';
|
||||
}
|
||||
|
||||
const size = addDecimalsFormatNumber(
|
||||
value,
|
||||
data.market.positionDecimalPlaces
|
||||
);
|
||||
return `${prefix}${size}`;
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'value',
|
||||
headerName: t('Value'),
|
||||
headerClass: 'uppercase text-center',
|
||||
cellClass: '!flex h-full items-center',
|
||||
field: 'price',
|
||||
valueFormatter: ({ value, data }: ValueFormatterParams) => {
|
||||
if (value && data?.market) {
|
||||
const asset =
|
||||
data.market.tradableInstrument.instrument.product.settlementAsset
|
||||
.symbol;
|
||||
const valueFormatted = addDecimalsFormatNumber(
|
||||
value,
|
||||
data.market.decimalPlaces
|
||||
);
|
||||
return `${valueFormatted} ${asset}`;
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
type: 'rightAligned',
|
||||
},
|
||||
{
|
||||
colId: 'filledvalue',
|
||||
headerName: t('Filled value'),
|
||||
headerClass: 'uppercase text-center',
|
||||
cellClass: '!flex h-full items-center',
|
||||
field: 'price',
|
||||
valueFormatter: ({ value, data }: ValueFormatterParams) => {
|
||||
if (value && data?.market) {
|
||||
const asset =
|
||||
data.market.tradableInstrument.instrument.product.settlementAsset
|
||||
.symbol;
|
||||
const size = new BigNumber(
|
||||
addDecimal(data.size, data.market.positionDecimalPlaces)
|
||||
);
|
||||
const price = new BigNumber(
|
||||
addDecimal(value, data.market.decimalPlaces)
|
||||
);
|
||||
|
||||
const total = size.times(price).toString();
|
||||
const valueFormatted = formatNumber(
|
||||
total,
|
||||
data?.market.decimalPlaces
|
||||
);
|
||||
return `${valueFormatted} ${asset}`;
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
type: 'rightAligned',
|
||||
},
|
||||
{
|
||||
colId: 'role',
|
||||
headerName: t('Role'),
|
||||
field: 'aggressor',
|
||||
width: 100,
|
||||
valueFormatter: ({ value, data }: ValueFormatterParams) => {
|
||||
if (value && data) {
|
||||
const taker = t('Taker');
|
||||
const maker = t('Maker');
|
||||
if (data?.buyer.id === partyId) {
|
||||
if (value === Schema.Side.SIDE_BUY) {
|
||||
return taker;
|
||||
} else {
|
||||
return maker;
|
||||
}
|
||||
} else if (data?.seller.id === partyId) {
|
||||
if (value === Schema.Side.SIDE_SELL) {
|
||||
return taker;
|
||||
} else {
|
||||
return maker;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'fee',
|
||||
headerName: t('Fee'),
|
||||
headerClass: 'uppercase text-center',
|
||||
cellClass: '!flex h-full items-center',
|
||||
field: 'market.tradableInstrument.instrument.product',
|
||||
valueFormatter: ({ value, data }: ValueFormatterParams) => {
|
||||
if (value && data) {
|
||||
const asset = value.settlementAsset;
|
||||
let feesObj;
|
||||
if (data.buyer.id === partyId) {
|
||||
feesObj = data.buyerFee;
|
||||
} else if (data?.seller.id === partyId) {
|
||||
feesObj = data?.sellerFee;
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const fee = new BigNumber(feesObj.makerFee)
|
||||
.plus(feesObj.infrastructureFee)
|
||||
.plus(feesObj.liquidityFee);
|
||||
const totalFees = addDecimalsFormatNumber(
|
||||
fee.toString(),
|
||||
asset.decimals
|
||||
);
|
||||
return `${totalFees} ${asset.symbol}`;
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
type: 'rightAligned',
|
||||
},
|
||||
{
|
||||
colId: 'date',
|
||||
headerName: t('Date'),
|
||||
field: 'createdAt',
|
||||
valueFormatter: ({ value }: ValueFormatterParams) => {
|
||||
return value ? getDateTimeFormat().format(new Date(value)) : '-';
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [partyId]);
|
||||
|
||||
const defaultColDef = useMemo(() => {
|
||||
return {
|
||||
sortable: true,
|
||||
unSortIcon: true,
|
||||
headerClass: 'uppercase',
|
||||
cellClass: '!flex h-full items-center',
|
||||
};
|
||||
}, []);
|
||||
return { columnDefs, defaultColDef };
|
||||
};
|
||||
|
||||
export default useColumnDefinitions;
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from './portfolio';
|
||||
export * from './accounts';
|
||||
export * from './fills';
|
||||
export * from './orders';
|
||||
export * from './positions';
|
||||
export * as constants from './constants';
|
||||
@@ -1 +0,0 @@
|
||||
export { default as OrdersManager } from './orders';
|
||||
@@ -1,107 +0,0 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { BodyScrollEndEvent, BodyScrollEvent } from 'ag-grid-community';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import type { Order } from '@vegaprotocol/orders';
|
||||
import {
|
||||
useOrderCancel,
|
||||
useOrderListData,
|
||||
useOrderEdit,
|
||||
OrderFeedback,
|
||||
getCancelDialogTitle,
|
||||
getCancelDialogIntent,
|
||||
getEditDialogTitle,
|
||||
OrderEditDialog,
|
||||
} from '@vegaprotocol/orders';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { ConsoleLiteGrid } from '../../console-lite-grid';
|
||||
import { NO_DATA_MESSAGE } from '../../../constants';
|
||||
import useColumnDefinitions from './use-column-definitions';
|
||||
|
||||
const OrdersManager = () => {
|
||||
const { partyId } = useOutletContext<{ partyId: string }>();
|
||||
const [editOrder, setEditOrder] = useState<Order | null>(null);
|
||||
const orderCancel = useOrderCancel();
|
||||
const orderEdit = useOrderEdit(editOrder);
|
||||
const { columnDefs, defaultColDef } = useColumnDefinitions({
|
||||
setEditOrder,
|
||||
orderCancel,
|
||||
});
|
||||
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const scrolledToTop = useRef(true);
|
||||
|
||||
const { data, error, loading, addNewRows, getRows } = useOrderListData({
|
||||
partyId,
|
||||
gridRef,
|
||||
scrolledToTop,
|
||||
});
|
||||
|
||||
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||
if (event.top === 0) {
|
||||
addNewRows();
|
||||
}
|
||||
};
|
||||
|
||||
const onBodyScroll = (event: BodyScrollEvent) => {
|
||||
scrolledToTop.current = event.top <= 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data?.length ? data : null}
|
||||
noDataMessage={NO_DATA_MESSAGE}
|
||||
>
|
||||
<ConsoleLiteGrid<Order>
|
||||
ref={gridRef}
|
||||
rowModelType={data?.length ? 'infinite' : 'clientSide'}
|
||||
rowData={data?.length ? undefined : []}
|
||||
datasource={{ getRows }}
|
||||
onBodyScrollEnd={onBodyScrollEnd}
|
||||
onBodyScroll={onBodyScroll}
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
/>
|
||||
<orderCancel.Dialog
|
||||
title={getCancelDialogTitle(orderCancel)}
|
||||
intent={getCancelDialogIntent(orderCancel)}
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback
|
||||
transaction={orderCancel.transaction}
|
||||
order={orderCancel.cancelledOrder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<orderEdit.Dialog
|
||||
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback
|
||||
transaction={orderEdit.transaction}
|
||||
order={orderEdit.updatedOrder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{editOrder && (
|
||||
<OrderEditDialog
|
||||
isOpen={Boolean(editOrder)}
|
||||
onChange={(isOpen) => {
|
||||
if (!isOpen) setEditOrder(null);
|
||||
}}
|
||||
order={editOrder}
|
||||
onSubmit={(fields) => {
|
||||
setEditOrder(null);
|
||||
orderEdit.edit({ price: fields.limitPrice });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrdersManager;
|
||||
@@ -1,269 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import type {
|
||||
ColDef,
|
||||
ValueFormatterParams,
|
||||
ICellRendererParams,
|
||||
} from 'ag-grid-community';
|
||||
import {
|
||||
addDecimal,
|
||||
getDateTimeFormat,
|
||||
negativeClassNames,
|
||||
positiveClassNames,
|
||||
t,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type { OrderFieldsFragment, Order } from '@vegaprotocol/orders';
|
||||
import type { OrderCancellationBody } from '@vegaprotocol/wallet';
|
||||
import { isOrderActive } from '@vegaprotocol/orders';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
type StatusKey = keyof typeof Schema.OrderStatusMapping;
|
||||
type RejectReasonKey = keyof typeof Schema.OrderRejectionReasonMapping;
|
||||
type OrderTimeKey = keyof typeof Schema.OrderTimeInForceMapping;
|
||||
interface Props {
|
||||
setEditOrder: (order: Order) => void;
|
||||
orderCancel: {
|
||||
cancel: (args: OrderCancellationBody['orderCancellation']) => void;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
const useColumnDefinitions = ({ setEditOrder, orderCancel }: Props) => {
|
||||
const columnDefs: ColDef[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
colId: 'market',
|
||||
headerName: t('Market'),
|
||||
field: 'market.tradableInstrument.instrument.code',
|
||||
headerClass: 'uppercase justify-start',
|
||||
cellClass: '!flex h-full items-center !md:pl-4',
|
||||
},
|
||||
{
|
||||
colId: 'size',
|
||||
headerName: t('Size'),
|
||||
field: 'size',
|
||||
headerClass: 'uppercase',
|
||||
cellClass: 'font-mono !flex h-full items-center',
|
||||
width: 80,
|
||||
cellClassRules: {
|
||||
[positiveClassNames]: ({ data }: { data: OrderFieldsFragment }) =>
|
||||
data?.side === Schema.Side.SIDE_BUY,
|
||||
[negativeClassNames]: ({ data }: { data: OrderFieldsFragment }) =>
|
||||
data?.side === Schema.Side.SIDE_SELL,
|
||||
},
|
||||
valueFormatter: ({ value, data }: ValueFormatterParams) => {
|
||||
if (value && data && data.market) {
|
||||
const prefix = data
|
||||
? data.side === Schema.Side.SIDE_BUY
|
||||
? '+'
|
||||
: '-'
|
||||
: '';
|
||||
return (
|
||||
prefix + addDecimal(value, data.market.positionDecimalPlaces)
|
||||
);
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'type',
|
||||
field: 'type',
|
||||
width: 80,
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: ValueFormatterParams & {
|
||||
value?: OrderFieldsFragment['type'];
|
||||
}) => Schema.OrderTypeMapping[value as Schema.OrderType],
|
||||
},
|
||||
{
|
||||
colId: 'status',
|
||||
field: 'status',
|
||||
cellClass: 'text-center font-mono !flex h-full items-center',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: ValueFormatterParams & {
|
||||
value?: StatusKey;
|
||||
}) => {
|
||||
if (value && data && data.market) {
|
||||
if (value === Schema.OrderStatus.STATUS_REJECTED) {
|
||||
return `${Schema.OrderStatusMapping[value as StatusKey]}: ${
|
||||
data.rejectionReason &&
|
||||
Schema.OrderRejectionReasonMapping[
|
||||
data.rejectionReason as RejectReasonKey
|
||||
]
|
||||
}`;
|
||||
}
|
||||
return Schema.OrderStatusMapping[value as StatusKey] as string;
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'filled',
|
||||
headerName: t('Filled'),
|
||||
headerClass: 'uppercase',
|
||||
field: 'remaining',
|
||||
cellClass: 'font-mono text-center !flex h-full items-center',
|
||||
width: 80,
|
||||
valueFormatter: ({
|
||||
data,
|
||||
value,
|
||||
}: ValueFormatterParams & {
|
||||
value?: OrderFieldsFragment['remaining'];
|
||||
}) => {
|
||||
if (value && data && data.market) {
|
||||
const dps = data.market.positionDecimalPlaces;
|
||||
const size = new BigNumber(data.size);
|
||||
const remaining = new BigNumber(value);
|
||||
const fills = size.minus(remaining);
|
||||
return `${addDecimal(fills.toString(), dps)}/${addDecimal(
|
||||
size.toString(),
|
||||
dps
|
||||
)}`;
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'price',
|
||||
field: 'price',
|
||||
type: 'rightAligned',
|
||||
width: 125,
|
||||
headerClass: 'uppercase text-right',
|
||||
cellClass: 'font-mono text-right !flex h-full items-center',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: ValueFormatterParams & {
|
||||
value?: OrderFieldsFragment['price'];
|
||||
}) => {
|
||||
if (
|
||||
value === undefined ||
|
||||
!data ||
|
||||
!data.market ||
|
||||
data.type === Schema.OrderType.TYPE_MARKET
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
return addDecimal(value, data.market.decimalPlaces);
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'timeInForce',
|
||||
field: 'timeInForce',
|
||||
minWidth: 120,
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: ValueFormatterParams & {
|
||||
value?: OrderTimeKey;
|
||||
}) => {
|
||||
if (value && data?.market) {
|
||||
if (
|
||||
value === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT &&
|
||||
data.expiresAt
|
||||
) {
|
||||
const expiry = getDateTimeFormat().format(
|
||||
new Date(data.expiresAt)
|
||||
);
|
||||
return `${
|
||||
Schema.OrderTimeInForceMapping[value as OrderTimeKey]
|
||||
}: ${expiry}`;
|
||||
}
|
||||
|
||||
return Schema.OrderTimeInForceMapping[value as OrderTimeKey];
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'createdat',
|
||||
field: 'createdAt',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: ValueFormatterParams & {
|
||||
value?: OrderFieldsFragment['createdAt'];
|
||||
}) => {
|
||||
return value ? getDateTimeFormat().format(new Date(value)) : value;
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'updated',
|
||||
field: 'updatedAt',
|
||||
cellClass: '!flex h-full items-center justify-center',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: ValueFormatterParams & {
|
||||
value?: OrderFieldsFragment['updatedAt'];
|
||||
}) => {
|
||||
return value ? getDateTimeFormat().format(new Date(value)) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'edit',
|
||||
field: 'edit',
|
||||
width: 75,
|
||||
cellRenderer: ({ data }: ICellRendererParams) => {
|
||||
if (!data) return null;
|
||||
if (isOrderActive(data.status)) {
|
||||
return (
|
||||
<Button
|
||||
data-testid="edit"
|
||||
onClick={() => {
|
||||
setEditOrder(data);
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
{t('Edit')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'cancel',
|
||||
field: 'cancel',
|
||||
minWidth: 130,
|
||||
cellRenderer: ({ data }: ICellRendererParams) => {
|
||||
if (!data) return null;
|
||||
if (isOrderActive(data.status)) {
|
||||
return (
|
||||
<Button
|
||||
size="xs"
|
||||
data-testid="cancel"
|
||||
onClick={() => {
|
||||
if (data.market) {
|
||||
orderCancel.cancel({
|
||||
orderId: data.id,
|
||||
marketId: data.market.id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [orderCancel, setEditOrder]);
|
||||
const defaultColDef = useMemo(() => {
|
||||
return {
|
||||
sortable: true,
|
||||
unSortIcon: true,
|
||||
headerClass: 'uppercase',
|
||||
cellClass: '!flex h-full items-center',
|
||||
};
|
||||
}, []);
|
||||
return { columnDefs, defaultColDef };
|
||||
};
|
||||
|
||||
export default useColumnDefinitions;
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import ConnectWallet from '../wallet-connector';
|
||||
import { useOutlet, useLocation } from 'react-router-dom';
|
||||
import { HorizontalMenu } from '../horizontal-menu';
|
||||
import * as constants from './constants';
|
||||
|
||||
export const Portfolio = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { pathname } = useLocation();
|
||||
const module = pathname.split('/portfolio/')?.[1] ?? '';
|
||||
const outlet = useOutlet({ partyId: pubKey || '' });
|
||||
if (!pubKey) {
|
||||
return (
|
||||
<section className="xl:w-1/2">
|
||||
<ConnectWallet />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 md:p-6 grid grid-rows-[min-content_1fr]">
|
||||
<HorizontalMenu active={module} items={constants.PORTFOLIO_ITEMS} />
|
||||
{outlet}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { default as PositionsManager } from './positions';
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { PriceFlashCell } from '@vegaprotocol/react-helpers';
|
||||
import { usePositionsData, getRowId } from '@vegaprotocol/positions';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { ConsoleLiteGrid } from '../../console-lite-grid';
|
||||
import { useRef } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { Position } from '@vegaprotocol/positions';
|
||||
import { NO_DATA_MESSAGE } from '../../../constants';
|
||||
|
||||
import useColumnDefinitions from './use-column-definitions';
|
||||
|
||||
const Positions = () => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const { partyId } = useOutletContext<{ partyId: string }>();
|
||||
const { data, error, loading } = usePositionsData(partyId, gridRef);
|
||||
const { columnDefs, defaultColDef } = useColumnDefinitions();
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data?.length ? data : null}
|
||||
noDataMessage={NO_DATA_MESSAGE}
|
||||
>
|
||||
<ConsoleLiteGrid<Position>
|
||||
ref={gridRef}
|
||||
domLayout="autoHeight"
|
||||
classNamesParam="h-auto"
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
getRowId={getRowId}
|
||||
rowData={data || undefined}
|
||||
components={{ PriceFlashCell }}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Positions;
|
||||
@@ -1,278 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
getDateTimeFormat,
|
||||
PriceFlashCell,
|
||||
signedNumberCssClassRules,
|
||||
t,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type { Position } from '@vegaprotocol/positions';
|
||||
import { AmountCell } from '@vegaprotocol/positions';
|
||||
import type {
|
||||
CellRendererSelectorResult,
|
||||
ICellRendererParams,
|
||||
ValueGetterParams,
|
||||
GroupCellRendererParams,
|
||||
ColDef,
|
||||
} from 'ag-grid-community';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { VegaValueFormatterParams } from '@vegaprotocol/ui-toolkit';
|
||||
import { Intent, ProgressBarCell } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const EmptyCell = () => '';
|
||||
|
||||
const useColumnDefinitions = () => {
|
||||
const columnDefs: ColDef[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
colId: 'market',
|
||||
headerName: t('Market'),
|
||||
headerClass: 'uppercase justify-start',
|
||||
cellClass: '!flex h-full items-center !md:pl-4',
|
||||
field: 'marketName',
|
||||
cellRenderer: ({ value }: GroupCellRendererParams) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const valueFormatted: [string, string?] = (() => {
|
||||
// split market name into two parts, 'Part1 (Part2)' or 'Part1 - Part2'
|
||||
const matches = value.match(/^(.*)(\((.*)\)| - (.*))\s*$/);
|
||||
if (matches && matches[1] && matches[3]) {
|
||||
return [matches[1].trim(), matches[3].trim()];
|
||||
}
|
||||
return [value];
|
||||
})();
|
||||
if (valueFormatted && valueFormatted[1]) {
|
||||
return (
|
||||
<div className="leading-tight">
|
||||
<div>{valueFormatted[0]}</div>
|
||||
<div>{valueFormatted[1]}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return valueFormatted ? valueFormatted[0] : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'amount',
|
||||
headerName: t('Amount'),
|
||||
headerClass: 'uppercase',
|
||||
cellClass: '!flex h-full items-center',
|
||||
field: 'openVolume',
|
||||
valueGetter: ({ node, data }: ValueGetterParams) => {
|
||||
return node?.rowPinned ? data?.notional : data?.openVolume;
|
||||
},
|
||||
type: 'rightAligned',
|
||||
cellRendererSelector: (
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: params.node.rowPinned ? PriceFlashCell : AmountCell,
|
||||
};
|
||||
},
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Position, 'openVolume'>) => {
|
||||
let ret;
|
||||
if (value && data) {
|
||||
ret = node?.rowPinned
|
||||
? addDecimalsFormatNumber(value, data.decimals)
|
||||
: data;
|
||||
}
|
||||
// FIXME this column needs refactoring
|
||||
return ret as unknown as string;
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'markprice',
|
||||
headerName: t('Mark price'),
|
||||
headerClass: 'uppercase',
|
||||
cellClass: '!flex h-full items-center justify-center',
|
||||
field: 'markPrice',
|
||||
cellRendererSelector: (
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
|
||||
};
|
||||
},
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
|
||||
if (
|
||||
data &&
|
||||
value &&
|
||||
node?.rowPinned &&
|
||||
data.marketTradingMode ===
|
||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
||||
) {
|
||||
return addDecimalsFormatNumber(
|
||||
value.toString(),
|
||||
data.marketDecimalPlaces
|
||||
);
|
||||
}
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'entryprice',
|
||||
headerName: t('Entry price'),
|
||||
headerClass: 'uppercase',
|
||||
cellClass: '!flex h-full items-center',
|
||||
field: 'averageEntryPrice',
|
||||
headerComponentParams: {
|
||||
template:
|
||||
'<div class="ag-cell-label-container" role="presentation">' +
|
||||
` <span>${t('Liquidation price (est)')}</span>` +
|
||||
' <span ref="eText" class="ag-header-cell-text"></span>' +
|
||||
'</div>',
|
||||
},
|
||||
cellRenderer: ({ node, data }: GroupCellRendererParams) => {
|
||||
const valueFormatted =
|
||||
data && !node?.rowPinned
|
||||
? (() => {
|
||||
const min = BigInt(data.averageEntryPrice);
|
||||
const max = BigInt(data.liquidationPrice);
|
||||
const mid = BigInt(data.markPrice);
|
||||
const range = max - min;
|
||||
return {
|
||||
low: addDecimalsFormatNumber(
|
||||
min.toString(),
|
||||
data.marketDecimalPlaces
|
||||
),
|
||||
high: addDecimalsFormatNumber(
|
||||
max.toString(),
|
||||
data.marketDecimalPlaces
|
||||
),
|
||||
value: range
|
||||
? Number(((mid - min) * BigInt(100)) / range)
|
||||
: 0,
|
||||
intent: data.lowMarginLevel ? Intent.Warning : undefined,
|
||||
};
|
||||
})()
|
||||
: undefined;
|
||||
return node.rowPinned ? (
|
||||
''
|
||||
) : (
|
||||
<ProgressBarCell valueFormatted={valueFormatted} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'leverage',
|
||||
headerName: t('Leverage'),
|
||||
headerClass: 'uppercase',
|
||||
cellClass: '!flex h-full items-center justify-center',
|
||||
field: 'currentLeverage',
|
||||
type: 'rightAligned',
|
||||
cellRendererSelector: (
|
||||
params: ICellRendererParams
|
||||
): CellRendererSelectorResult => {
|
||||
return {
|
||||
component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
|
||||
};
|
||||
},
|
||||
valueFormatter: ({
|
||||
value,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
|
||||
value === undefined ? '' : formatNumber(value.toString(), 1),
|
||||
},
|
||||
{
|
||||
colId: 'marginallocated',
|
||||
headerName: t('Margin allocated'),
|
||||
headerClass: 'uppercase',
|
||||
cellClass: '!flex h-full flex-col justify-center',
|
||||
field: 'capitalUtilisation',
|
||||
type: 'rightAligned',
|
||||
cellRenderer: ({ value, node, data }: GroupCellRendererParams) => {
|
||||
const valueFormatted =
|
||||
data && value
|
||||
? (() => {
|
||||
return {
|
||||
low: `${formatNumber(value, 2)}%`,
|
||||
high: addDecimalsFormatNumber(
|
||||
data.totalBalance,
|
||||
data.decimals
|
||||
),
|
||||
value: Number(value),
|
||||
};
|
||||
})()
|
||||
: undefined;
|
||||
return node.rowPinned ? (
|
||||
''
|
||||
) : (
|
||||
<ProgressBarCell valueFormatted={valueFormatted} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: 'realisedpnl',
|
||||
headerName: t('Realised PNL'),
|
||||
headerClass: 'uppercase',
|
||||
cellClass: '!flex h-full items-center',
|
||||
field: 'realisedPNL',
|
||||
type: 'rightAligned',
|
||||
cellClassRules: signedNumberCssClassRules,
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'realisedPNL'>) =>
|
||||
value === undefined || data === undefined
|
||||
? ''
|
||||
: addDecimalsFormatNumber(value.toString(), data.decimals),
|
||||
cellRenderer: 'PriceFlashCell',
|
||||
headerTooltip: t('P&L excludes any fees paid.'),
|
||||
},
|
||||
{
|
||||
colId: 'unrealisedpnl',
|
||||
headerName: t('Unrealised PNL'),
|
||||
headerClass: 'uppercase',
|
||||
cellClass: '!flex h-full items-center',
|
||||
field: 'unrealisedPNL',
|
||||
type: 'rightAligned',
|
||||
cellClassRules: signedNumberCssClassRules,
|
||||
valueFormatter: ({
|
||||
value,
|
||||
data,
|
||||
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
|
||||
value === undefined || data === undefined
|
||||
? ''
|
||||
: addDecimalsFormatNumber(value.toString(), data.decimals),
|
||||
cellRenderer: 'PriceFlashCell',
|
||||
},
|
||||
{
|
||||
colId: 'updated',
|
||||
headerName: t('Updated'),
|
||||
headerClass: 'uppercase',
|
||||
cellClass: '!flex h-full items-center',
|
||||
field: 'updatedAt',
|
||||
type: 'rightAligned',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return getDateTimeFormat().format(new Date(value));
|
||||
},
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
const defaultColDef = useMemo(() => {
|
||||
return {
|
||||
sortable: true,
|
||||
unSortIcon: true,
|
||||
headerClass: 'uppercase',
|
||||
cellClass: '!flex h-full items-center',
|
||||
};
|
||||
}, []);
|
||||
return { columnDefs, defaultColDef };
|
||||
};
|
||||
|
||||
export default useColumnDefinitions;
|
||||
@@ -1,43 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type CandleLiveSubscriptionVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type CandleLiveSubscription = { __typename?: 'Subscription', candles: { __typename?: 'Candle', close: string } };
|
||||
|
||||
|
||||
export const CandleLiveDocument = gql`
|
||||
subscription CandleLive($marketId: ID!) {
|
||||
candles(marketId: $marketId, interval: INTERVAL_I1H) {
|
||||
close
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useCandleLiveSubscription__
|
||||
*
|
||||
* To run a query within a React component, call `useCandleLiveSubscription` and pass it any options that fit your needs.
|
||||
* When your component renders, `useCandleLiveSubscription` 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 } = useCandleLiveSubscription({
|
||||
* variables: {
|
||||
* marketId: // value for 'marketId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCandleLiveSubscription(baseOptions: Apollo.SubscriptionHookOptions<CandleLiveSubscription, CandleLiveSubscriptionVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useSubscription<CandleLiveSubscription, CandleLiveSubscriptionVariables>(CandleLiveDocument, options);
|
||||
}
|
||||
export type CandleLiveSubscriptionHookResult = ReturnType<typeof useCandleLiveSubscription>;
|
||||
export type CandleLiveSubscriptionResult = Apollo.SubscriptionResult<CandleLiveSubscription>;
|
||||
@@ -1,147 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
getAllByRole,
|
||||
fireEvent,
|
||||
} from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import SimpleMarketList from './simple-market-list';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
useParams: () => ({}),
|
||||
}));
|
||||
|
||||
jest.mock('./simple-market-percent-change', () => jest.fn());
|
||||
|
||||
let marketsMock = [
|
||||
{
|
||||
id: 'MARKET_A',
|
||||
state: Schema.MarketState.STATE_ACTIVE,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
settlementAsset: {
|
||||
symbol: 'tUSD',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
tags: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'MARKET_B',
|
||||
state: Schema.MarketState.STATE_ACTIVE,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
settlementAsset: {
|
||||
symbol: 'ETH',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
tags: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
] as unknown as Market[];
|
||||
|
||||
const LIB = '@vegaprotocol/react-helpers';
|
||||
const useDataProvider = () => {
|
||||
return {
|
||||
data: marketsMock,
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
};
|
||||
jest.mock(LIB, () => ({
|
||||
...jest.requireActual(LIB),
|
||||
useDataProvider: jest.fn(() => useDataProvider()),
|
||||
}));
|
||||
|
||||
const mockIsTradable = jest.fn((_arg) => true);
|
||||
jest.mock('../../constants', () => ({
|
||||
...jest.requireActual('../../constants'),
|
||||
IS_MARKET_TRADABLE: jest.fn((arg) => mockIsTradable(arg)),
|
||||
}));
|
||||
|
||||
describe('SimpleMarketList', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be properly rendered with some data', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MockedProvider mocks={[]}>
|
||||
<SimpleMarketList />
|
||||
</MockedProvider>,
|
||||
{ wrapper: BrowserRouter }
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
document.querySelector('.ag-center-cols-container')
|
||||
).toBeInTheDocument();
|
||||
const container = document.querySelector('.ag-center-cols-container');
|
||||
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('click on row should be properly handled', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<MockedProvider mocks={[]}>
|
||||
<SimpleMarketList />
|
||||
</MockedProvider>,
|
||||
{ wrapper: BrowserRouter }
|
||||
);
|
||||
});
|
||||
mockIsTradable.mockClear();
|
||||
const container = document.querySelector('.ag-center-cols-container');
|
||||
const firstRow = getAllByRole(container as HTMLDivElement, 'row')[0];
|
||||
expect(firstRow).toHaveAttribute('row-id', marketsMock[0].id);
|
||||
await act(async () => {
|
||||
fireEvent.click(firstRow);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockIsTradable).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: marketsMock[0].id,
|
||||
state: Schema.MarketState.STATE_ACTIVE,
|
||||
})
|
||||
);
|
||||
expect(mockedNavigate).toHaveBeenCalledWith(
|
||||
`/trading/${marketsMock[0].id}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be properly renderer as empty', async () => {
|
||||
marketsMock = [];
|
||||
await act(async () => {
|
||||
render(
|
||||
<MockedProvider mocks={[]}>
|
||||
<SimpleMarketList />
|
||||
</MockedProvider>,
|
||||
{ wrapper: BrowserRouter }
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No data to display')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import {
|
||||
useScreenDimensions,
|
||||
useDataProvider,
|
||||
useYesterday,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import useMarketsFilterData from './use-markets-filter-data';
|
||||
import useColumnDefinitions from './use-column-definitions';
|
||||
import SimpleMarketToolbar from './simple-market-toolbar';
|
||||
import { IS_MARKET_TRADABLE } from '../../constants';
|
||||
import { ConsoleLiteGrid } from '../console-lite-grid';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { marketsWithCandlesProvider } from '@vegaprotocol/market-list';
|
||||
|
||||
export type MarketWithPercentChange = Market & {
|
||||
percentChange?: number | '-';
|
||||
};
|
||||
|
||||
export type RouterParams = Partial<{
|
||||
product: string;
|
||||
asset: string;
|
||||
state: string;
|
||||
}>;
|
||||
|
||||
const SimpleMarketList = () => {
|
||||
const { isMobile } = useScreenDimensions();
|
||||
const navigate = useNavigate();
|
||||
const params = useParams<RouterParams>();
|
||||
const statusesRef = useRef<Record<string, Schema.MarketState | ''>>({});
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
|
||||
const yesterday = useYesterday();
|
||||
const variables = useMemo(() => {
|
||||
return {
|
||||
since: new Date(yesterday).toISOString(),
|
||||
interval: Schema.Interval.INTERVAL_I1H,
|
||||
};
|
||||
}, [yesterday]);
|
||||
const { data, error, loading } = useDataProvider({
|
||||
dataProvider: marketsWithCandlesProvider,
|
||||
variables,
|
||||
skipUpdates: true,
|
||||
});
|
||||
const localData = useMarketsFilterData(data, params);
|
||||
|
||||
const handleOnGridReady = useCallback(() => {
|
||||
gridRef.current?.api?.sizeColumnsToFit();
|
||||
}, [gridRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const statuses: Record<string, Schema.MarketState | ''> = {};
|
||||
data?.forEach((market) => {
|
||||
statuses[market.id] = market.state || '';
|
||||
});
|
||||
statusesRef.current = statuses;
|
||||
}, [data, statusesRef]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', handleOnGridReady);
|
||||
return () => window.removeEventListener('resize', handleOnGridReady);
|
||||
}, [handleOnGridReady]);
|
||||
|
||||
const { columnDefs, defaultColDef } = useColumnDefinitions({ isMobile });
|
||||
|
||||
const handleRowClicked = useCallback(
|
||||
({ data }: { data: Market }) => {
|
||||
if (IS_MARKET_TRADABLE(data)) {
|
||||
navigate(`/trading/${data.id}`);
|
||||
}
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full p-4 md:p-6 grid grid-rows-[min-content,1fr]">
|
||||
<SimpleMarketToolbar data={data || []} />
|
||||
<AsyncRenderer loading={loading} error={error} data={localData}>
|
||||
<ConsoleLiteGrid<MarketWithPercentChange>
|
||||
classNamesParam="mb-32 min-h-[300px]"
|
||||
columnDefs={columnDefs}
|
||||
rowData={localData}
|
||||
defaultColDef={defaultColDef}
|
||||
handleRowClicked={handleRowClicked}
|
||||
getRowId={({ data }) => data.id}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleMarketList;
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import MarketNameRenderer from './simple-market-renderer';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
|
||||
describe('SimpleMarketRenderer', () => {
|
||||
const market = {
|
||||
id: 'MARKET_A',
|
||||
state: Schema.MarketState.STATE_ACTIVE,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'MARKET_A_CODE',
|
||||
name: 'MARKET_A_NAME',
|
||||
product: {
|
||||
quoteName: 'MARKET_A_QUOTE_NAME',
|
||||
},
|
||||
metadata: {
|
||||
tags: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Market;
|
||||
|
||||
it('should properly render not mobile', () => {
|
||||
render(<MarketNameRenderer market={market} isMobile={false} />);
|
||||
expect(screen.getByText('MARKET_A_NAME')).toBeInTheDocument();
|
||||
expect(screen.getByText('MARKET_A_QUOTE_NAME')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should properly render mobile', () => {
|
||||
render(<MarketNameRenderer market={market} isMobile={true} />);
|
||||
expect(screen.getByText('MARKET_A_CODE')).toBeInTheDocument();
|
||||
expect(screen.getByText('MARKET_A_QUOTE_NAME')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
-214
@@ -1,214 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
useLocation,
|
||||
useRoutes,
|
||||
BrowserRouter,
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
act,
|
||||
getAllByText,
|
||||
} from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import SimpleMarketToolbar from './simple-market-toolbar';
|
||||
import { markets as filterData } from './mocks/market-filters.json';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actualRouter = jest.requireActual('react-router-dom');
|
||||
return {
|
||||
...actualRouter,
|
||||
useNavigate: () => mockedNavigate,
|
||||
useParams: jest.fn(() => actualRouter.useParams()),
|
||||
};
|
||||
});
|
||||
|
||||
describe('SimpleMarketToolbar', () => {
|
||||
const WrappedCompForTest = () => {
|
||||
const routes = useRoutes([
|
||||
{
|
||||
path: '/',
|
||||
element: <SimpleMarketToolbar data={filterData as Market[]} />,
|
||||
},
|
||||
{
|
||||
path: 'markets',
|
||||
children: [
|
||||
{
|
||||
path: `:state`,
|
||||
element: <SimpleMarketToolbar data={filterData as Market[]} />,
|
||||
children: [
|
||||
{
|
||||
path: `:product`,
|
||||
element: <SimpleMarketToolbar data={filterData as Market[]} />,
|
||||
children: [
|
||||
{
|
||||
path: `:asset`,
|
||||
element: (
|
||||
<SimpleMarketToolbar data={filterData as Market[]} />
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
element: <SimpleMarketToolbar data={filterData as Market[]} />,
|
||||
},
|
||||
]);
|
||||
const location = useLocation();
|
||||
return (
|
||||
<>
|
||||
{routes}
|
||||
<div data-testid="location-display">{location.pathname}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be properly rendered', async () => {
|
||||
render(
|
||||
<MockedProvider mocks={[]} addTypename={false}>
|
||||
<WrappedCompForTest />
|
||||
</MockedProvider>,
|
||||
{ wrapper: BrowserRouter }
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Future')).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByText('Future'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('market-products-menu').children).toHaveLength(
|
||||
3
|
||||
);
|
||||
expect(screen.getByTestId('market-assets-menu').children).toHaveLength(6);
|
||||
});
|
||||
fireEvent.click(screen.getByTestId('state-trigger'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument();
|
||||
expect(screen.getByRole('menu').children).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
|
||||
it('navigation should work well', async () => {
|
||||
render(
|
||||
<MockedProvider mocks={[]} addTypename={false}>
|
||||
<WrappedCompForTest />
|
||||
</MockedProvider>,
|
||||
{ wrapper: BrowserRouter }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Future')).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByText('Future'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(
|
||||
`/markets/${Schema.MarketState.STATE_ACTIVE}/Future`
|
||||
);
|
||||
});
|
||||
|
||||
fireEvent.click(
|
||||
screen
|
||||
.getByTestId('market-assets-menu')
|
||||
.children[5].querySelector('a') as HTMLAnchorElement
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('location-display')).toHaveTextContent(
|
||||
`/markets/${Schema.MarketState.STATE_ACTIVE}/Future/tEURO`
|
||||
);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('state-trigger'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument();
|
||||
});
|
||||
const menu = screen.getByRole('menu');
|
||||
const pending = getAllByText(menu, 'Pending')[0];
|
||||
await act(() => {
|
||||
fireEvent.click(pending);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toHaveBeenCalledWith(
|
||||
`/markets/${Schema.MarketState.STATE_PENDING}/Future/tEURO`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('stateChange callback should work well', async () => {
|
||||
(useParams as jest.Mock).mockImplementation(() => ({
|
||||
asset: 'asset1',
|
||||
product: 'product1',
|
||||
state: 'state1',
|
||||
}));
|
||||
render(
|
||||
<MockedProvider mocks={[]} addTypename={false}>
|
||||
<SimpleMarketToolbar data={filterData as Market[]} />
|
||||
</MockedProvider>,
|
||||
{ wrapper: BrowserRouter }
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('state-trigger'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument();
|
||||
});
|
||||
const menu = screen.getByRole('menu');
|
||||
const suspended = getAllByText(menu, 'Suspended')[0];
|
||||
fireEvent.click(suspended);
|
||||
|
||||
expect(mockedNavigate).toHaveBeenCalledWith(
|
||||
`/markets/${Schema.MarketState.STATE_SUSPENDED}/product1/asset1`
|
||||
);
|
||||
});
|
||||
|
||||
it('stateChange callback should call navigate with url with state only', async () => {
|
||||
(useParams as jest.Mock).mockImplementation(() => ({}));
|
||||
render(
|
||||
<MockedProvider mocks={[]} addTypename={false}>
|
||||
<SimpleMarketToolbar data={filterData as Market[]} />
|
||||
</MockedProvider>,
|
||||
{ wrapper: BrowserRouter }
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('state-trigger'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument();
|
||||
});
|
||||
const menu = screen.getByRole('menu');
|
||||
const closed = getAllByText(menu, 'Closed')[0];
|
||||
fireEvent.click(closed);
|
||||
|
||||
expect(mockedNavigate).toHaveBeenCalledWith(
|
||||
`/markets/${Schema.MarketState.STATE_CLOSED}`
|
||||
);
|
||||
});
|
||||
|
||||
it('stateChange callback should call navigate with no asset', async () => {
|
||||
(useParams as jest.Mock).mockImplementation(() => ({
|
||||
asset: 'all',
|
||||
state: 'state1',
|
||||
}));
|
||||
render(
|
||||
<MockedProvider mocks={[]} addTypename={false}>
|
||||
<SimpleMarketToolbar data={filterData as Market[]} />
|
||||
</MockedProvider>,
|
||||
{ wrapper: BrowserRouter }
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('state-trigger'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const menu = screen.getByRole('menu');
|
||||
const active = getAllByText(menu, 'Active')[0];
|
||||
fireEvent.click(active);
|
||||
expect(mockedNavigate).toHaveBeenCalledWith('/markets');
|
||||
});
|
||||
});
|
||||
@@ -1,165 +0,0 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useNavigate, useParams, Link } from 'react-router-dom';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
} from '@radix-ui/react-dropdown-menu';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItemIndicator,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { HorizontalMenu } from '../horizontal-menu';
|
||||
import type { HorizontalMenuItem } from '../horizontal-menu';
|
||||
import * as constants from './constants';
|
||||
import { useMarketFilters } from '../../hooks/use-markets-filter';
|
||||
|
||||
interface Props {
|
||||
data: Market[];
|
||||
}
|
||||
|
||||
const SimpleMarketToolbar = ({ data }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const { products, assetsPerProduct } = useMarketFilters(data);
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const onStateChange = useCallback(
|
||||
(activeState: string) => {
|
||||
const asset =
|
||||
params.asset && params.asset !== 'all' ? `/${params.asset}` : '';
|
||||
const product = params.product ? `/${params.product}` : '';
|
||||
const state =
|
||||
activeState !== Schema.MarketState.STATE_ACTIVE || product
|
||||
? `/${activeState}`
|
||||
: '';
|
||||
navigate(`/markets${state}${product}${asset}`);
|
||||
},
|
||||
[params, navigate]
|
||||
);
|
||||
|
||||
const productItems = useMemo(() => {
|
||||
const currentState = params.state || Schema.MarketState.STATE_ACTIVE;
|
||||
const noStateSkip = currentState !== Schema.MarketState.STATE_ACTIVE;
|
||||
const items: HorizontalMenuItem[] = [
|
||||
{
|
||||
...constants.ALL_PRODUCTS_ITEM,
|
||||
url: `/markets${noStateSkip ? '/' + currentState : ''}`,
|
||||
},
|
||||
];
|
||||
products.forEach((product) =>
|
||||
items.push({
|
||||
name: product,
|
||||
id: product,
|
||||
url: `/markets/${currentState}/${product}`,
|
||||
})
|
||||
);
|
||||
return items;
|
||||
}, [params, products]);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-full mb-2 md:mb-8 font-alpha">
|
||||
<HorizontalMenu
|
||||
active={params.product || constants.ALL_PRODUCTS_ITEM.id}
|
||||
items={productItems}
|
||||
data-testid="market-products-menu"
|
||||
aria-label={t('Product type')}
|
||||
/>
|
||||
<div className="grid gap-4 pb-2 mt-2 md:mt-6 md:grid-cols-[min-content,min-content,1fr]">
|
||||
<div className="pb-2">
|
||||
<DropdownMenu open={isOpen} onOpenChange={(open) => setOpen(open)}>
|
||||
<DropdownMenuTrigger
|
||||
className="mr-2 w-auto text-capMenu text-black dark:text-white"
|
||||
data-testid="state-trigger"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<div className="w-full justify-between uppercase inline-flex items-center justify-center box-border">
|
||||
{constants.STATES_FILTER.find(
|
||||
(state) =>
|
||||
state.value === params.state ||
|
||||
(!params.state &&
|
||||
state.value === Schema.MarketState.STATE_ACTIVE)
|
||||
)?.text || params.state}
|
||||
<Icon
|
||||
name={IconNames.ARROW_DOWN}
|
||||
className={classNames(
|
||||
'fill-current ml-2 transition-transform',
|
||||
{
|
||||
'rotate-180': isOpen,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{constants.STATES_FILTER.map(({ value, text }) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
className="uppercase text-ui"
|
||||
key={value}
|
||||
checked={
|
||||
value === params.state ||
|
||||
(!params.state && value === Schema.MarketState.STATE_ACTIVE)
|
||||
}
|
||||
onCheckedChange={() => onStateChange(value)}
|
||||
>
|
||||
{text}
|
||||
<DropdownMenuItemIndicator>
|
||||
<Icon name="tick" />
|
||||
</DropdownMenuItemIndicator>
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="hidden md:block text-deemphasise dark:text-midGrey">
|
||||
|
|
||||
</div>
|
||||
{params.product && (
|
||||
<ul
|
||||
className="md:gap-x-6 gap-x-4 gap-y-1 pb-2 md:ml-2 flex flex-wrap"
|
||||
data-testid="market-assets-menu"
|
||||
aria-label={t('Asset on the market')}
|
||||
>
|
||||
<li key="all">
|
||||
<Link
|
||||
to={`/markets/${params.state}/${params.product}`}
|
||||
className={classNames('uppercase pl-0 md:pl-2 text-capMenu', {
|
||||
'text-deemphasise dark:text-midGrey':
|
||||
params.asset && params.asset !== 'all',
|
||||
'active text-black dark:text-white':
|
||||
!params.asset || params.asset === 'all',
|
||||
})}
|
||||
aria-label={t('All assets')}
|
||||
>
|
||||
{t('All')}
|
||||
</Link>
|
||||
</li>
|
||||
{assetsPerProduct[params?.product]?.map((asset) => (
|
||||
<li key={asset}>
|
||||
<Link
|
||||
to={`/markets/${params.state}/${params.product}/${asset}`}
|
||||
className={classNames('uppercase text-capMenu', {
|
||||
'text-deemphasise dark:text-midGrey':
|
||||
params.asset !== asset,
|
||||
'active text-black dark:text-white': params.asset === asset,
|
||||
})}
|
||||
aria-label={asset}
|
||||
>
|
||||
{asset}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleMarketToolbar;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user