Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba0af64bef | ||
|
|
be4fb779ef | ||
|
|
f2eedc434d | ||
|
|
fde49e5446 | ||
|
|
45283161aa | ||
|
|
4eacbbba1c | ||
|
|
375d447fa8 | ||
|
|
1dd97a2bce | ||
|
|
e9188344cf | ||
|
|
16c18b972d | ||
|
|
d4a1a9b193 | ||
|
|
376c5241a8 | ||
|
|
8e6d8517cf | ||
|
|
5f9ec222c1 | ||
|
|
8954c41c0a | ||
|
|
4414ab4f47 | ||
|
|
d1d4bacc68 | ||
|
|
f476688c7f | ||
|
|
8845222700 | ||
|
|
e64c464091 | ||
|
|
b2c2d0d7d6 | ||
|
|
1fb61d313c | ||
|
|
ccf5ff632c | ||
|
|
54d6aaf56f | ||
|
|
736b262947 |
+2
-1
@@ -51,7 +51,8 @@
|
|||||||
"ul": ["list"],
|
"ul": ["list"],
|
||||||
"ol": ["list"]
|
"ol": ["list"]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"no-console": ["error", { "allow": ["warn", "error"] }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: Feature Epic
|
||||||
|
about:
|
||||||
|
A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
|
||||||
|
title: 'Epic: '
|
||||||
|
labels: feature-epic
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Feature
|
||||||
|
|
||||||
|
<Name>
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
- [ ] Define high level requirements
|
||||||
|
- [ ] Create basic mockups
|
||||||
|
- [ ] Update "API Requirements" in core spec
|
||||||
|
- [ ] Update "User-Interface Spec" in relevant front end repo
|
||||||
|
- [ ] Create detailed user stories using normal template
|
||||||
|
|
||||||
|
## High Level Requirements
|
||||||
|
|
||||||
|
## Basic Mockups
|
||||||
|
|
||||||
|
## Link to API Requirements in Core spec
|
||||||
|
|
||||||
|
## Link to User Interface Specs
|
||||||
|
|
||||||
|
## Linked User Stories
|
||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
after-release:
|
after-release:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
timeout-minutes: 30
|
timeout-minutes: 45
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -30,18 +30,20 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Wait for publish to complete
|
||||||
|
uses: lewagon/wait-on-check-action@v1.3.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.release.tag_name }}
|
||||||
|
check-name: '(CD) publish dist / trading'
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
wait-interval: 10
|
||||||
|
|
||||||
- name: resolve ipfs hashes for release
|
- name: resolve ipfs hashes for release
|
||||||
run: |
|
run: |
|
||||||
echo "Name: ${{ github.event.release.name }}"
|
echo "Name: ${{ github.event.release.name }}"
|
||||||
echo "Description: ${{ github.event.release.body }}"
|
echo "Description: ${{ github.event.release.body }}"
|
||||||
echo "Tag: ${{ github.event.release.tag_name }}"
|
echo "Tag: ${{ github.event.release.tag_name }}"
|
||||||
commit="$(git rev-list -n 1 ${{ github.event.release.tag_name }})"
|
docker run --rm vegaprotocol/trading:mainnet cat /ipfs-hash > ipfs-hash
|
||||||
echo "Commit: $commit"
|
|
||||||
until docker pull vegaprotocol/trading:$commit; do
|
|
||||||
echo "Image not pushed yet, waiting 60 seconds"
|
|
||||||
sleep 60
|
|
||||||
done
|
|
||||||
docker run --rm vegaprotocol/trading:$commit cat /ipfs-hash > ipfs-hash
|
|
||||||
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
|
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
|
||||||
tar -xzf kubo.tgz
|
tar -xzf kubo.tgz
|
||||||
export PATH="$PATH:$PWD/kubo"
|
export PATH="$PATH:$PWD/kubo"
|
||||||
|
|||||||
@@ -120,6 +120,15 @@ jobs:
|
|||||||
preview_explorer: ${{ env.PREVIEW_EXPLORER }}
|
preview_explorer: ${{ env.PREVIEW_EXPLORER }}
|
||||||
preview_tools: ${{ env.PREVIEW_TOOLS }}
|
preview_tools: ${{ env.PREVIEW_TOOLS }}
|
||||||
|
|
||||||
|
console-e2e:
|
||||||
|
needs: lint-test-build
|
||||||
|
name: '(CI) console python'
|
||||||
|
uses: ./.github/workflows/console-test-run.yml
|
||||||
|
secrets: inherit
|
||||||
|
if: ${{ contains(fromJSON(needs.lint-test-build.outputs.projects), 'trading') && github.event_name == 'pull_request' }}
|
||||||
|
with:
|
||||||
|
github-sha: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
cypress:
|
cypress:
|
||||||
needs: lint-test-build
|
needs: lint-test-build
|
||||||
name: '(CI) cypress'
|
name: '(CI) cypress'
|
||||||
|
|||||||
@@ -1,55 +1,138 @@
|
|||||||
name: console-test-run
|
name: (CI) Console tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
github-sha:
|
github-sha:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
console-test:
|
run-tests:
|
||||||
timeout-minutes: 5
|
name: run-tests
|
||||||
runs-on: self-hosted-runner
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 25
|
||||||
steps:
|
steps:
|
||||||
|
#----------------------------------------------
|
||||||
|
# check-out frontend-monorepo
|
||||||
|
#----------------------------------------------
|
||||||
|
- name: Checkout console test repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.github-sha }}
|
||||||
|
#----------------------------------------------
|
||||||
|
# cache node modules
|
||||||
|
#----------------------------------------------
|
||||||
|
- name: Cache node modules
|
||||||
|
id: cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
|
||||||
|
# comment out "restore-keys" if you need to rebuild yarn from 0
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cache-node-modules-
|
||||||
|
#----------------------------------------------
|
||||||
|
# setup node
|
||||||
|
#----------------------------------------------
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
|
||||||
|
cache: yarn
|
||||||
|
#----------------------------------------------
|
||||||
|
# install deps if cache missing
|
||||||
|
#----------------------------------------------
|
||||||
|
- name: yarn install
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
run: yarn install --pure-lockfile
|
||||||
|
#----------------------------------------------
|
||||||
|
# build trading
|
||||||
|
#----------------------------------------------
|
||||||
|
- name: Build affected spec
|
||||||
|
run: |
|
||||||
|
yarn env-cmd -f ./apps/trading/.env.stagnet1 yarn nx export trading
|
||||||
|
#----------------------------------------------
|
||||||
|
# run trading server
|
||||||
|
#----------------------------------------------
|
||||||
|
- name: Run trading server
|
||||||
|
run: |
|
||||||
|
docker run -d -p 80:4200 -v $PWD/docker/nginx.conf:/etc/nginx/conf.d/default.conf -v $PWD/dist/apps/trading/exported:/usr/share/nginx/html nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
|
||||||
|
sleep 5
|
||||||
|
docker ps
|
||||||
|
#----------------------------------------------
|
||||||
|
# check if container persists between runs
|
||||||
|
#----------------------------------------------
|
||||||
|
- name: Check server
|
||||||
|
run: |
|
||||||
|
docker ps
|
||||||
|
#----------------------------------------------
|
||||||
|
# check-out tests repo
|
||||||
|
#----------------------------------------------
|
||||||
- name: Checkout console test repo
|
- name: Checkout console test repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
repository: vegaprotocol/console-test
|
repository: vegaprotocol/console-test
|
||||||
path: './console-test'
|
path: './console-test'
|
||||||
|
#----------------------------------------------
|
||||||
- name: Set up Python
|
# set-up python
|
||||||
|
#----------------------------------------------
|
||||||
|
- name: Set up python
|
||||||
|
id: setup-python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.10.11'
|
python-version: '3.10.11'
|
||||||
|
#----------------------------------------------
|
||||||
|
# ----- install & configure poetry -----
|
||||||
|
#----------------------------------------------
|
||||||
- name: Install Poetry
|
- name: Install Poetry
|
||||||
uses: snok/install-poetry@v1
|
uses: snok/install-poetry@v1
|
||||||
|
with:
|
||||||
|
virtualenvs-create: true
|
||||||
|
virtualenvs-in-project: true
|
||||||
|
virtualenvs-path: console-test/.venv
|
||||||
|
#----------------------------------------------
|
||||||
|
# load cached venv if cache exists
|
||||||
|
#----------------------------------------------
|
||||||
|
- name: Load cached venv
|
||||||
|
id: cached-poetry-dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: console-test/.venv
|
||||||
|
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
||||||
|
#----------------------------------------------
|
||||||
|
# install dependencies if cache does not exist
|
||||||
|
#----------------------------------------------
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
|
||||||
poetry install --no-root
|
|
||||||
working-directory: ./console-test
|
working-directory: ./console-test
|
||||||
|
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||||
- name: load Binaries
|
run: poetry install --no-interaction --no-root
|
||||||
run: |
|
#----------------------------------------------
|
||||||
poetry run python -m vega_sim.tools.load_binaries
|
# install vega binaries
|
||||||
|
#----------------------------------------------
|
||||||
|
- name: Install vega binaries
|
||||||
working-directory: ./console-test
|
working-directory: ./console-test
|
||||||
|
run: poetry run python -m vega_sim.tools.load_binaries --force
|
||||||
- name: pull console
|
#----------------------------------------------
|
||||||
run: |
|
# install playwright
|
||||||
poetry run docker pull ghcr.io/vegaprotocol/frontend/trading:${{ inputs.github-sha }}
|
#----------------------------------------------
|
||||||
|
|
||||||
- name: Update container_name in config.py
|
|
||||||
run: |
|
|
||||||
sed -i "s/container_name = \".*\"/container_name = \"vegaprotocol\/frontend\/trading:${{ inputs.github-sha }}\"/g" config.py
|
|
||||||
|
|
||||||
- name: install playwright
|
- name: install playwright
|
||||||
run: poetry run playwright install
|
run: poetry run playwright install
|
||||||
working-directory: ./console-test
|
working-directory: ./console-test
|
||||||
|
#----------------------------------------------
|
||||||
- name: run tests
|
# run tests
|
||||||
run: poetry run pytest --numprocesses auto
|
#----------------------------------------------
|
||||||
|
- name: Run tests
|
||||||
working-directory: ./console-test
|
working-directory: ./console-test
|
||||||
|
run: poetry run pytest --numprocesses auto
|
||||||
|
- name: Check files
|
||||||
|
run: |
|
||||||
|
ls -al .
|
||||||
|
ls -al console-test
|
||||||
|
#----------------------------------------------
|
||||||
|
# upload traces
|
||||||
|
#----------------------------------------------
|
||||||
- name: Upload Playwright Trace
|
- name: Upload Playwright Trace
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ jobs:
|
|||||||
echo IS_TESTNET_RELEASE=false >> $GITHUB_ENV
|
echo IS_TESTNET_RELEASE=false >> $GITHUB_ENV
|
||||||
echo IS_IPFS_RELEASE=false >> $GITHUB_ENV
|
echo IS_IPFS_RELEASE=false >> $GITHUB_ENV
|
||||||
echo IS_S3_RELEASE=false >> $GITHUB_ENV
|
echo IS_S3_RELEASE=false >> $GITHUB_ENV
|
||||||
|
echo IS_DEV_IMAGE=false >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Is dev image
|
||||||
|
if: ${{ contains(github.ref, 'develop') && github.event_name == 'push' && matrix.app == 'trading' }}
|
||||||
|
run: |
|
||||||
|
echo IS_DEV_IMAGE=true >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Is PR
|
- name: Is PR
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
@@ -36,7 +42,7 @@ jobs:
|
|||||||
echo IS_PR=true >> $GITHUB_ENV
|
echo IS_PR=true >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Is mainnet release
|
- name: Is mainnet release
|
||||||
if: ${{ contains(github.ref, 'release/mainnnet') && !contains(github.ref, 'mirror') }}
|
if: ${{ contains(github.ref, 'release/mainnet') && !contains(github.ref, 'mirror') }}
|
||||||
run: |
|
run: |
|
||||||
echo IS_MAINNET_RELEASE=true >> $GITHUB_ENV
|
echo IS_MAINNET_RELEASE=true >> $GITHUB_ENV
|
||||||
|
|
||||||
@@ -75,7 +81,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to the Container registry (docker hub)
|
- name: Log in to the Container registry (docker hub)
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
|
if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@@ -118,8 +124,12 @@ jobs:
|
|||||||
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
|
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
|
||||||
NODE_ENV=production yarn nx run ui-toolkit:build-storybook
|
NODE_ENV=production yarn nx run ui-toolkit:build-storybook
|
||||||
DIST_LOCATION=dist/storybook/ui-toolkit
|
DIST_LOCATION=dist/storybook/ui-toolkit
|
||||||
|
elif [ "${{ matrix.app }}" = "static" ]; then
|
||||||
|
yarn nx build static || (yarn install && yarn nx build static)
|
||||||
else
|
else
|
||||||
$envCmd yarn nx build ${{ matrix.app }} || (yarn install && $envCmd yarn nx build ${{ matrix.app }})
|
$envCmd yarn nx build ${{ matrix.app }} || (yarn install && $envCmd yarn nx build ${{ matrix.app }})
|
||||||
|
fi
|
||||||
|
if [[ -z "$DIST_LOCATION" ]]; then
|
||||||
DIST_LOCATION=dist/apps/${{ matrix.app }}
|
DIST_LOCATION=dist/apps/${{ matrix.app }}
|
||||||
fi
|
fi
|
||||||
mv $DIST_LOCATION dist-result
|
mv $DIST_LOCATION dist-result
|
||||||
@@ -169,7 +179,7 @@ jobs:
|
|||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
id: dockerhub-push
|
id: dockerhub-push
|
||||||
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
|
if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/node-outside-docker.Dockerfile
|
file: docker/node-outside-docker.Dockerfile
|
||||||
@@ -179,7 +189,7 @@ jobs:
|
|||||||
ENV_NAME=${{ env.ENV_NAME }}
|
ENV_NAME=${{ env.ENV_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
|
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
|
||||||
vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || '' }}
|
vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || env.IS_DEV_IMAGE == 'true' && 'develop' || '' }}
|
||||||
|
|
||||||
- name: Publish dist as docker image (ghcr - retry)
|
- name: Publish dist as docker image (ghcr - retry)
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
Icon,
|
Icon,
|
||||||
Button,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import type { Dispatch, SetStateAction } from 'react';
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
import { FilterLabel } from './tx-filter-label';
|
import { FilterLabel } from './tx-filter-label';
|
||||||
@@ -100,15 +99,13 @@ export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
|
|||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
modal={false}
|
modal={false}
|
||||||
trigger={
|
trigger={
|
||||||
<DropdownMenuTrigger className="ml-0">
|
<DropdownMenuTrigger>
|
||||||
<Button size="xs" data-testid="filter-trigger">
|
<FilterLabel filters={filters} />
|
||||||
<FilterLabel filters={filters} />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
{filters.size > 0 ? null : (
|
{filters.size > 1 ? null : (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
|
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const TxsListNavigation = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
disabled={!hasMoreTxs || loading}
|
disabled={!hasMoreTxs}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
nextPage();
|
nextPage();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -86,14 +86,18 @@ describe('Txs infinite list item', () => {
|
|||||||
render(
|
render(
|
||||||
<MockedProvider>
|
<MockedProvider>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<TxsInfiniteListItem
|
<table>
|
||||||
type="testType"
|
<tbody>
|
||||||
submitter="testPubKey"
|
<TxsInfiniteListItem
|
||||||
hash="testTxHash"
|
type="testType"
|
||||||
block="1"
|
submitter="testPubKey"
|
||||||
code={0}
|
hash="testTxHash"
|
||||||
command={{}}
|
block="1"
|
||||||
/>
|
code={0}
|
||||||
|
command={{}}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ const DEFAULT_TRUNCATE_LENGTH = 7;
|
|||||||
|
|
||||||
export function getIdTruncateLength(screen: Screen): number {
|
export function getIdTruncateLength(screen: Screen): number {
|
||||||
if (['xxxl', 'xxl'].includes(screen)) {
|
if (['xxxl', 'xxl'].includes(screen)) {
|
||||||
return 64;
|
|
||||||
} else if (['xl', 'lg', 'md'].includes(screen)) {
|
|
||||||
return 32;
|
return 32;
|
||||||
|
} else if (['xl', 'lg', 'md'].includes(screen)) {
|
||||||
|
return 16;
|
||||||
}
|
}
|
||||||
return DEFAULT_TRUNCATE_LENGTH;
|
return DEFAULT_TRUNCATE_LENGTH;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,17 @@ import type { BlockExplorerTransactionResult } from '../../routes/types/block-ex
|
|||||||
import { Side } from '@vegaprotocol/types';
|
import { Side } from '@vegaprotocol/types';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
|
const generateHash = (): string =>
|
||||||
|
Array.from(
|
||||||
|
{ length: 64 },
|
||||||
|
() => '0123456789ABCDEF'[Math.floor(Math.random() * 16)]
|
||||||
|
).join('');
|
||||||
|
|
||||||
const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
|
const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
|
||||||
return Array.from(Array(number)).map((_) => ({
|
return Array.from(Array(number)).map((_) => ({
|
||||||
block: '87901',
|
block: '87901',
|
||||||
index: 2,
|
index: 2,
|
||||||
hash: '0F8B98DA0923A50786B852D9CA11E051CACC4C733E1DB93D535C7D81DBD10F6F',
|
hash: generateHash(),
|
||||||
submitter:
|
submitter:
|
||||||
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
|
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
|
||||||
type: 'Submit Order',
|
type: 'Submit Order',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Table, TableRow } from '../table';
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||||
import type { BlockExplorerTransactions } from '../../routes/types/block-explorer-response';
|
import type { BlockExplorerTransactions } from '../../routes/types/block-explorer-response';
|
||||||
import { getTxsDataUrl } from '../../hooks/use-txs-data';
|
import { getTxsDataUrl } from '../../hooks/get-txs-data-url';
|
||||||
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
|
||||||
import EmptyList from '../empty-list/empty-list';
|
import EmptyList from '../empty-list/empty-list';
|
||||||
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||||
@@ -14,7 +14,7 @@ interface TxsPerBlockProps {
|
|||||||
|
|
||||||
export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
|
export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
|
||||||
const filters = `filters[block.height]=${blockHeight}`;
|
const filters = `filters[block.height]=${blockHeight}`;
|
||||||
const url = getTxsDataUrl({ limit: txCount.toString(), filters });
|
const url = getTxsDataUrl({ filters, count: txCount });
|
||||||
const {
|
const {
|
||||||
state: { data, loading, error },
|
state: { data, loading, error },
|
||||||
} = useFetch<BlockExplorerTransactions>(url);
|
} = useFetch<BlockExplorerTransactions>(url);
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { DATA_SOURCES } from '../config';
|
||||||
|
|
||||||
|
type IGetTxsDataFirstPage = {
|
||||||
|
baseUrl?: string;
|
||||||
|
count?: number;
|
||||||
|
party?: string;
|
||||||
|
filters?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IGetTxsDataPrevious extends IGetTxsDataFirstPage {
|
||||||
|
before: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGetTxsDataNext extends IGetTxsDataFirstPage {
|
||||||
|
after: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type IGetTxsDataUrl =
|
||||||
|
| IGetTxsDataPrevious
|
||||||
|
| IGetTxsDataNext
|
||||||
|
| IGetTxsDataFirstPage;
|
||||||
|
|
||||||
|
export const BE_TXS_PER_REQUEST = 25;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properly encodes the filters and parameters for a request to the block explorer
|
||||||
|
* API for transactions. As the API uses a slightly less common format for encoding
|
||||||
|
* filters, some of it is more manual than you might expect.
|
||||||
|
*
|
||||||
|
* @param params An object containing the pagination and filters
|
||||||
|
* @returns string URL to call
|
||||||
|
*/
|
||||||
|
export const getTxsDataUrl = (params: IGetTxsDataUrl) => {
|
||||||
|
const baseUrl =
|
||||||
|
params.baseUrl || `${DATA_SOURCES.blockExplorerUrl}/transactions`;
|
||||||
|
const url = new URL(baseUrl);
|
||||||
|
const count = `${params.count || BE_TXS_PER_REQUEST}`;
|
||||||
|
|
||||||
|
if ('before' in params && params.before?.length > 0) {
|
||||||
|
url.searchParams.append('last', count);
|
||||||
|
url.searchParams.append('before', params.before);
|
||||||
|
} else if ('after' in params && params.after?.length > 0) {
|
||||||
|
url.searchParams.append('first', count);
|
||||||
|
url.searchParams.append('after', params.after);
|
||||||
|
} else {
|
||||||
|
url.searchParams.append('first', count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hacky fix for param as array
|
||||||
|
let urlAsString = url.toString();
|
||||||
|
if (params.filters && params.filters?.length > 0) {
|
||||||
|
urlAsString += '&' + params.filters.replaceAll(' ', '%20');
|
||||||
|
}
|
||||||
|
if (params.party && params.party?.length > 0) {
|
||||||
|
urlAsString += `&filters[tx.submitter]=${params.party}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlAsString;
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { getTxsDataUrl } from './get-txs-data-url'; // import the function to be tested
|
||||||
|
|
||||||
|
describe('getTxsDataUrl', () => {
|
||||||
|
it('should return the correct URL without filters and party', () => {
|
||||||
|
const params = {
|
||||||
|
count: 10,
|
||||||
|
baseUrl: 'https://example.com/transactions',
|
||||||
|
};
|
||||||
|
const expectedUrl = 'https://example.com/transactions?first=10';
|
||||||
|
|
||||||
|
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct URL with "before" in params', () => {
|
||||||
|
const params = {
|
||||||
|
count: 5,
|
||||||
|
before: '100.1',
|
||||||
|
baseUrl: 'https://example.com/transactions',
|
||||||
|
};
|
||||||
|
const expectedUrl = 'https://example.com/transactions?last=5&before=100.1';
|
||||||
|
|
||||||
|
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct URL with "after" in params', () => {
|
||||||
|
const params = {
|
||||||
|
count: 5,
|
||||||
|
after: '222.1',
|
||||||
|
baseUrl: 'https://example.com/transactions',
|
||||||
|
};
|
||||||
|
const expectedUrl = 'https://example.com/transactions?first=5&after=222.1';
|
||||||
|
|
||||||
|
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct URL with filters and party', () => {
|
||||||
|
const params = {
|
||||||
|
count: 10,
|
||||||
|
filters: 'filters[cmd.type]=Made Up Transaction',
|
||||||
|
party: '1234',
|
||||||
|
baseUrl: 'https://example.com/transactions',
|
||||||
|
};
|
||||||
|
const expectedUrl =
|
||||||
|
'https://example.com/transactions?first=10&filters[cmd.type]=Made%20Up%20Transaction&filters[tx.submitter]=1234';
|
||||||
|
|
||||||
|
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,130 +1,141 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import type { URLSearchParamsInit } from 'react-router-dom';
|
||||||
|
import { useCallback } from 'react';
|
||||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||||
import type {
|
import type {
|
||||||
BlockExplorerTransactionResult,
|
BlockExplorerTransactionResult,
|
||||||
BlockExplorerTransactions,
|
BlockExplorerTransactions,
|
||||||
} from '../routes/types/block-explorer-response';
|
} from '../routes/types/block-explorer-response';
|
||||||
import { DATA_SOURCES } from '../config';
|
|
||||||
import isNumber from 'lodash/isNumber';
|
import isNumber from 'lodash/isNumber';
|
||||||
|
import { AllFilterOptions } from '../components/txs/tx-filter';
|
||||||
|
import type { FilterOption } from '../components/txs/tx-filter';
|
||||||
|
import { BE_TXS_PER_REQUEST, getTxsDataUrl } from './get-txs-data-url';
|
||||||
|
|
||||||
export interface TxsStateProps {
|
export function getTypeFilters(filters?: Set<FilterOption>) {
|
||||||
txsData: BlockExplorerTransactionResult[];
|
if (!filters) {
|
||||||
hasMoreTxs: boolean;
|
return '';
|
||||||
cursor: string;
|
} else if (filters.size > 1) {
|
||||||
previousCursors: string[];
|
return '';
|
||||||
hasPreviousPage: boolean;
|
}
|
||||||
|
|
||||||
|
const forcedSingleFilter = Array.from(filters)[0];
|
||||||
|
return `filters[cmd.type]=${forcedSingleFilter}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUseTxsData {
|
export interface IUseTxsData {
|
||||||
limit: number;
|
count?: number;
|
||||||
filters?: string;
|
before?: string;
|
||||||
|
after?: string;
|
||||||
|
party?: string;
|
||||||
|
filters?: Set<FilterOption>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGetTxsDataUrl {
|
export const useTxsData = ({
|
||||||
limit: string;
|
count = 25,
|
||||||
filters?: string;
|
before,
|
||||||
}
|
after,
|
||||||
|
filters,
|
||||||
|
party,
|
||||||
|
}: IUseTxsData) => {
|
||||||
|
const [, setSearchParams] = useSearchParams();
|
||||||
|
let hasMoreTxs = true;
|
||||||
|
let txsData: BlockExplorerTransactionResult[] = [];
|
||||||
|
|
||||||
export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
|
const url = getTxsDataUrl({
|
||||||
const url = new URL(`${DATA_SOURCES.blockExplorerUrl}/transactions`);
|
filters: getTypeFilters(filters),
|
||||||
|
count,
|
||||||
if (limit) {
|
before,
|
||||||
url.searchParams.append('limit', limit);
|
after,
|
||||||
}
|
party,
|
||||||
|
|
||||||
// Hacky fix for param as array
|
|
||||||
let urlAsString = url.toString();
|
|
||||||
if (filters) {
|
|
||||||
urlAsString += '&' + filters.replace(' ', '%20');
|
|
||||||
}
|
|
||||||
|
|
||||||
return urlAsString;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useTxsData = ({ limit, filters }: IUseTxsData) => {
|
|
||||||
const [
|
|
||||||
{ txsData, hasMoreTxs, cursor, previousCursors, hasPreviousPage },
|
|
||||||
setTxsState,
|
|
||||||
] = useState<TxsStateProps>({
|
|
||||||
txsData: [],
|
|
||||||
hasMoreTxs: false,
|
|
||||||
previousCursors: [],
|
|
||||||
cursor: '',
|
|
||||||
hasPreviousPage: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = getTxsDataUrl({ limit: limit.toString(), filters });
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: { data, error, loading },
|
state: { data, error, loading },
|
||||||
refetch,
|
refetch,
|
||||||
} = useFetch<BlockExplorerTransactions>(url, {}, true);
|
} = useFetch<BlockExplorerTransactions>(url, {}, true);
|
||||||
|
|
||||||
useEffect(() => {
|
if (!loading && data && isNumber(data.transactions.length)) {
|
||||||
if (!loading && data && isNumber(data.transactions.length)) {
|
hasMoreTxs = data.transactions.length >= count;
|
||||||
setTxsState((prev) => {
|
txsData = data.transactions;
|
||||||
return {
|
}
|
||||||
...prev,
|
|
||||||
txsData: data.transactions,
|
|
||||||
hasMoreTxs: data.transactions.length >= limit,
|
|
||||||
cursor: data?.transactions.at(-1)?.cursor || '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [loading, setTxsState, data, limit]);
|
|
||||||
|
|
||||||
const nextPage = useCallback(() => {
|
const nextPage = useCallback(() => {
|
||||||
const c = data?.transactions.at(0)?.cursor;
|
const after = data?.transactions.at(-1)?.cursor || '';
|
||||||
const newPreviousCursors = c ? [...previousCursors, c] : previousCursors;
|
const params: URLSearchParamsInit = { after };
|
||||||
|
if (filters) {
|
||||||
setTxsState((prev) => ({
|
params.filters = Array.from(filters).join(',');
|
||||||
...prev,
|
}
|
||||||
hasPreviousPage: true,
|
setSearchParams(params);
|
||||||
previousCursors: newPreviousCursors,
|
}, [filters, data, setSearchParams]);
|
||||||
}));
|
|
||||||
|
|
||||||
return refetch({
|
|
||||||
limit,
|
|
||||||
before: cursor,
|
|
||||||
});
|
|
||||||
}, [data, previousCursors, cursor, limit, refetch]);
|
|
||||||
|
|
||||||
const previousPage = useCallback(() => {
|
const previousPage = useCallback(() => {
|
||||||
const previousCursor = [...previousCursors].pop();
|
const before = data?.transactions[0]?.cursor || '';
|
||||||
const newPreviousCursors = previousCursors.slice(0, -1);
|
const params: URLSearchParamsInit = { before };
|
||||||
setTxsState((prev) => ({
|
if (filters && filters.size > 0 && filters.size === 1) {
|
||||||
...prev,
|
params.filters = Array.from(filters)[0];
|
||||||
hasPreviousPage: newPreviousCursors.length > 0,
|
}
|
||||||
previousCursors: newPreviousCursors,
|
setSearchParams(params);
|
||||||
}));
|
}, [filters, data, setSearchParams]);
|
||||||
return refetch({
|
|
||||||
limit,
|
|
||||||
before: previousCursor,
|
|
||||||
});
|
|
||||||
}, [previousCursors, limit, refetch]);
|
|
||||||
|
|
||||||
const refreshTxs = useCallback(async () => {
|
const refreshTxs = useCallback(async () => {
|
||||||
setTxsState(() => ({
|
const params: URLSearchParamsInit = {};
|
||||||
txsData: [],
|
if (filters && filters.size > 0 && filters.size === 1) {
|
||||||
cursor: '',
|
params.filters = Array.from(filters)[0];
|
||||||
previousCursors: [],
|
}
|
||||||
hasMoreTxs: false,
|
setSearchParams(params);
|
||||||
hasPreviousPage: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
refetch({ limit });
|
refetch({ count: BE_TXS_PER_REQUEST });
|
||||||
}, [setTxsState, limit, refetch, filters]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [setSearchParams, refetch, filters]);
|
||||||
|
|
||||||
|
const updateFilters = useCallback(
|
||||||
|
(newFilters: Set<FilterOption>) => {
|
||||||
|
const params: URLSearchParamsInit = {};
|
||||||
|
if (newFilters && newFilters.size === 1) {
|
||||||
|
params.filters = Array.from(newFilters)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams(params);
|
||||||
|
},
|
||||||
|
[setSearchParams]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
updateFilters,
|
||||||
txsData,
|
txsData,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
hasMoreTxs,
|
hasMoreTxs,
|
||||||
hasPreviousPage,
|
|
||||||
previousCursors,
|
|
||||||
cursor,
|
|
||||||
refreshTxs,
|
refreshTxs,
|
||||||
nextPage,
|
nextPage,
|
||||||
previousPage,
|
previousPage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Set of filters based on the URLSearchParams, or
|
||||||
|
* defaults to all.
|
||||||
|
* @param params
|
||||||
|
* @returns Set
|
||||||
|
*/
|
||||||
|
export function getInitialFilters(params: URLSearchParams): Set<FilterOption> {
|
||||||
|
const defaultFilters = new Set(AllFilterOptions);
|
||||||
|
|
||||||
|
const p = params.get('filters');
|
||||||
|
|
||||||
|
if (!p) {
|
||||||
|
return defaultFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = new Set<FilterOption>();
|
||||||
|
p.split(',').forEach((f) => {
|
||||||
|
if (AllFilterOptions.includes(f as FilterOption)) {
|
||||||
|
filters.add(f as FilterOption);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filters.size === 0) {
|
||||||
|
return defaultFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ describe('Block', () => {
|
|||||||
(useFetch as jest.Mock).mockReturnValue({
|
(useFetch as jest.Mock).mockReturnValue({
|
||||||
state: { data: createBlockResponse(1), loading: false, error: null },
|
state: { data: createBlockResponse(1), loading: false, error: null },
|
||||||
});
|
});
|
||||||
|
|
||||||
render(renderComponent(1));
|
render(renderComponent(1));
|
||||||
await waitFor(() => screen.getByTestId('block-header'));
|
await waitFor(() => screen.getByTestId('block-header'));
|
||||||
expect(screen.getByTestId('previous-block-button')).toHaveAttribute(
|
expect(screen.getByTestId('previous-block-button')).toHaveAttribute(
|
||||||
|
|||||||
@@ -15,9 +15,13 @@ import { PartyBlockAccounts } from './components/party-block-accounts';
|
|||||||
import { isValidPartyId } from './components/party-id-error';
|
import { isValidPartyId } from './components/party-id-error';
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
|
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
|
||||||
|
import type { FilterOption } from '../../../components/txs/tx-filter';
|
||||||
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
const Party = () => {
|
const Party = () => {
|
||||||
|
const [params] = useSearchParams();
|
||||||
|
|
||||||
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
||||||
const { party } = useParams<{ party: string }>();
|
const { party } = useParams<{ party: string }>();
|
||||||
|
|
||||||
@@ -27,24 +31,21 @@ const Party = () => {
|
|||||||
const partyId = toNonHex(party ? party : '');
|
const partyId = toNonHex(party ? party : '');
|
||||||
const { isMobile } = useScreenDimensions();
|
const { isMobile } = useScreenDimensions();
|
||||||
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
|
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
|
||||||
const baseFilters = `filters[tx.submitter]=${partyId}`;
|
|
||||||
const f =
|
|
||||||
filters && filters.size === 1
|
|
||||||
? `${baseFilters}&filters[cmd.type]=${Array.from(filters)[0]}`
|
|
||||||
: baseFilters;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hasMoreTxs,
|
|
||||||
nextPage,
|
nextPage,
|
||||||
|
refreshTxs,
|
||||||
previousPage,
|
previousPage,
|
||||||
error,
|
error,
|
||||||
refreshTxs,
|
|
||||||
loading,
|
loading,
|
||||||
txsData,
|
txsData,
|
||||||
hasPreviousPage,
|
hasMoreTxs,
|
||||||
|
updateFilters,
|
||||||
} = useTxsData({
|
} = useTxsData({
|
||||||
limit: 25,
|
filters: filters.size === 1 ? filters : undefined,
|
||||||
filters: f,
|
before: params.get('before') || undefined,
|
||||||
|
after: !params.get('before') ? params.get('after') || undefined : undefined,
|
||||||
|
party: partyId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
@@ -102,15 +103,21 @@ const Party = () => {
|
|||||||
refreshTxs={refreshTxs}
|
refreshTxs={refreshTxs}
|
||||||
nextPage={nextPage}
|
nextPage={nextPage}
|
||||||
previousPage={previousPage}
|
previousPage={previousPage}
|
||||||
hasPreviousPage={hasPreviousPage}
|
hasPreviousPage={true}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
hasMoreTxs={hasMoreTxs}
|
hasMoreTxs={hasMoreTxs}
|
||||||
>
|
>
|
||||||
<TxsFilter filters={filters} setFilters={setFilters} />
|
<TxsFilter
|
||||||
|
filters={filters}
|
||||||
|
setFilters={(f) => {
|
||||||
|
setFilters(f);
|
||||||
|
updateFilters(f as Set<FilterOption>);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</TxsListNavigation>
|
</TxsListNavigation>
|
||||||
{!error && txsData ? (
|
{!error && txsData ? (
|
||||||
<TxsInfiniteList
|
<TxsInfiniteList
|
||||||
hasMoreTxs={hasMoreTxs}
|
hasMoreTxs={true}
|
||||||
areTxsLoading={loading}
|
areTxsLoading={loading}
|
||||||
txs={txsData}
|
txs={txsData}
|
||||||
loadMoreTxs={nextPage}
|
loadMoreTxs={nextPage}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { RouteTitle } from '../../../components/route-title';
|
import { RouteTitle } from '../../../components/route-title';
|
||||||
import { TxsInfiniteList } from '../../../components/txs';
|
import { TxsInfiniteList } from '../../../components/txs';
|
||||||
import { useTxsData } from '../../../hooks/use-txs-data';
|
import { useTxsData, getInitialFilters } from '../../../hooks/use-txs-data';
|
||||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
import { TxsFilter } from '../../../components/txs/tx-filter';
|
||||||
|
import type { FilterOption } from '../../../components/txs/tx-filter';
|
||||||
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
|
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
const BE_TXS_PER_REQUEST = 25;
|
|
||||||
|
|
||||||
export const TxsList = () => {
|
export const TxsList = () => {
|
||||||
useDocumentTitle(['Transactions']);
|
useDocumentTitle(['Transactions']);
|
||||||
@@ -27,25 +27,22 @@ export const TxsList = () => {
|
|||||||
* @returns {JSX.Element} Transaction List and controls
|
* @returns {JSX.Element} Transaction List and controls
|
||||||
*/
|
*/
|
||||||
export const TxsListFiltered = () => {
|
export const TxsListFiltered = () => {
|
||||||
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
const [params] = useSearchParams();
|
||||||
|
const [filters, setFilters] = useState(getInitialFilters(params));
|
||||||
const f =
|
|
||||||
filters && filters.size === 1
|
|
||||||
? `filters[cmd.type]=${Array.from(filters)[0]}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hasMoreTxs,
|
|
||||||
nextPage,
|
nextPage,
|
||||||
previousPage,
|
previousPage,
|
||||||
error,
|
error,
|
||||||
refreshTxs,
|
refreshTxs,
|
||||||
loading,
|
loading,
|
||||||
txsData,
|
txsData,
|
||||||
hasPreviousPage,
|
hasMoreTxs,
|
||||||
|
updateFilters,
|
||||||
} = useTxsData({
|
} = useTxsData({
|
||||||
limit: BE_TXS_PER_REQUEST,
|
filters: filters.size === 1 ? filters : undefined,
|
||||||
filters: f,
|
before: params.get('before') || undefined,
|
||||||
|
after: !params.get('before') ? params.get('after') || undefined : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -54,11 +51,17 @@ export const TxsListFiltered = () => {
|
|||||||
refreshTxs={refreshTxs}
|
refreshTxs={refreshTxs}
|
||||||
nextPage={nextPage}
|
nextPage={nextPage}
|
||||||
previousPage={previousPage}
|
previousPage={previousPage}
|
||||||
hasPreviousPage={hasPreviousPage}
|
hasPreviousPage={true}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
hasMoreTxs={hasMoreTxs}
|
hasMoreTxs={hasMoreTxs}
|
||||||
>
|
>
|
||||||
<TxsFilter filters={filters} setFilters={setFilters} />
|
<TxsFilter
|
||||||
|
filters={filters}
|
||||||
|
setFilters={(f) => {
|
||||||
|
setFilters(f);
|
||||||
|
updateFilters(f as Set<FilterOption>);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</TxsListNavigation>
|
</TxsListNavigation>
|
||||||
<TxsInfiniteList
|
<TxsInfiniteList
|
||||||
hasFilters={filters.size > 0}
|
hasFilters={filters.size > 0}
|
||||||
|
|||||||
@@ -218,17 +218,33 @@ describe(
|
|||||||
getProposalInformationFromTable('Tokens for proposal')
|
getProposalInformationFromTable('Tokens for proposal')
|
||||||
.should('have.text', (1).toFixed(2))
|
.should('have.text', (1).toFixed(2))
|
||||||
.and('be.visible');
|
.and('be.visible');
|
||||||
|
navigateTo(navigation.proposals);
|
||||||
|
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
|
||||||
|
getProposalFromTitle(rawProposal.rationale.title).within(() => {
|
||||||
|
// 3002-PROP-021
|
||||||
|
cy.getByTestId('user-voted-yes').should('exist');
|
||||||
|
cy.getByTestId(viewProposalButton).click();
|
||||||
|
});
|
||||||
|
});
|
||||||
cy.getByTestId(changeVoteButton).should('be.visible').click();
|
cy.getByTestId(changeVoteButton).should('be.visible').click();
|
||||||
voteForProposal('against');
|
voteForProposal('against');
|
||||||
cy.getByTestId(proposalVoteProgressAgainstPercentage)
|
cy.getByTestId(proposalVoteProgressAgainstPercentage)
|
||||||
.contains('100.00%')
|
.contains('100.00%')
|
||||||
.and('be.visible');
|
.and('be.visible');
|
||||||
|
cy.getByTestId(voteBreakdownToggle).click();
|
||||||
getProposalInformationFromTable('Tokens against proposal')
|
getProposalInformationFromTable('Tokens against proposal')
|
||||||
.should('have.text', (1).toFixed(2))
|
.should('have.text', (1).toFixed(2))
|
||||||
.and('be.visible');
|
.and('be.visible');
|
||||||
getProposalInformationFromTable('Number of voting parties')
|
getProposalInformationFromTable('Number of voting parties')
|
||||||
.should('have.text', '1')
|
.should('have.text', '1')
|
||||||
.and('be.visible');
|
.and('be.visible');
|
||||||
|
navigateTo(navigation.proposals);
|
||||||
|
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
|
||||||
|
getProposalFromTitle(rawProposal.rationale.title).within(() => {
|
||||||
|
cy.getByTestId('user-voted-no').should('exist');
|
||||||
|
cy.getByTestId(viewProposalButton).click();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3001-VOTE-042, 3001-VOTE-057, 3001-VOTE-058, 3001-VOTE-059, 3001-VOTE-060
|
// 3001-VOTE-042, 3001-VOTE-057, 3001-VOTE-058, 3001-VOTE-059, 3001-VOTE-060
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ const votesTable = 'votes-table';
|
|||||||
const openProposals = 'open-proposals';
|
const openProposals = 'open-proposals';
|
||||||
const proposalVoteProgressForPercentage =
|
const proposalVoteProgressForPercentage =
|
||||||
'vote-progress-indicator-percentage-for';
|
'vote-progress-indicator-percentage-for';
|
||||||
|
const majorityVoteReached = 'majority-reached';
|
||||||
|
const minParticipationReached = 'participation-reached';
|
||||||
const proposalTimeout = { timeout: 8000 };
|
const proposalTimeout = { timeout: 8000 };
|
||||||
|
|
||||||
context(
|
context(
|
||||||
@@ -72,7 +74,7 @@ context(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3001-VOTE-046 3001-VOTE-044 3001-VOTE-074 3001-VOTE-074
|
// 3001-VOTE-020 3001-VOTE-021 3001-VOTE-046 3001-VOTE-044 3001-VOTE-074 3001-VOTE-074
|
||||||
it('Able to enact proposal by voting', function () {
|
it('Able to enact proposal by voting', function () {
|
||||||
const proposalTitle = 'Add New proposal with short enactment';
|
const proposalTitle = 'Add New proposal with short enactment';
|
||||||
const proposalTx = createUpdateNetworkProposalTxBody();
|
const proposalTx = createUpdateNetworkProposalTxBody();
|
||||||
@@ -85,10 +87,18 @@ context(
|
|||||||
cy.contains(proposalTitle)
|
cy.contains(proposalTitle)
|
||||||
.parentsUntil(proposalListItem)
|
.parentsUntil(proposalListItem)
|
||||||
.last()
|
.last()
|
||||||
.within(() => cy.getByTestId(viewProposalButton).click());
|
.within(() => {
|
||||||
|
// 3001-VOTE-019 time to vote is highlighted red
|
||||||
|
cy.getByTestId('vote-details')
|
||||||
|
.find('span')
|
||||||
|
.should('have.class', 'text-vega-pink');
|
||||||
|
cy.getByTestId(viewProposalButton).click();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
cy.getByTestId(proposalStatus).should('have.text', 'Open');
|
cy.getByTestId(proposalStatus).should('have.text', 'Open');
|
||||||
voteForProposal('for');
|
voteForProposal('for');
|
||||||
|
cy.getByTestId(majorityVoteReached).should('exist');
|
||||||
|
cy.getByTestId(minParticipationReached).should('exist');
|
||||||
cy.getByTestId(proposalStatus, proposalTimeout)
|
cy.getByTestId(proposalStatus, proposalTimeout)
|
||||||
.should('have.text', 'Passed')
|
.should('have.text', 'Passed')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -104,6 +114,14 @@ context(
|
|||||||
cy.getByTestId(proposalVoteProgressForPercentage)
|
cy.getByTestId(proposalVoteProgressForPercentage)
|
||||||
.contains('100.00%')
|
.contains('100.00%')
|
||||||
.and('be.visible');
|
.and('be.visible');
|
||||||
|
navigateTo(navigation.proposals);
|
||||||
|
cy.contains(proposalTitle)
|
||||||
|
.parentsUntil(proposalListItem)
|
||||||
|
.last()
|
||||||
|
.within(() => {
|
||||||
|
cy.getByTestId(majorityVoteReached).should('exist');
|
||||||
|
cy.getByTestId(minParticipationReached).should('exist');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3001-VOTE-047
|
// 3001-VOTE-047
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
|
|||||||
navigateTo(navigation.proposals);
|
navigateTo(navigation.proposals);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 3001-VOTE-018
|
||||||
it('Newly created proposals list - proposals closest to closing date appear higher in list', function () {
|
it('Newly created proposals list - proposals closest to closing date appear higher in list', function () {
|
||||||
// 3001-VOTE-005
|
|
||||||
const proposalDays = [364, 50, 2];
|
const proposalDays = [364, 50, 2];
|
||||||
for (let index = 0; index < proposalDays.length; index++) {
|
for (let index = 0; index < proposalDays.length; index++) {
|
||||||
goToMakeNewProposal(governanceProposalType.RAW);
|
goToMakeNewProposal(governanceProposalType.RAW);
|
||||||
@@ -120,16 +120,24 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
|
|||||||
submitUniqueRawProposal({ proposalTitle: proposalTitle });
|
submitUniqueRawProposal({ proposalTitle: proposalTitle });
|
||||||
getProposalFromTitle(proposalTitle).within(() => {
|
getProposalFromTitle(proposalTitle).within(() => {
|
||||||
// 3001-VOTE-039
|
// 3001-VOTE-039
|
||||||
cy.getByTestId(voteStatus).should(
|
cy.getByTestId('participation-not-reached').should(
|
||||||
'have.text',
|
'have.text',
|
||||||
'Participation not reached'
|
'Min. participation not reached'
|
||||||
);
|
);
|
||||||
cy.getByTestId(viewProposalButton).click();
|
cy.getByTestId(viewProposalButton).click();
|
||||||
});
|
});
|
||||||
voteForProposal('for');
|
voteForProposal('for');
|
||||||
navigateTo(navigation.proposals);
|
navigateTo(navigation.proposals);
|
||||||
getProposalFromTitle(proposalTitle).within(() => {
|
getProposalFromTitle(proposalTitle).within(() => {
|
||||||
cy.getByTestId(voteStatus).should('have.text', 'Set to pass');
|
cy.getByTestId(voteStatus).should(
|
||||||
|
'have.text',
|
||||||
|
'Currently expected to pass'
|
||||||
|
);
|
||||||
|
cy.getByTestId('user-voted-yes').should('exist');
|
||||||
|
cy.getByTestId('participation-reached').should(
|
||||||
|
'have.text',
|
||||||
|
'Min. participation reached'
|
||||||
|
);
|
||||||
cy.getByTestId(viewProposalButton).click();
|
cy.getByTestId(viewProposalButton).click();
|
||||||
});
|
});
|
||||||
cy.getByTestId(voteBreakDownToggle).click();
|
cy.getByTestId(voteBreakDownToggle).click();
|
||||||
|
|||||||
@@ -351,6 +351,12 @@ context(
|
|||||||
);
|
);
|
||||||
stakingPageDisassociateAllTokens();
|
stakingPageDisassociateAllTokens();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Able to associate over 1 million tokens', function () {
|
||||||
|
cy.get(ethWalletAssociateButton).click();
|
||||||
|
stakingPageAssociateTokens('2000000', { approve: true });
|
||||||
|
stakingPageDisassociateAllTokens();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const governanceDocsUrl = 'https://vega.xyz/governance';
|
|||||||
const networkUpgradeProposalListItem = 'protocol-upgrade-proposals-list-item';
|
const networkUpgradeProposalListItem = 'protocol-upgrade-proposals-list-item';
|
||||||
const closedProposals = 'closed-proposals';
|
const closedProposals = 'closed-proposals';
|
||||||
const closedProposalToggle = 'closed-proposals-toggle-networkUpgrades';
|
const closedProposalToggle = 'closed-proposals-toggle-networkUpgrades';
|
||||||
|
const protocolUpgradeTime = 'protocol-upgrade-time';
|
||||||
|
|
||||||
context(
|
context(
|
||||||
'Governance Page - verify elements on page',
|
'Governance Page - verify elements on page',
|
||||||
@@ -174,6 +175,7 @@ context(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 3009-NTWU-003 3009-NTWU-004 3009-NTWU-007
|
||||||
it('should see details of network upgrade proposal', function () {
|
it('should see details of network upgrade proposal', function () {
|
||||||
mockNetworkUpgradeProposal();
|
mockNetworkUpgradeProposal();
|
||||||
navigateTo(navigation.proposals);
|
navigateTo(navigation.proposals);
|
||||||
@@ -228,6 +230,7 @@ context(
|
|||||||
cy.getByTestId(closedProposalToggle).should('not.exist');
|
cy.getByTestId(closedProposalToggle).should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 3009-NTWU-001 3009-NTWU-002 3009-NTWU-006 3009-NTWU-009
|
||||||
it('should display network upgrade banner with estimate', function () {
|
it('should display network upgrade banner with estimate', function () {
|
||||||
mockNetworkUpgradeProposal();
|
mockNetworkUpgradeProposal();
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
@@ -259,10 +262,18 @@ context(
|
|||||||
.as('displayedEstimate');
|
.as('displayedEstimate');
|
||||||
cy.get('@displayedEstimate').then((estimateText) => {
|
cy.get('@displayedEstimate').then((estimateText) => {
|
||||||
// Estimated time should automatically update every second
|
// Estimated time should automatically update every second
|
||||||
cy.getByTestId('protocol-upgrade-time')
|
cy.getByTestId(protocolUpgradeTime)
|
||||||
.invoke('text')
|
.invoke('text')
|
||||||
.should('not.eq', estimateText);
|
.should('not.eq', estimateText);
|
||||||
});
|
});
|
||||||
|
// time estimate on proposal detail
|
||||||
|
cy.getByTestId('protocol-upgrade-proposal').within(() => {
|
||||||
|
cy.get('@displayedEstimate').then((estimateText) => {
|
||||||
|
cy.getByTestId(protocolUpgradeTime)
|
||||||
|
.invoke('text')
|
||||||
|
.should('not.eq', estimateText);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ import { VegaWallet } from '../vega-wallet';
|
|||||||
import { useLocation, useMatch } from 'react-router-dom';
|
import { useLocation, useMatch } from 'react-router-dom';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useTelemetryDialog } from '../telemetry-dialog/telemetry-dialog';
|
import { useTelemetryDialog } from '../telemetry-dialog/telemetry-dialog';
|
||||||
import {
|
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
|
||||||
ProtocolUpgradeCountdown,
|
|
||||||
ProtocolUpgradeCountdownMode,
|
|
||||||
} from '@vegaprotocol/proposals';
|
|
||||||
|
|
||||||
export const SettingsLink = () => {
|
export const SettingsLink = () => {
|
||||||
const { open, isOpen, close } = useTelemetryDialog();
|
const { open, isOpen, close } = useTelemetryDialog();
|
||||||
@@ -68,9 +65,7 @@ export const Nav = ({ theme }: Pick<NavigationProps, 'theme'>) => {
|
|||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<SettingsLink />
|
<SettingsLink />
|
||||||
<ProtocolUpgradeCountdown
|
<ProtocolUpgradeCountdown />
|
||||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ export const downloadJson = (jsonString: string, proposalTitle: string) => {
|
|||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
-15
@@ -5,7 +5,6 @@ import {
|
|||||||
RewardsTable,
|
RewardsTable,
|
||||||
} from '../shared-rewards-table-assets/shared-rewards-table-assets';
|
} from '../shared-rewards-table-assets/shared-rewards-table-assets';
|
||||||
import type { EpochIndividualReward } from './generate-epoch-individual-rewards-list';
|
import type { EpochIndividualReward } from './generate-epoch-individual-rewards-list';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
interface EpochIndividualRewardsGridProps {
|
interface EpochIndividualRewardsGridProps {
|
||||||
data: EpochIndividualReward;
|
data: EpochIndividualReward;
|
||||||
@@ -22,14 +21,11 @@ interface RewardItemProps {
|
|||||||
const DisplayReward = ({
|
const DisplayReward = ({
|
||||||
reward,
|
reward,
|
||||||
decimals,
|
decimals,
|
||||||
percentageOfTotal,
|
|
||||||
}: {
|
}: {
|
||||||
reward: string;
|
reward: string;
|
||||||
decimals: number;
|
decimals: number;
|
||||||
percentageOfTotal?: string;
|
percentageOfTotal?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
if (Number(reward) === 0) {
|
if (Number(reward) === 0) {
|
||||||
return <span className="text-vega-dark-300">-</span>;
|
return <span className="text-vega-dark-300">-</span>;
|
||||||
}
|
}
|
||||||
@@ -39,23 +35,12 @@ const DisplayReward = ({
|
|||||||
description={
|
description={
|
||||||
<div className="flex flex-col items-start">
|
<div className="flex flex-col items-start">
|
||||||
<span>{formatNumber(toBigNum(reward, decimals), decimals)}</span>
|
<span>{formatNumber(toBigNum(reward, decimals), decimals)}</span>
|
||||||
{percentageOfTotal && (
|
|
||||||
<span className="text-vega-dark-300">
|
|
||||||
({percentageOfTotal}% {t('ofTotalDistributed')})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<button>
|
<button>
|
||||||
<div className="flex flex-col items-start">
|
<div className="flex flex-col items-start">
|
||||||
<span>{formatNumber(toBigNum(reward, decimals), 4)}</span>
|
<span>{formatNumber(toBigNum(reward, decimals), 4)}</span>
|
||||||
{percentageOfTotal && (
|
|
||||||
<span className="text-vega-dark-300">
|
|
||||||
({formatNumber(percentageOfTotal, 4).toString()}
|
|
||||||
%)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
-76
@@ -168,17 +168,6 @@ describe('generateEpochIndividualRewardsList', () => {
|
|||||||
expect(result2[1].epoch).toEqual(1);
|
expect(result2[1].epoch).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('correctly calculates the total value of rewards for an asset', () => {
|
|
||||||
const rewards = [reward1, reward4];
|
|
||||||
const result = generateEpochIndividualRewardsList({
|
|
||||||
rewards,
|
|
||||||
epochId: 1,
|
|
||||||
epochRewardSummaries: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result[0].rewards[0].totalAmount).toEqual('200');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns data in the expected shape', () => {
|
it('returns data in the expected shape', () => {
|
||||||
// Just sanity checking the whole structure here
|
// Just sanity checking the whole structure here
|
||||||
const rewards = [reward1, reward2, reward3, reward4];
|
const rewards = [reward1, reward2, reward3, reward4];
|
||||||
@@ -458,69 +447,4 @@ describe('generateEpochIndividualRewardsList', () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('correctly calculates the percentage of two or more rewards by referencing the total rewards amount', () => {
|
|
||||||
const result = generateEpochIndividualRewardsList({
|
|
||||||
rewards: [
|
|
||||||
// reward1 is 100 usd, which is 10% of the total rewards amount
|
|
||||||
reward1,
|
|
||||||
{
|
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
|
||||||
amount: '200',
|
|
||||||
percentageOfTotal: '0.2',
|
|
||||||
receivedAt: new Date(),
|
|
||||||
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
|
|
||||||
party: { id: 'blah' },
|
|
||||||
epoch: { id: '1' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
epochId: 1,
|
|
||||||
epochRewardSummaries: [
|
|
||||||
{
|
|
||||||
__typename: 'EpochRewardSummary',
|
|
||||||
epoch: 1,
|
|
||||||
assetId: 'usd',
|
|
||||||
amount: '1000',
|
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result[0]).toEqual({
|
|
||||||
epoch: 1,
|
|
||||||
rewards: [
|
|
||||||
{
|
|
||||||
asset: 'USD',
|
|
||||||
decimals: 6,
|
|
||||||
totalAmount: '300',
|
|
||||||
rewardTypes: {
|
|
||||||
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
|
|
||||||
amount: '0',
|
|
||||||
percentageOfTotal: '0',
|
|
||||||
},
|
|
||||||
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
|
|
||||||
amount: '0',
|
|
||||||
percentageOfTotal: '0',
|
|
||||||
},
|
|
||||||
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
|
|
||||||
amount: '300',
|
|
||||||
percentageOfTotal: '30',
|
|
||||||
},
|
|
||||||
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
|
|
||||||
amount: '0',
|
|
||||||
percentageOfTotal: '0',
|
|
||||||
},
|
|
||||||
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
|
|
||||||
amount: '0',
|
|
||||||
percentageOfTotal: '0',
|
|
||||||
},
|
|
||||||
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
|
|
||||||
amount: '0',
|
|
||||||
percentageOfTotal: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-16
@@ -59,7 +59,6 @@ export const generateEpochIndividualRewardsList = ({
|
|||||||
const epochIndividualRewards = rewards.reduce((acc, reward) => {
|
const epochIndividualRewards = rewards.reduce((acc, reward) => {
|
||||||
const epochId = reward.epoch.id;
|
const epochId = reward.epoch.id;
|
||||||
const assetName = reward.asset.name;
|
const assetName = reward.asset.name;
|
||||||
const assetId = reward.asset.id;
|
|
||||||
const assetDecimals = reward.asset.decimals;
|
const assetDecimals = reward.asset.decimals;
|
||||||
const rewardType = reward.rewardType;
|
const rewardType = reward.rewardType;
|
||||||
const amount = reward.amount;
|
const amount = reward.amount;
|
||||||
@@ -77,13 +76,6 @@ export const generateEpochIndividualRewardsList = ({
|
|||||||
const epoch = acc.get(epochId);
|
const epoch = acc.get(epochId);
|
||||||
|
|
||||||
// matchingTotalReward is the total awarded for all users for the reward type in the epoch of the asset
|
// matchingTotalReward is the total awarded for all users for the reward type in the epoch of the asset
|
||||||
const matchingTotalRewardAmount = epochRewardSummaries.find(
|
|
||||||
(summary) =>
|
|
||||||
summary.epoch === Number(epochId) &&
|
|
||||||
summary.assetId === assetId &&
|
|
||||||
summary.rewardType === rewardType
|
|
||||||
)?.amount;
|
|
||||||
|
|
||||||
let asset = epoch?.rewards.find((r) => r.asset === assetName);
|
let asset = epoch?.rewards.find((r) => r.asset === assetName);
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
@@ -106,14 +98,7 @@ export const generateEpochIndividualRewardsList = ({
|
|||||||
|
|
||||||
asset.rewardTypes[rewardType] = {
|
asset.rewardTypes[rewardType] = {
|
||||||
amount: newAmount,
|
amount: newAmount,
|
||||||
percentageOfTotal: matchingTotalRewardAmount
|
percentageOfTotal: percentageOfTotal,
|
||||||
? new BigNumber(newAmount)
|
|
||||||
.dividedBy(matchingTotalRewardAmount)
|
|
||||||
.multipliedBy(100)
|
|
||||||
.toString()
|
|
||||||
: // this should never be reached, if there's an individual reward there should
|
|
||||||
// always be a reward total from the api too, but set it as a fallback just in case
|
|
||||||
percentageOfTotal,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ export const RewardsPage = () => {
|
|||||||
error: paramsError,
|
error: paramsError,
|
||||||
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
|
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
|
||||||
|
|
||||||
console.log('params', params);
|
|
||||||
|
|
||||||
const payoutDuration = useMemo(() => {
|
const payoutDuration = useMemo(() => {
|
||||||
if (!params) {
|
if (!params) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import { TxState } from '../../../hooks/transaction-reducer';
|
|||||||
import { useTransaction } from '../../../hooks/use-transaction';
|
import { useTransaction } from '../../../hooks/use-transaction';
|
||||||
import { BigNumber } from '../../../lib/bignumber';
|
import { BigNumber } from '../../../lib/bignumber';
|
||||||
import { AssociateInfo } from './associate-info';
|
import { AssociateInfo } from './associate-info';
|
||||||
import { removeDecimal, toBigNum } from '@vegaprotocol/utils';
|
import { toBigNum } from '@vegaprotocol/utils';
|
||||||
import type { EthereumConfig } from '@vegaprotocol/web3';
|
import type { EthereumConfig } from '@vegaprotocol/web3';
|
||||||
import { useBalances } from '../../../lib/balances/balances-store';
|
import { useBalances } from '../../../lib/balances/balances-store';
|
||||||
|
import { MaxUint256 } from '@ethersproject/constants';
|
||||||
|
|
||||||
export const WalletAssociate = ({
|
export const WalletAssociate = ({
|
||||||
perform,
|
perform,
|
||||||
@@ -42,7 +43,7 @@ export const WalletAssociate = ({
|
|||||||
} = useTransaction(() => {
|
} = useTransaction(() => {
|
||||||
return token.approve(
|
return token.approve(
|
||||||
ethereumConfig.staking_bridge_contract.address,
|
ethereumConfig.staking_bridge_contract.address,
|
||||||
removeDecimal('1000000', decimals).toString()
|
MaxUint256.toString()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -535,6 +535,7 @@ function checkIfDataAndTimeOfCreationAndUpdateIsEqual(date: string) {
|
|||||||
// unexpected latency
|
// unexpected latency
|
||||||
const minBefore = subSeconds(new Date(), 5);
|
const minBefore = subSeconds(new Date(), 5);
|
||||||
const maxAfter = addSeconds(new Date(), 5);
|
const maxAfter = addSeconds(new Date(), 5);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(maxAfter);
|
console.log(maxAfter);
|
||||||
const date = new Date($dateTime.toString());
|
const date = new Date($dateTime.toString());
|
||||||
expect(isAfter(date, minBefore) && isBefore(date, maxAfter)).to.equal(
|
expect(isAfter(date, minBefore) && isBefore(date, maxAfter)).to.equal(
|
||||||
|
|||||||
@@ -68,22 +68,4 @@ describe('home', { tags: '@regression' }, () => {
|
|||||||
cy.getByTestId('connect').click();
|
cy.getByTestId('connect').click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Network switcher', () => {
|
|
||||||
before(() => {
|
|
||||||
cy.mockTradingPage();
|
|
||||||
cy.mockSubscription();
|
|
||||||
cy.visit('/');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('switch to fairground network and check status & incidents link', () => {
|
|
||||||
// 0006-NETW-002
|
|
||||||
// 0006-NETW-003
|
|
||||||
cy.getByTestId('navigation')
|
|
||||||
.find('[data-testid="network-switcher"]')
|
|
||||||
.should('have.text', 'Custom')
|
|
||||||
.click();
|
|
||||||
cy.getByTestId('network-item').contains('Fairground testnet');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
|
|||||||
.find('button')
|
.find('button')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
const dropdownContent = '[data-testid="market-actions-content"]';
|
const dropdownContent = '[data-testid="proposal-actions-content"]';
|
||||||
const dropdownContentItem = '[role="menuitem"]';
|
const dropdownContentItem = '[role="menuitem"]';
|
||||||
|
|
||||||
// 6001-MARK-059
|
// 6001-MARK-059
|
||||||
@@ -100,7 +100,6 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
|
|||||||
'VEGA_TOKEN_URL'
|
'VEGA_TOKEN_URL'
|
||||||
)}/proposals/e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829`
|
)}/proposals/e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829`
|
||||||
);
|
);
|
||||||
cy.getByTestId('market-actions-content').click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6001-MARK-060
|
// 6001-MARK-060
|
||||||
@@ -214,11 +213,12 @@ describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => {
|
|||||||
aliasGQLQuery(req, 'ProposalsList', proposal);
|
aliasGQLQuery(req, 'ProposalsList', proposal);
|
||||||
});
|
});
|
||||||
cy.mockSubscription();
|
cy.mockSubscription();
|
||||||
cy.visit('/#/markets/all');
|
|
||||||
cy.get('[data-testid="Proposed markets"]').click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can see no markets message', () => {
|
it('can see no markets message', () => {
|
||||||
|
cy.visit('/#/markets/all');
|
||||||
|
cy.get('[data-testid="Proposed markets"]').click();
|
||||||
|
|
||||||
// 6001-MARK-061
|
// 6001-MARK-061
|
||||||
cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets');
|
cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import { mockConnectWallet } from '@vegaprotocol/cypress';
|
|
||||||
|
|
||||||
describe('Navbar', { tags: '@smoke' }, () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.clearAllLocalStorage();
|
|
||||||
cy.mockTradingPage();
|
|
||||||
cy.mockSubscription();
|
|
||||||
cy.visit('/');
|
|
||||||
cy.wait('@Markets');
|
|
||||||
cy.wait('@MarketsData');
|
|
||||||
});
|
|
||||||
|
|
||||||
const pages = [
|
|
||||||
{ name: 'Markets', link: '#/markets/all' },
|
|
||||||
{ name: 'Trading', link: '#/markets' },
|
|
||||||
{ name: 'Portfolio', link: '#/portfolio' },
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('desktop view', () => {
|
|
||||||
pages.forEach(({ name, link }) => {
|
|
||||||
it(`${name} should be correctly rendered`, () => {
|
|
||||||
cy.get('nav')
|
|
||||||
.find(`a[data-testid=${name}]:visible`)
|
|
||||||
.then((element) => {
|
|
||||||
cy.wrap(element).click();
|
|
||||||
cy.location('hash').should('contain', link);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Resources dropdown should be correctly rendered', () => {
|
|
||||||
const resourceSelector = 'ul li:contains(Resources)';
|
|
||||||
['Docs', 'Give Feedback'].forEach((text, index) => {
|
|
||||||
cy.get('nav').find(resourceSelector).contains('Resources').click();
|
|
||||||
cy.get('nav')
|
|
||||||
.find(resourceSelector)
|
|
||||||
.find('.navigation-content li')
|
|
||||||
.eq(index)
|
|
||||||
.find('a')
|
|
||||||
.then((element) => {
|
|
||||||
expect(element.attr('target')).to.eq('_blank');
|
|
||||||
expect(element.attr('href')).to.not.be.empty;
|
|
||||||
expect(element.text()).to.eq(text);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Disclaimer should be presented after choosing from menu', () => {
|
|
||||||
cy.get('nav')
|
|
||||||
.find('ul li:contains(Resources)')
|
|
||||||
.contains('Resources')
|
|
||||||
.click();
|
|
||||||
cy.getByTestId('Disclaimer').eq(0).click();
|
|
||||||
cy.location('hash').should('equal', '#/disclaimer');
|
|
||||||
cy.get('p').contains(
|
|
||||||
'Vega is a decentralised peer-to-peer protocol that can be used to trade derivatives with cryptoassets.'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('mobile view', () => {
|
|
||||||
const viewportHeight = Cypress.config('viewportHeight');
|
|
||||||
const viewportWidth = Cypress.config('viewportWidth');
|
|
||||||
before(() => {
|
|
||||||
// a little hack to keep the viewport size between tests (cypress bug)
|
|
||||||
Cypress.config({
|
|
||||||
viewportWidth: 560,
|
|
||||||
viewportHeight: 890,
|
|
||||||
});
|
|
||||||
cy.viewport(560, 890);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('wallet drawer', () => {
|
|
||||||
it('wallet drawer should be correctly rendered', () => {
|
|
||||||
mockConnectWallet();
|
|
||||||
cy.connectVegaWallet(true);
|
|
||||||
cy.getByTestId('connect-vega-wallet-mobile').click();
|
|
||||||
cy.getByTestId('wallets-drawer').should('be.visible');
|
|
||||||
cy.getByTestId('wallets-drawer').within((el) => {
|
|
||||||
cy.wrap(el).get('button').contains('Disconnect').click();
|
|
||||||
});
|
|
||||||
cy.getByTestId('wallets-drawer').should('not.be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('menu drawer', () => {
|
|
||||||
pages.forEach(({ name, link }) => {
|
|
||||||
it(`${name} should be correctly rendered`, () => {
|
|
||||||
cy.getByTestId('button-menu-drawer').click();
|
|
||||||
cy.getByTestId('menu-drawer').should('be.visible');
|
|
||||||
cy.getByTestId('menu-drawer').within((el) => {
|
|
||||||
cy.wrap(el).getByTestId(name).click();
|
|
||||||
cy.location('hash').should('contain', link);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Menu drawer should not be visible until opened', () => {
|
|
||||||
cy.getByTestId('menu-drawer').should('not.be.visible');
|
|
||||||
cy.getByTestId('button-menu-drawer').click();
|
|
||||||
cy.getByTestId('menu-drawer').should('be.visible');
|
|
||||||
cy.getByTestId('button-menu-drawer').click();
|
|
||||||
cy.getByTestId('menu-drawer').should('not.be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
after(() => {
|
|
||||||
// a little hack to keep the viewport size between tests (cypress bug)
|
|
||||||
Cypress.config({
|
|
||||||
viewportWidth,
|
|
||||||
viewportHeight,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -171,6 +171,7 @@ describe(
|
|||||||
.invoke('text')
|
.invoke('text')
|
||||||
.then((text) => {
|
.then((text) => {
|
||||||
const actualDate = text.slice(0, -67);
|
const actualDate = text.slice(0, -67);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(actualDate);
|
console.log(actualDate);
|
||||||
const actualOhlc = text.slice(-67);
|
const actualOhlc = text.slice(-67);
|
||||||
assert.isTrue(expectedDateRegex.test(actualDate));
|
assert.isTrue(expectedDateRegex.test(actualDate));
|
||||||
|
|||||||
@@ -28,16 +28,16 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
|
|||||||
|
|
||||||
it('must be able to select order direction - long/short', function () {
|
it('must be able to select order direction - long/short', function () {
|
||||||
// 7002-SORD-004
|
// 7002-SORD-004
|
||||||
cy.getByTestId(toggleShort).click().children('input').should('be.checked');
|
cy.getByTestId(toggleShort).click().next('input').should('be.checked');
|
||||||
cy.getByTestId(toggleLong).click().children('input').should('be.checked');
|
cy.getByTestId(toggleLong).click().next('input').should('be.checked');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('must be able to select order type - limit/market', function () {
|
it('must be able to select order type - limit/market', function () {
|
||||||
// 7002-SORD-005
|
// 7002-SORD-005
|
||||||
// 7002-SORD-006
|
// 7002-SORD-006
|
||||||
// 7002-SORD-007
|
// 7002-SORD-007
|
||||||
cy.getByTestId(toggleLimit).click().children('input').should('be.checked');
|
cy.getByTestId(toggleLimit).click().next('input').should('be.checked');
|
||||||
cy.getByTestId(toggleMarket).click().children('input').should('be.checked');
|
cy.getByTestId(toggleMarket).click().next('input').should('be.checked');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('order connect vega wallet button should connect', () => {
|
it('order connect vega wallet button should connect', () => {
|
||||||
@@ -51,7 +51,7 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
|
|||||||
.click();
|
.click();
|
||||||
cy.wait('@walletReq');
|
cy.wait('@walletReq');
|
||||||
cy.getByTestId(placeOrderBtn).should('be.visible');
|
cy.getByTestId(placeOrderBtn).should('be.visible');
|
||||||
cy.getByTestId(toggleLimit).children('input').should('be.checked');
|
cy.getByTestId(toggleLimit).next('input').should('be.checked');
|
||||||
cy.getByTestId(orderPriceField).should('have.value', '101');
|
cy.getByTestId(orderPriceField).should('have.value', '101');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const closePosition = 'close-position';
|
|||||||
const dialogCloseX = 'dialog-close';
|
const dialogCloseX = 'dialog-close';
|
||||||
const dialogContent = 'dialog-content';
|
const dialogContent = 'dialog-content';
|
||||||
const dropDownMenu = 'dropdown-menu';
|
const dropDownMenu = 'dropdown-menu';
|
||||||
const marketActionsContent = 'market-actions-content';
|
const marketActionsContent = 'position-actions-content';
|
||||||
const positions = 'Positions';
|
const positions = 'Positions';
|
||||||
const tabPositions = 'tab-positions';
|
const tabPositions = 'tab-positions';
|
||||||
const toastContent = 'toast-content';
|
const toastContent = 'toast-content';
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
|
|||||||
cy.setVegaWallet();
|
cy.setVegaWallet();
|
||||||
|
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
|
cy.getByTestId(manageVegaWallet).click();
|
||||||
|
cy.getByTestId(walletTransfer).click();
|
||||||
|
|
||||||
cy.wait('@Assets');
|
cy.wait('@Assets');
|
||||||
cy.wait('@Accounts');
|
cy.wait('@Accounts');
|
||||||
@@ -57,7 +59,6 @@ describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
|
|||||||
// 1003-TRAN-019
|
// 1003-TRAN-019
|
||||||
cy.getByTestId(transferForm);
|
cy.getByTestId(transferForm);
|
||||||
cy.contains('Enter manually').click();
|
cy.contains('Enter manually').click();
|
||||||
|
|
||||||
cy.getByTestId(transferForm)
|
cy.getByTestId(transferForm)
|
||||||
.find(toAddressField)
|
.find(toAddressField)
|
||||||
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
|
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { OrderType } from '@vegaprotocol/types';
|
||||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||||
|
|
||||||
const orderSizeField = 'order-size';
|
const orderSizeField = 'order-size';
|
||||||
@@ -9,7 +10,9 @@ export const createOrder = (order: OrderSubmission): void => {
|
|||||||
cy.log('Placing order', order);
|
cy.log('Placing order', order);
|
||||||
const { type, side, size, price, timeInForce, expiresAt } = order;
|
const { type, side, size, price, timeInForce, expiresAt } = order;
|
||||||
|
|
||||||
cy.getByTestId(`order-type-${type}`).click();
|
cy.getByTestId(
|
||||||
|
`order-type-${type === OrderType.TYPE_LIMIT ? 'Limit' : 'Market'}`
|
||||||
|
).click();
|
||||||
cy.getByTestId(`order-side-${side}`).click();
|
cy.getByTestId(`order-side-${side}`).click();
|
||||||
cy.getByTestId(orderSizeField).clear().type(size);
|
cy.getByTestId(orderSizeField).clear().type(size);
|
||||||
if (price) {
|
if (price) {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ export const orderTIFDropDown = 'order-tif';
|
|||||||
export const placeOrderBtn = 'place-order';
|
export const placeOrderBtn = 'place-order';
|
||||||
export const toggleShort = 'order-side-SIDE_SELL';
|
export const toggleShort = 'order-side-SIDE_SELL';
|
||||||
export const toggleLong = 'order-side-SIDE_BUY';
|
export const toggleLong = 'order-side-SIDE_BUY';
|
||||||
export const toggleLimit = 'order-type-TYPE_LIMIT';
|
export const toggleLimit = 'order-type-Limit';
|
||||||
export const toggleMarket = 'order-type-TYPE_MARKET';
|
export const toggleMarket = 'order-type-Market';
|
||||||
|
|
||||||
export const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
|
export const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
+1
-1
@@ -16,6 +16,6 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=true
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
@@ -18,6 +18,6 @@ NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supp
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=false
|
NX_SUCCESSOR_MARKETS=false
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=false
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
@@ -16,6 +16,6 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=true
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
@@ -18,6 +18,6 @@ NX_APP_VERSION=v0.20.21-core-0.71.6
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=false
|
NX_SUCCESSOR_MARKETS=false
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=false
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
@@ -18,6 +18,6 @@ NX_APP_VERSION=v0.20.19-core-0.71.6
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=false
|
NX_SUCCESSOR_MARKETS=false
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=false
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
@@ -16,6 +16,6 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=true
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
@@ -17,6 +17,6 @@ NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=true
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
@@ -17,6 +17,6 @@ NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=false
|
NX_SUCCESSOR_MARKETS=false
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=false
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
@@ -17,7 +17,10 @@ import {
|
|||||||
usePaneLayout,
|
usePaneLayout,
|
||||||
} from '../../components/resizable-grid';
|
} from '../../components/resizable-grid';
|
||||||
import { TradingViews } from './trade-views';
|
import { TradingViews } from './trade-views';
|
||||||
import { MarketSuccessorBanner } from '../../components/market-banner';
|
import {
|
||||||
|
MarketSuccessorBanner,
|
||||||
|
MarketSuccessorProposalBanner,
|
||||||
|
} from '../../components/market-banner';
|
||||||
import { FLAGS } from '@vegaprotocol/environment';
|
import { FLAGS } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
interface TradeGridProps {
|
interface TradeGridProps {
|
||||||
@@ -127,6 +130,13 @@ const MainGrid = memo(
|
|||||||
<TradingViews.orders.component marketId={marketId} />
|
<TradingViews.orders.component marketId={marketId} />
|
||||||
</VegaWalletContainer>
|
</VegaWalletContainer>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
{FLAGS.STOP_ORDERS ? (
|
||||||
|
<Tab id="stop-orders" name={t('Stop orders')}>
|
||||||
|
<VegaWalletContainer>
|
||||||
|
<TradingViews.stopOrders.component />
|
||||||
|
</VegaWalletContainer>
|
||||||
|
</Tab>
|
||||||
|
) : null}
|
||||||
<Tab id="fills" name={t('Fills')}>
|
<Tab id="fills" name={t('Fills')}>
|
||||||
<VegaWalletContainer>
|
<VegaWalletContainer>
|
||||||
<TradingViews.fills.component
|
<TradingViews.fills.component
|
||||||
@@ -162,7 +172,12 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className={wrapperClasses}>
|
<div className={wrapperClasses}>
|
||||||
<div>
|
<div>
|
||||||
{FLAGS.SUCCESSOR_MARKETS && <MarketSuccessorBanner market={market} />}
|
{FLAGS.SUCCESSOR_MARKETS && (
|
||||||
|
<>
|
||||||
|
<MarketSuccessorBanner market={market} />
|
||||||
|
<MarketSuccessorProposalBanner marketId={market?.id} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<OracleBanner marketId={market?.id || ''} />
|
<OracleBanner marketId={market?.id || ''} />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 p-0.5">
|
<div className="min-h-0 p-0.5">
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import { Splash } from '@vegaprotocol/ui-toolkit';
|
|||||||
import { NO_MARKET } from './constants';
|
import { NO_MARKET } from './constants';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { MarketSuccessorBanner } from '../../components/market-banner';
|
import {
|
||||||
|
MarketSuccessorBanner,
|
||||||
|
MarketSuccessorProposalBanner,
|
||||||
|
} from '../../components/market-banner';
|
||||||
import { FLAGS } from '@vegaprotocol/environment';
|
import { FLAGS } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
interface TradePanelsProps {
|
interface TradePanelsProps {
|
||||||
@@ -65,7 +68,12 @@ export const TradePanels = ({
|
|||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
|
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
|
||||||
<div>
|
<div>
|
||||||
{FLAGS.SUCCESSOR_MARKETS && <MarketSuccessorBanner market={market} />}
|
{FLAGS.SUCCESSOR_MARKETS && (
|
||||||
|
<>
|
||||||
|
<MarketSuccessorBanner market={market} />
|
||||||
|
<MarketSuccessorProposalBanner marketId={market?.id} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<OracleBanner marketId={market?.id || ''} />
|
<OracleBanner marketId={market?.id || ''} />
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
@@ -80,9 +88,12 @@ export const TradePanels = ({
|
|||||||
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||||
{Object.keys(TradingViews).map((key) => {
|
{Object.keys(TradingViews).map((key) => {
|
||||||
const isActive = view === key;
|
const isActive = view === key;
|
||||||
const className = classNames('p-4 min-w-[100px] capitalize', {
|
const className = classNames(
|
||||||
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
|
'py-2 px-4 min-w-[100px] capitalize text-sm',
|
||||||
});
|
{
|
||||||
|
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
|
||||||
|
}
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
data-testid={key}
|
data-testid={key}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { AccountsContainer } from '../../components/accounts-container';
|
|||||||
import { LiquidityContainer } from '../../components/liquidity-container';
|
import { LiquidityContainer } from '../../components/liquidity-container';
|
||||||
import type { OrderContainerProps } from '../../components/orders-container';
|
import type { OrderContainerProps } from '../../components/orders-container';
|
||||||
import { OrdersContainer } from '../../components/orders-container';
|
import { OrdersContainer } from '../../components/orders-container';
|
||||||
|
import { StopOrdersContainer } from '../../components/stop-orders-container';
|
||||||
|
|
||||||
type MarketDependantView =
|
type MarketDependantView =
|
||||||
| typeof CandlesChartContainer
|
| typeof CandlesChartContainer
|
||||||
@@ -74,6 +75,10 @@ export const TradingViews = {
|
|||||||
label: 'All',
|
label: 'All',
|
||||||
component: OrdersContainer,
|
component: OrdersContainer,
|
||||||
},
|
},
|
||||||
|
stopOrders: {
|
||||||
|
label: 'Stop',
|
||||||
|
component: StopOrdersContainer,
|
||||||
|
},
|
||||||
collateral: { label: 'Collateral', component: AccountsContainer },
|
collateral: { label: 'Collateral', component: AccountsContainer },
|
||||||
fills: { label: 'Fills', component: FillsContainer },
|
fills: { label: 'Fills', component: FillsContainer },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const Header = ({ title, children }: TradeMarketHeaderProps) => {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<header className={headerClasses}>
|
<header className={headerClasses}>
|
||||||
<div className="flex flex-col justify-center items-start pl-3 lg:pl-4 pt-2 xl:pb-2 pb-0">
|
<div className="hidden lg:flex flex-col justify-center items-start pl-3 lg:pl-4 pt-2 xl:pb-2 pb-0">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
<div data-testid="header-summary" className="min-w-0">
|
<div data-testid="header-summary" className="min-w-0">
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export const WalletIcon = ({ className }: { className?: string }) => {
|
export const WalletIcon = ({ className }: { className?: string }) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="26"
|
width="26"
|
||||||
height="18"
|
height="18"
|
||||||
viewBox="0 0 26 18"
|
viewBox="0 0 26 18"
|
||||||
className={className}
|
className={classNames('fill-current', className)}
|
||||||
data-testid="wallet-icon"
|
data-testid="wallet-icon"
|
||||||
>
|
>
|
||||||
<path d="M4.77437 17.7499H4.74987C3.6504 17.7368 2.77439 16.8489 2.77439 15.7772V12.8495V12.6343L2.5615 12.6023C1.59672 12.4575 0.849609 11.6116 0.849609 10.6266V7.40064C0.849609 6.39018 1.59509 5.56985 2.56147 5.4249L2.77439 5.39297V5.17767V2.24998C2.77439 1.14102 3.66537 0.25 4.77437 0.25H23.7501C24.8591 0.25 25.7501 1.14098 25.7501 2.24998V15.7499C25.7501 16.8588 24.8591 17.7499 23.7501 17.7499H4.77437ZM4.44917 12.5992H4.19917L4.77441 16.075V16.325H4.77466H23.7502C24.0778 16.325 24.3254 16.0777 24.3254 15.7497V2.24984C24.3254 1.9222 24.0782 1.6746 23.7502 1.6746H4.77441C4.44677 1.6746 4.19917 1.92182 4.19917 2.24984V5.12306V5.37306H4.44917H7.0244C8.51139 5.37306 9.67508 6.56094 9.67508 8.02374V9.94852C9.67508 11.4355 8.4872 12.5992 7.0244 12.5992H4.44917ZM2.84963 6.8253C2.52199 6.8253 2.27439 7.07253 2.27439 7.40054V10.6264C2.27439 10.9541 2.52161 11.2017 2.84962 11.2017L7.02419 11.2019C7.73619 11.2019 8.25009 10.6515 8.25009 9.97598V8.0512C8.25009 7.3392 7.69976 6.8253 7.0242 6.8253H2.84963Z" />
|
<path d="M4.77437 17.7499H4.74987C3.6504 17.7368 2.77439 16.8489 2.77439 15.7772V12.8495V12.6343L2.5615 12.6023C1.59672 12.4575 0.849609 11.6116 0.849609 10.6266V7.40064C0.849609 6.39018 1.59509 5.56985 2.56147 5.4249L2.77439 5.39297V5.17767V2.24998C2.77439 1.14102 3.66537 0.25 4.77437 0.25H23.7501C24.8591 0.25 25.7501 1.14098 25.7501 2.24998V15.7499C25.7501 16.8588 24.8591 17.7499 23.7501 17.7499H4.77437ZM4.44917 12.5992H4.19917L4.77441 16.075V16.325H4.77466H23.7502C24.0778 16.325 24.3254 16.0777 24.3254 15.7497V2.24984C24.3254 1.9222 24.0782 1.6746 23.7502 1.6746H4.77441C4.44677 1.6746 4.19917 1.92182 4.19917 2.24984V5.12306V5.37306H4.44917H7.0244C8.51139 5.37306 9.67508 6.56094 9.67508 8.02374V9.94852C9.67508 11.4355 8.4872 12.5992 7.0244 12.5992H4.44917ZM2.84963 6.8253C2.52199 6.8253 2.27439 7.07253 2.27439 7.40054V10.6264C2.27439 10.9541 2.52161 11.2017 2.84962 11.2017L7.02419 11.2019C7.73619 11.2019 8.25009 10.6515 8.25009 9.97598V8.0512C8.25009 7.3392 7.69976 6.8253 7.0242 6.8253H2.84963Z" />
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ export const LayoutWithSidebar = () => {
|
|||||||
|
|
||||||
const gridClasses = classNames(
|
const gridClasses = classNames(
|
||||||
'h-full relative z-0 grid',
|
'h-full relative z-0 grid',
|
||||||
'grid-rows-[min-content_1fr]',
|
'grid-rows-[min-content_1fr_40px]',
|
||||||
'grid-cols-[1fr_45px]',
|
'lg:grid-rows-[min-content_1fr]',
|
||||||
'lg:grid-cols-[1fr_350px_45px]'
|
'lg:grid-cols-[1fr_350px_40px]'
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -40,7 +40,14 @@ export const LayoutWithSidebar = () => {
|
|||||||
>
|
>
|
||||||
<SidebarContent />
|
<SidebarContent />
|
||||||
</aside>
|
</aside>
|
||||||
<div className="col-start-2 lg:col-start-3 bg-vega-clight-800 dark:bg-vega-cdark-800 border-l border-default">
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'bg-vega-clight-800 dark:bg-vega-cdark-800',
|
||||||
|
'border-t lg:border-l lg:border-t-0 border-default',
|
||||||
|
'row-start-3 col-start-1 cols-span-full',
|
||||||
|
'lg:row-start-2 lg:row-span-full lg:col-start-3'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ export const LiquidityHeader = () => {
|
|||||||
triggeringRatio,
|
triggeringRatio,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(market);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header
|
<Header
|
||||||
title={
|
title={
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './market-successor-banner';
|
export * from './market-successor-banner';
|
||||||
|
export * from './market-successor-proposal-banner';
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import type { SingleExecutionResult } from '@apollo/client';
|
||||||
|
import type { MockedResponse } from '@apollo/react-testing';
|
||||||
|
import { MockedProvider } from '@apollo/react-testing';
|
||||||
|
import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
|
||||||
|
import type { SuccessorProposalsListQuery } from '@vegaprotocol/proposals';
|
||||||
|
import { SuccessorProposalsListDocument } from '@vegaprotocol/proposals';
|
||||||
|
|
||||||
|
const marketProposalMock: MockedResponse<SuccessorProposalsListQuery> = {
|
||||||
|
request: {
|
||||||
|
query: SuccessorProposalsListDocument,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
proposalsConnection: {
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
__typename: 'Proposal',
|
||||||
|
id: 'proposal-1',
|
||||||
|
terms: {
|
||||||
|
__typename: 'ProposalTerms',
|
||||||
|
change: {
|
||||||
|
__typename: 'NewMarket',
|
||||||
|
instrument: {
|
||||||
|
name: 'New proposal of the market successor',
|
||||||
|
},
|
||||||
|
successorConfiguration: {
|
||||||
|
parentMarketId: 'marketId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('MarketSuccessorProposalBanner', () => {
|
||||||
|
it('should display single proposal', async () => {
|
||||||
|
render(
|
||||||
|
<MockedProvider mocks={[marketProposalMock]}>
|
||||||
|
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText('A successors to this market has been proposed')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen
|
||||||
|
.getByRole('link')
|
||||||
|
.getAttribute('href')
|
||||||
|
?.endsWith('/proposals/proposal-1') ?? false
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
it('should display plural proposals', async () => {
|
||||||
|
const dualProposalMock = {
|
||||||
|
...marketProposalMock,
|
||||||
|
result: {
|
||||||
|
...marketProposalMock.result,
|
||||||
|
data: {
|
||||||
|
proposalsConnection: {
|
||||||
|
edges: [
|
||||||
|
...((
|
||||||
|
marketProposalMock?.result as SingleExecutionResult<SuccessorProposalsListQuery>
|
||||||
|
)?.data?.proposalsConnection?.edges ?? []),
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
__typename: 'Proposal',
|
||||||
|
id: 'proposal-2',
|
||||||
|
terms: {
|
||||||
|
__typename: 'ProposalTerms',
|
||||||
|
change: {
|
||||||
|
__typename: 'NewMarket',
|
||||||
|
instrument: {
|
||||||
|
name: 'New second proposal of the market successor',
|
||||||
|
},
|
||||||
|
successorConfiguration: {
|
||||||
|
parentMarketId: 'marketId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MockedProvider mocks={[dualProposalMock]}>
|
||||||
|
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText('Successors to this market have been proposed')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen
|
||||||
|
.getAllByRole('link')[0]
|
||||||
|
.getAttribute('href')
|
||||||
|
?.endsWith('/proposals/proposal-1') ?? false
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
screen
|
||||||
|
.getAllByRole('link')[1]
|
||||||
|
.getAttribute('href')
|
||||||
|
?.endsWith('/proposals/proposal-2') ?? false
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('banner should be hidden because no proposals', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<MockedProvider>
|
||||||
|
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('banner should be hidden because no proposals for the market', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<MockedProvider mocks={[marketProposalMock]}>
|
||||||
|
<MarketSuccessorProposalBanner marketId="otherMarketId" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('banner should be hidden after user close click', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<MockedProvider mocks={[marketProposalMock]}>
|
||||||
|
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText('A successors to this market has been proposed')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
await act(() => {
|
||||||
|
screen.getByTestId('notification-banner-close').click();
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type {
|
||||||
|
SuccessorProposalListFieldsFragment,
|
||||||
|
NewMarketSuccessorFieldsFragment,
|
||||||
|
} from '@vegaprotocol/proposals';
|
||||||
|
import { useSuccessorProposalsListQuery } from '@vegaprotocol/proposals';
|
||||||
|
import {
|
||||||
|
ExternalLink,
|
||||||
|
Intent,
|
||||||
|
NotificationBanner,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
|
export const MarketSuccessorProposalBanner = ({
|
||||||
|
marketId,
|
||||||
|
}: {
|
||||||
|
marketId?: string;
|
||||||
|
}) => {
|
||||||
|
const { data: proposals } = useSuccessorProposalsListQuery({
|
||||||
|
skip: !marketId,
|
||||||
|
});
|
||||||
|
const successors =
|
||||||
|
proposals?.proposalsConnection?.edges
|
||||||
|
?.map((item) => item?.node as SuccessorProposalListFieldsFragment)
|
||||||
|
.filter(
|
||||||
|
(item: SuccessorProposalListFieldsFragment) =>
|
||||||
|
(item.terms?.change as NewMarketSuccessorFieldsFragment)
|
||||||
|
?.successorConfiguration?.parentMarketId === marketId
|
||||||
|
) ?? [];
|
||||||
|
const [visible, setVisible] = useState(true);
|
||||||
|
const tokenLink = useLinks(DApp.Token);
|
||||||
|
if (visible && successors.length) {
|
||||||
|
return (
|
||||||
|
<NotificationBanner
|
||||||
|
intent={Intent.Primary}
|
||||||
|
onClose={() => {
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="uppercase mb-1">
|
||||||
|
{successors.length === 1
|
||||||
|
? t('A successors to this market has been proposed')
|
||||||
|
: t('Successors to this market have been proposed')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{successors.length === 1
|
||||||
|
? t('Check out the terms of the proposal and vote:')
|
||||||
|
: t('Check out the terms of the proposals and vote:')}{' '}
|
||||||
|
{successors.map((item, i) => {
|
||||||
|
const externalLink = tokenLink(
|
||||||
|
TOKEN_PROPOSAL.replace(':id', item.id || '')
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ExternalLink href={externalLink} key={i}>
|
||||||
|
{
|
||||||
|
(item.terms?.change as NewMarketSuccessorFieldsFragment)
|
||||||
|
?.instrument.name
|
||||||
|
}
|
||||||
|
</ExternalLink>
|
||||||
|
{i < successors.length - 1 && ', '}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</NotificationBanner>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -4,10 +4,12 @@ import { useParams } from 'react-router-dom';
|
|||||||
import { MarketSelector } from '../../components/market-selector/market-selector';
|
import { MarketSelector } from '../../components/market-selector/market-selector';
|
||||||
import { MarketHeaderStats } from '../../client-pages/market/market-header-stats';
|
import { MarketHeaderStats } from '../../client-pages/market/market-header-stats';
|
||||||
import { useMarket } from '@vegaprotocol/markets';
|
import { useMarket } from '@vegaprotocol/markets';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
export const MarketHeader = () => {
|
export const MarketHeader = () => {
|
||||||
const { marketId } = useParams();
|
const { marketId } = useParams();
|
||||||
const { data } = useMarket(marketId);
|
const { data } = useMarket(marketId);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
@@ -15,6 +17,8 @@ export const MarketHeader = () => {
|
|||||||
<Header
|
<Header
|
||||||
title={
|
title={
|
||||||
<Popover
|
<Popover
|
||||||
|
open={open}
|
||||||
|
onChange={setOpen}
|
||||||
trigger={
|
trigger={
|
||||||
<HeaderTitle>
|
<HeaderTitle>
|
||||||
{data.tradableInstrument.instrument.code}
|
{data.tradableInstrument.instrument.code}
|
||||||
@@ -23,7 +27,10 @@ export const MarketHeader = () => {
|
|||||||
}
|
}
|
||||||
alignOffset={-10}
|
alignOffset={-10}
|
||||||
>
|
>
|
||||||
<MarketSelector currentMarketId={marketId} />
|
<MarketSelector
|
||||||
|
currentMarketId={marketId}
|
||||||
|
onSelect={() => setOpen(false)}
|
||||||
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ describe('MarketSelectorItem', () => {
|
|||||||
market={market}
|
market={market}
|
||||||
currentMarketId={market.id}
|
currentMarketId={market.id}
|
||||||
style={{}}
|
style={{}}
|
||||||
|
onSelect={jest.fn()}
|
||||||
/>
|
/>
|
||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ export const MarketSelectorItem = ({
|
|||||||
market,
|
market,
|
||||||
style,
|
style,
|
||||||
currentMarketId,
|
currentMarketId,
|
||||||
|
onSelect,
|
||||||
}: {
|
}: {
|
||||||
market: MarketMaybeWithDataAndCandles;
|
market: MarketMaybeWithDataAndCandles;
|
||||||
style: CSSProperties;
|
style: CSSProperties;
|
||||||
currentMarketId?: string;
|
currentMarketId?: string;
|
||||||
|
onSelect: (marketId: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div style={style} role="row">
|
<div style={style} role="row">
|
||||||
@@ -32,6 +34,7 @@ export const MarketSelectorItem = ({
|
|||||||
'bg-vega-clight-600 dark:bg-vega-cdark-600':
|
'bg-vega-clight-600 dark:bg-vega-cdark-600':
|
||||||
market.id === currentMarketId,
|
market.id === currentMarketId,
|
||||||
})}
|
})}
|
||||||
|
onClick={() => onSelect(market.id)}
|
||||||
>
|
>
|
||||||
<MarketData market={market} />
|
<MarketData market={market} />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -80,7 +83,7 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-2/5" role="gridcell">
|
<div className="w-2/5" role="gridcell">
|
||||||
<h3 className="text-ellipsis whitespace-nowrap overflow-hidden">
|
<h3 className="text-ellipsis text-sm lg:text-base whitespace-nowrap overflow-hidden">
|
||||||
{market.tradableInstrument.instrument.code}
|
{market.tradableInstrument.instrument.code}
|
||||||
</h3>
|
</h3>
|
||||||
{mode && (
|
{mode && (
|
||||||
@@ -90,7 +93,7 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="w-1/5 text-sm whitespace-nowrap text-ellipsis overflow-hidden"
|
className="w-1/5 text-xs lg:text-sm whitespace-nowrap text-ellipsis overflow-hidden"
|
||||||
title={instrument.product.settlementAsset.symbol}
|
title={instrument.product.settlementAsset.symbol}
|
||||||
data-testid="market-selector-price"
|
data-testid="market-selector-price"
|
||||||
role="gridcell"
|
role="gridcell"
|
||||||
@@ -98,7 +101,7 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
|||||||
{price} {instrument.product.settlementAsset.symbol}
|
{price} {instrument.product.settlementAsset.symbol}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="w-1/5 text-sm text-right whitespace-nowrap text-ellipsis overflow-hidden"
|
className="w-1/5 text-xs lg:text-sm text-right whitespace-nowrap text-ellipsis overflow-hidden"
|
||||||
title={t('24h vol')}
|
title={t('24h vol')}
|
||||||
data-testid="market-selector-volume"
|
data-testid="market-selector-volume"
|
||||||
role="gridcell"
|
role="gridcell"
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ describe('MarketSelector', () => {
|
|||||||
it('renders only active markets', () => {
|
it('renders only active markets', () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<MarketSelector currentMarketId="market-0" />
|
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(
|
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(
|
||||||
@@ -148,7 +148,7 @@ describe('MarketSelector', () => {
|
|||||||
it('filters by product type', async () => {
|
it('filters by product type', async () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<MarketSelector currentMarketId="market-0" />
|
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ describe('MarketSelector', () => {
|
|||||||
it('filters by search term', async () => {
|
it('filters by search term', async () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<MarketSelector currentMarketId="market-0" />
|
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ describe('MarketSelector', () => {
|
|||||||
it('filters by asset', async () => {
|
it('filters by asset', async () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<MarketSelector currentMarketId="market-0" />
|
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ describe('MarketSelector', () => {
|
|||||||
it('sorts by gained', async () => {
|
it('sorts by gained', async () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<MarketSelector currentMarketId="market-0" />
|
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@ describe('MarketSelector', () => {
|
|||||||
it('sorts by lost', async () => {
|
it('sorts by lost', async () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<MarketSelector currentMarketId="market-0" />
|
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ describe('MarketSelector', () => {
|
|||||||
it('sorts by new', async () => {
|
it('sorts by new', async () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<MarketSelector currentMarketId="market-0" />
|
<MarketSelector currentMarketId="market-0" onSelect={jest.fn()} />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const MarketSelector = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
}: {
|
}: {
|
||||||
currentMarketId?: string;
|
currentMarketId?: string;
|
||||||
onSelect?: (marketId: string) => void;
|
onSelect: (marketId: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [filter, setFilter] = useState<Filter>({
|
const [filter, setFilter] = useState<Filter>({
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
@@ -48,7 +48,7 @@ export const MarketSelector = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="market-selector">
|
<div data-testid="market-selector">
|
||||||
<div className="pt-2 px-2 mb-2 w-[320px] lg:w-[584px]">
|
<div className="pt-2 px-2 mb-2">
|
||||||
<ProductSelector
|
<ProductSelector
|
||||||
product={filter.product}
|
product={filter.product}
|
||||||
onSelect={(product) => {
|
onSelect={(product) => {
|
||||||
@@ -147,16 +147,17 @@ const MarketList = ({
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
currentMarketId?: string;
|
currentMarketId?: string;
|
||||||
onSelect?: (marketId: string) => void;
|
onSelect: (marketId: string) => void;
|
||||||
noItems: string;
|
noItems: string;
|
||||||
}) => {
|
}) => {
|
||||||
const itemSize = 45;
|
const itemSize = 45;
|
||||||
const listRef = useRef<HTMLDivElement | null>(null);
|
const listRef = useRef<HTMLDivElement | null>(null);
|
||||||
const rect = listRef.current?.getBoundingClientRect();
|
const rect = listRef.current?.getBoundingClientRect();
|
||||||
// allow virtualized list to grow until it runs out of space
|
// allow virtualized list to grow until it runs out of space
|
||||||
const height = rect
|
const computedHeight = rect
|
||||||
? Math.min(data.length * itemSize, window.innerHeight - rect.y)
|
? Math.min(data.length * itemSize, window.innerHeight - rect.y)
|
||||||
: 400;
|
: 400;
|
||||||
|
const height = Math.max(computedHeight, 45);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <div>{error.message}</div>;
|
return <div>{error.message}</div>;
|
||||||
@@ -199,7 +200,7 @@ const MarketList = ({
|
|||||||
|
|
||||||
interface ListItemData {
|
interface ListItemData {
|
||||||
data: MarketMaybeWithDataAndCandles[];
|
data: MarketMaybeWithDataAndCandles[];
|
||||||
onSelect?: (marketId: string) => void;
|
onSelect: (marketId: string) => void;
|
||||||
currentMarketId?: string;
|
currentMarketId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +217,7 @@ const ListItem = ({
|
|||||||
market={data.data[index]}
|
market={data.data[index]}
|
||||||
currentMarketId={data.currentMarketId}
|
currentMarketId={data.currentMarketId}
|
||||||
style={style}
|
style={style}
|
||||||
|
onSelect={data.onSelect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -252,7 +254,11 @@ const List = ({
|
|||||||
|
|
||||||
if (!data.length) {
|
if (!data.length) {
|
||||||
return (
|
return (
|
||||||
<div style={{ height }} data-testid="no-items">
|
<div
|
||||||
|
style={{ height }}
|
||||||
|
className="flex items-center"
|
||||||
|
data-testid="no-items"
|
||||||
|
>
|
||||||
<div className="mx-4 my-2 text-sm">{noItems}</div>
|
<div className="mx-4 my-2 text-sm">{noItems}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './navbar';
|
export * from './navbar';
|
||||||
|
export * from './nav-header';
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { MarketSelector } from '../market-selector';
|
||||||
|
import { useMarket } from '@vegaprotocol/markets';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is only rendered for the mobile navigation
|
||||||
|
*/
|
||||||
|
export const NavHeader = () => {
|
||||||
|
const { marketId } = useParams();
|
||||||
|
const { data } = useMarket(marketId);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
if (!marketId) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FullScreenPopover
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(x) => {
|
||||||
|
setOpen(x);
|
||||||
|
}}
|
||||||
|
trigger={
|
||||||
|
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-default text-lg whitespace-nowrap xl:pr-4 xl:border-r border-default">
|
||||||
|
{data ? data.tradableInstrument.instrument.code : t('Select market')}
|
||||||
|
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
|
||||||
|
</h1>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MarketSelector
|
||||||
|
currentMarketId={marketId}
|
||||||
|
onSelect={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
</FullScreenPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PopoverProps extends PopoverPrimitive.PopoverProps {
|
||||||
|
trigger: React.ReactNode | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FullScreenPopover = ({
|
||||||
|
trigger,
|
||||||
|
children,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: PopoverProps) => {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
||||||
|
<PopoverPrimitive.Trigger data-testid="popover-trigger">
|
||||||
|
{trigger}
|
||||||
|
</PopoverPrimitive.Trigger>
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-testid="popover-content"
|
||||||
|
className="w-screen bg-vega-clight-800 dark:bg-vega-cdark-800 text-default border border-default"
|
||||||
|
sideOffset={5}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PopoverPrimitive.Content>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
</PopoverPrimitive.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,42 +1,158 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen, within } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||||
import { Navbar } from './navbar';
|
import { Navbar } from './navbar';
|
||||||
|
import { useGlobalStore } from '../../stores';
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/proposals', () => ({
|
||||||
|
ProtocolUpgradeCountdown: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Navbar', () => {
|
describe('Navbar', () => {
|
||||||
const pubKey = 'pubKey';
|
const pubKey = '000';
|
||||||
it('should be properly rendered', () => {
|
const pubKeys = [
|
||||||
render(
|
{
|
||||||
<MockedProvider>
|
publicKey: pubKey,
|
||||||
<MemoryRouter>
|
name: 'Pub key 0',
|
||||||
<VegaWalletContext.Provider
|
},
|
||||||
value={{ pubKey } as VegaWalletContextShape}
|
{
|
||||||
>
|
publicKey: '111',
|
||||||
<Navbar theme="dark" />
|
name: 'Pub key 1',
|
||||||
</VegaWalletContext.Provider>
|
},
|
||||||
</MemoryRouter>
|
];
|
||||||
</MockedProvider>
|
const marketId = 'abc';
|
||||||
|
const navbarContent = 'navbar-menu-content';
|
||||||
|
|
||||||
|
const renderComponent = (
|
||||||
|
initialEntries?: string[],
|
||||||
|
walletContext?: Partial<VegaWalletContextShape>
|
||||||
|
) => {
|
||||||
|
const context = {
|
||||||
|
pubKey,
|
||||||
|
pubKeys,
|
||||||
|
selectPubKey: jest.fn(),
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
...walletContext,
|
||||||
|
} as VegaWalletContextShape;
|
||||||
|
return render(
|
||||||
|
<MemoryRouter initialEntries={initialEntries}>
|
||||||
|
<VegaWalletContext.Provider value={context}>
|
||||||
|
<Navbar />
|
||||||
|
</VegaWalletContext.Provider>
|
||||||
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId('Markets')).toBeInTheDocument();
|
};
|
||||||
expect(screen.getByTestId('Trading')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId('Portfolio')).toBeInTheDocument();
|
beforeAll(() => {
|
||||||
|
useGlobalStore.setState({ marketId });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be properly rendered', () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const expectedLinks = [
|
||||||
|
['/', ''],
|
||||||
|
['/markets/all', 'Markets'],
|
||||||
|
[`/markets/${marketId}`, 'Trading'],
|
||||||
|
['/portfolio', 'Portfolio'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const links = screen.getAllByRole('link');
|
||||||
|
|
||||||
|
links.forEach((link, i) => {
|
||||||
|
const [href, text] = expectedLinks[i];
|
||||||
|
expect(link).toHaveAttribute('href', href);
|
||||||
|
expect(link).toHaveTextContent(text);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Markets page route should not match empty market page', () => {
|
it('Markets page route should not match empty market page', () => {
|
||||||
render(
|
renderComponent(['/markets/all']);
|
||||||
<MockedProvider>
|
expect(screen.getByRole('link', { name: 'Markets' })).toHaveClass('active');
|
||||||
<MemoryRouter initialEntries={['/markets/all']}>
|
expect(screen.getByRole('link', { name: 'Trading' })).not.toHaveClass(
|
||||||
<VegaWalletContext.Provider
|
'active'
|
||||||
value={{ pubKey } as VegaWalletContextShape}
|
|
||||||
>
|
|
||||||
<Navbar theme="dark" />
|
|
||||||
</VegaWalletContext.Provider>
|
|
||||||
</MemoryRouter>
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId('Markets')).toHaveClass('active');
|
});
|
||||||
expect(screen.getByTestId('Trading')).not.toHaveClass('active');
|
|
||||||
|
it('can open menu and navigate on small screens', async () => {
|
||||||
|
renderComponent();
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'Menu' }));
|
||||||
|
|
||||||
|
const menuEl = screen.getByTestId(navbarContent);
|
||||||
|
expect(menuEl).toBeInTheDocument();
|
||||||
|
const menu = within(menuEl);
|
||||||
|
|
||||||
|
const expectedLinks = [
|
||||||
|
['/markets/all', 'Markets'],
|
||||||
|
[`/markets/${marketId}`, 'Trading'],
|
||||||
|
['/portfolio', 'Portfolio'],
|
||||||
|
];
|
||||||
|
const links = menu.getAllByRole('link');
|
||||||
|
links.forEach((link, i) => {
|
||||||
|
const [href, text] = expectedLinks[i];
|
||||||
|
expect(link).toHaveAttribute('href', href);
|
||||||
|
expect(link).toHaveTextContent(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'Close menu' }));
|
||||||
|
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can close menu by clicking overlay', async () => {
|
||||||
|
renderComponent();
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'Menu' }));
|
||||||
|
expect(screen.getByTestId(navbarContent)).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByTestId('navbar-menu-overlay'));
|
||||||
|
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can open wallet menu on small screens and change pubkey', async () => {
|
||||||
|
const mockSelectPubKey = jest.fn();
|
||||||
|
renderComponent(undefined, { selectPubKey: mockSelectPubKey });
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'Wallet' }));
|
||||||
|
|
||||||
|
const menuEl = screen.getByTestId(navbarContent);
|
||||||
|
expect(menuEl).toBeInTheDocument();
|
||||||
|
const menu = within(menuEl);
|
||||||
|
|
||||||
|
expect(menu.getAllByTestId(/key-\d+-mobile/)).toHaveLength(pubKeys.length);
|
||||||
|
|
||||||
|
const activeKey = within(menu.getByTestId('key-000-mobile'));
|
||||||
|
expect(activeKey.getByText(pubKeys[0].name)).toBeInTheDocument();
|
||||||
|
expect(activeKey.getByTestId('icon-tick')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const inactiveKey = within(menu.getByTestId('key-111-mobile'));
|
||||||
|
await userEvent.click(inactiveKey.getByText(pubKeys[1].name));
|
||||||
|
expect(mockSelectPubKey).toHaveBeenCalledWith(pubKeys[1].publicKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can transfer and close menu', async () => {
|
||||||
|
renderComponent();
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'Wallet' }));
|
||||||
|
|
||||||
|
const menuEl = screen.getByTestId(navbarContent);
|
||||||
|
expect(menuEl).toBeInTheDocument();
|
||||||
|
const menu = within(menuEl);
|
||||||
|
|
||||||
|
await userEvent.click(menu.getByText('Transfer'));
|
||||||
|
|
||||||
|
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can disconnect and close menu', async () => {
|
||||||
|
const mockDisconnect = jest.fn();
|
||||||
|
renderComponent(undefined, { disconnect: mockDisconnect });
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: 'Wallet' }));
|
||||||
|
|
||||||
|
const menuEl = screen.getByTestId(navbarContent);
|
||||||
|
expect(menuEl).toBeInTheDocument();
|
||||||
|
const menu = within(menuEl);
|
||||||
|
|
||||||
|
await userEvent.click(menu.getByText('Disconnect'));
|
||||||
|
|
||||||
|
expect(mockDisconnect).toHaveBeenCalled();
|
||||||
|
expect(screen.queryByTestId(navbarContent)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,140 +1,400 @@
|
|||||||
import type { ComponentProps, ReactNode } from 'react';
|
import type { ButtonHTMLAttributes, LiHTMLAttributes, ReactNode } from 'react';
|
||||||
import {
|
import { useState } from 'react';
|
||||||
DApp,
|
import { useEnvironment, DocsLinks, Networks } from '@vegaprotocol/environment';
|
||||||
NetworkSwitcher,
|
|
||||||
TOKEN_GOVERNANCE,
|
|
||||||
useEnvironment,
|
|
||||||
useLinks,
|
|
||||||
DocsLinks,
|
|
||||||
} from '@vegaprotocol/environment';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useGlobalStore } from '../../stores';
|
import { useGlobalStore } from '../../stores';
|
||||||
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
|
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
|
||||||
import {
|
import { VegaIconNames, VegaIcon, VLogo } from '@vegaprotocol/ui-toolkit';
|
||||||
Navigation,
|
import * as N from '@radix-ui/react-navigation-menu';
|
||||||
NavigationList,
|
import * as D from '@radix-ui/react-dialog';
|
||||||
NavigationItem,
|
import { NavLink } from 'react-router-dom';
|
||||||
NavigationLink,
|
|
||||||
ExternalLink,
|
|
||||||
NavigationBreakpoint,
|
|
||||||
NavigationTrigger,
|
|
||||||
NavigationContent,
|
|
||||||
VegaIconNames,
|
|
||||||
VegaIcon,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
import { Links, Routes } from '../../pages/client-router';
|
import { Links, Routes } from '../../pages/client-router';
|
||||||
import {
|
import classNames from 'classnames';
|
||||||
ProtocolUpgradeCountdown,
|
import { VegaWalletMenu } from '../vega-wallet';
|
||||||
ProtocolUpgradeCountdownMode,
|
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||||
} from '@vegaprotocol/proposals';
|
import { WalletIcon } from '../icons/wallet';
|
||||||
|
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
|
||||||
|
|
||||||
|
type MenuState = 'wallet' | 'nav' | null;
|
||||||
|
type Theme = 'system' | 'yellow';
|
||||||
|
|
||||||
export const Navbar = ({
|
export const Navbar = ({
|
||||||
|
children,
|
||||||
theme = 'system',
|
theme = 'system',
|
||||||
}: {
|
}: {
|
||||||
theme: ComponentProps<typeof Navigation>['theme'];
|
children?: ReactNode;
|
||||||
|
theme?: Theme;
|
||||||
}) => {
|
}) => {
|
||||||
const { GITHUB_FEEDBACK_URL } = useEnvironment();
|
// menu state for small screens
|
||||||
const tokenLink = useLinks(DApp.Token);
|
const [menu, setMenu] = useState<MenuState>(null);
|
||||||
|
|
||||||
|
const { pubKey } = useVegaWallet();
|
||||||
|
|
||||||
|
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||||
|
(store) => store.openVegaWalletDialog
|
||||||
|
);
|
||||||
|
|
||||||
|
const isConnected = pubKey !== null;
|
||||||
|
|
||||||
|
const navTextClasses = 'text-vega-clight-200 dark:text-vega-cdark-200';
|
||||||
|
const rootClasses = classNames(
|
||||||
|
navTextClasses,
|
||||||
|
'flex gap-3 h-10 pr-1',
|
||||||
|
'border-b border-default',
|
||||||
|
'bg-vega-clight-800 dark:bg-vega-cdark-800'
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<N.Root className={rootClasses}>
|
||||||
|
<NavLink
|
||||||
|
to="/"
|
||||||
|
className={classNames('flex items-center px-3', {
|
||||||
|
'bg-vega-yellow text-vega-clight-50': theme === 'yellow',
|
||||||
|
'text-default': theme === 'system',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<VLogo className="w-4" />
|
||||||
|
</NavLink>
|
||||||
|
{/* Left section */}
|
||||||
|
<div className="lg:hidden flex items-center">{children}</div>
|
||||||
|
{/* Used to show header in nav on mobile */}
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<NavbarMenu onClick={() => setMenu(null)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right section */}
|
||||||
|
<div className="ml-auto flex justify-end items-center gap-2">
|
||||||
|
<ProtocolUpgradeCountdown />
|
||||||
|
<NavbarMobileButton
|
||||||
|
onClick={() => {
|
||||||
|
if (isConnected) {
|
||||||
|
setMenu((x) => (x === 'wallet' ? null : 'wallet'));
|
||||||
|
} else {
|
||||||
|
openVegaWalletDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data-testid="navbar-mobile-wallet"
|
||||||
|
>
|
||||||
|
<span className="sr-only">{t('Wallet')}</span>
|
||||||
|
<WalletIcon className="w-6" />
|
||||||
|
</NavbarMobileButton>
|
||||||
|
<NavbarMobileButton
|
||||||
|
onClick={() => {
|
||||||
|
setMenu((x) => (x === 'nav' ? null : 'nav'));
|
||||||
|
}}
|
||||||
|
data-testid="navbar-mobile-burger"
|
||||||
|
>
|
||||||
|
<span className="sr-only">{t('Menu')}</span>
|
||||||
|
<BurgerIcon />
|
||||||
|
</NavbarMobileButton>
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<VegaWalletConnectButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{menu !== null && (
|
||||||
|
<D.Root
|
||||||
|
open={menu !== null}
|
||||||
|
onOpenChange={(open) => setMenu((x) => (open ? x : null))}
|
||||||
|
>
|
||||||
|
<D.Overlay
|
||||||
|
className="lg:hidden fixed inset-0 dark:bg-black/80 bg-black/50 z-20"
|
||||||
|
data-testid="navbar-menu-overlay"
|
||||||
|
/>
|
||||||
|
<D.Content
|
||||||
|
className={classNames(
|
||||||
|
'lg:hidden',
|
||||||
|
'fixed top-0 right-0 z-20 w-3/4 h-screen border-l border-default bg-vega-clight-700 dark:bg-vega-cdark-700',
|
||||||
|
navTextClasses
|
||||||
|
)}
|
||||||
|
data-testid="navbar-menu-content"
|
||||||
|
>
|
||||||
|
<div className="flex justify-end items-center h-10 p-1">
|
||||||
|
<NavbarMobileButton onClick={() => setMenu(null)}>
|
||||||
|
<span className="sr-only">{t('Close menu')}</span>
|
||||||
|
<VegaIcon name={VegaIconNames.CROSS} size={24} />
|
||||||
|
</NavbarMobileButton>
|
||||||
|
</div>
|
||||||
|
{menu === 'nav' && <NavbarMenu onClick={() => setMenu(null)} />}
|
||||||
|
{menu === 'wallet' && <VegaWalletMenu setMenu={setMenu} />}
|
||||||
|
</D.Content>
|
||||||
|
</D.Root>
|
||||||
|
)}
|
||||||
|
</N.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of links or dropdown triggers to show in the main section
|
||||||
|
* of the navigation
|
||||||
|
*/
|
||||||
|
const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
||||||
|
const { VEGA_ENV, VEGA_NETWORKS, GITHUB_FEEDBACK_URL } = useEnvironment();
|
||||||
const marketId = useGlobalStore((store) => store.marketId);
|
const marketId = useGlobalStore((store) => store.marketId);
|
||||||
|
|
||||||
|
// If we have a stored marketId make Trade link go to that market
|
||||||
|
// otherwise always go to /markets/all
|
||||||
const tradingPath = marketId
|
const tradingPath = marketId
|
||||||
? Links[Routes.MARKET](marketId)
|
? Links[Routes.MARKET](marketId)
|
||||||
: Links[Routes.MARKET]();
|
: Links[Routes.MARKET]('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navigation
|
<div className="lg:flex lg:h-full gap-3">
|
||||||
appName="console"
|
<NavbarList>
|
||||||
theme={theme}
|
<NavbarItem>
|
||||||
actions={
|
<NavbarTrigger data-testid="navbar-network-switcher-trigger">
|
||||||
<>
|
{envNameMapping[VEGA_ENV]}
|
||||||
<ProtocolUpgradeCountdown
|
</NavbarTrigger>
|
||||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
<NavbarContent data-testid="navbar-content-network-switcher">
|
||||||
/>
|
<ul className="lg:p-4">
|
||||||
<VegaWalletConnectButton />
|
{[Networks.MAINNET, Networks.TESTNET].map((n) => {
|
||||||
</>
|
const url = VEGA_NETWORKS[n];
|
||||||
}
|
if (!url) return;
|
||||||
breakpoints={[521, 1122]}
|
return (
|
||||||
>
|
<NavbarSubItem key={n}>
|
||||||
<NavigationList
|
<NavbarLink to={url}>{envNameMapping[n]}</NavbarLink>
|
||||||
className="[.drawer-content_&]:border-b [.drawer-content_&]:border-b-vega-light-200 dark:[.drawer-content_&]:border-b-vega-dark-200 [.drawer-content_&]:pb-8 [.drawer-content_&]:mb-2"
|
</NavbarSubItem>
|
||||||
hide={[NavigationBreakpoint.Small]}
|
);
|
||||||
>
|
})}
|
||||||
<NavigationItem className="[.drawer-content_&]:w-full">
|
</ul>
|
||||||
<NetworkSwitcher className="[.drawer-content_&]:w-full" />
|
</NavbarContent>
|
||||||
</NavigationItem>
|
</NavbarItem>
|
||||||
</NavigationList>
|
</NavbarList>
|
||||||
<NavigationList
|
<NavbarListDivider />
|
||||||
hide={[NavigationBreakpoint.Narrow, NavigationBreakpoint.Small]}
|
<NavbarList>
|
||||||
>
|
<NavbarItem>
|
||||||
<NavigationItem>
|
<NavbarLink to={Links[Routes.MARKETS]()} onClick={onClick}>
|
||||||
<NavigationLink data-testid="Markets" to={Links[Routes.MARKETS]()}>
|
|
||||||
{t('Markets')}
|
{t('Markets')}
|
||||||
</NavigationLink>
|
</NavbarLink>
|
||||||
</NavigationItem>
|
</NavbarItem>
|
||||||
<NavigationItem>
|
<NavbarItem>
|
||||||
<NavigationLink data-testid="Trading" to={tradingPath} end>
|
<NavbarLink to={tradingPath} onClick={onClick}>
|
||||||
{t('Trading')}
|
{t('Trading')}
|
||||||
</NavigationLink>
|
</NavbarLink>
|
||||||
</NavigationItem>
|
</NavbarItem>
|
||||||
<NavigationItem>
|
<NavbarItem>
|
||||||
<NavigationLink
|
<NavbarLink to={Links[Routes.PORTFOLIO]()} onClick={onClick}>
|
||||||
data-testid="Portfolio"
|
|
||||||
to={Links[Routes.PORTFOLIO]()}
|
|
||||||
>
|
|
||||||
{t('Portfolio')}
|
{t('Portfolio')}
|
||||||
</NavigationLink>
|
</NavbarLink>
|
||||||
</NavigationItem>
|
</NavbarItem>
|
||||||
<NavigationItem>
|
<NavbarItem>
|
||||||
<NavExternalLink href={tokenLink(TOKEN_GOVERNANCE)}>
|
<NavbarTrigger>{t('Resources')}</NavbarTrigger>
|
||||||
{t('Governance')}
|
<NavbarContent data-testid="navbar-content-resources">
|
||||||
</NavExternalLink>
|
<ul className="lg:p-4">
|
||||||
</NavigationItem>
|
{DocsLinks?.NEW_TO_VEGA && (
|
||||||
{DocsLinks?.NEW_TO_VEGA && GITHUB_FEEDBACK_URL && (
|
<NavbarSubItem>
|
||||||
<NavigationItem>
|
<NavbarLinkExternal to={DocsLinks?.NEW_TO_VEGA}>
|
||||||
<NavigationTrigger>{t('Resources')}</NavigationTrigger>
|
|
||||||
<NavigationContent>
|
|
||||||
<NavigationList>
|
|
||||||
<NavigationItem>
|
|
||||||
<NavExternalLink href={DocsLinks.NEW_TO_VEGA}>
|
|
||||||
{t('Docs')}
|
{t('Docs')}
|
||||||
</NavExternalLink>
|
</NavbarLinkExternal>
|
||||||
</NavigationItem>
|
</NavbarSubItem>
|
||||||
<NavigationItem>
|
)}
|
||||||
<NavExternalLink href={GITHUB_FEEDBACK_URL}>
|
{GITHUB_FEEDBACK_URL && (
|
||||||
|
<NavbarSubItem>
|
||||||
|
<NavbarLinkExternal to={GITHUB_FEEDBACK_URL}>
|
||||||
{t('Give Feedback')}
|
{t('Give Feedback')}
|
||||||
</NavExternalLink>
|
</NavbarLinkExternal>
|
||||||
</NavigationItem>
|
</NavbarSubItem>
|
||||||
<NavigationItem>
|
)}
|
||||||
<NavigationLink
|
<NavbarSubItem>
|
||||||
data-testid="Disclaimer"
|
<NavbarLink to={Links[Routes.DISCLAIMER]()} onClick={onClick}>
|
||||||
to={Links[Routes.DISCLAIMER]()}
|
{t('Disclaimer')}
|
||||||
>
|
</NavbarLink>
|
||||||
{t('Disclaimer')}
|
</NavbarSubItem>
|
||||||
</NavigationLink>
|
</ul>
|
||||||
</NavigationItem>
|
</NavbarContent>
|
||||||
</NavigationList>
|
</NavbarItem>
|
||||||
</NavigationContent>
|
</NavbarList>
|
||||||
</NavigationItem>
|
</div>
|
||||||
)}
|
|
||||||
</NavigationList>
|
|
||||||
</Navigation>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NavExternalLink = ({
|
/**
|
||||||
|
* Wrapper for radix-ux Trigger for consistent styles
|
||||||
|
*/
|
||||||
|
const NavbarTrigger = ({
|
||||||
children,
|
children,
|
||||||
href,
|
...props
|
||||||
|
}: N.NavigationMenuTriggerProps) => {
|
||||||
|
return (
|
||||||
|
<N.Trigger
|
||||||
|
{...props}
|
||||||
|
onPointerMove={preventHover}
|
||||||
|
onPointerLeave={preventHover}
|
||||||
|
className={classNames(
|
||||||
|
'w-full lg:w-auto lg:h-full',
|
||||||
|
'flex items-center justify-between lg:justify-center gap-2 px-6 py-2 lg:p-0',
|
||||||
|
'text-lg lg:text-sm',
|
||||||
|
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />
|
||||||
|
</N.Trigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for react-router-dom NavLink for consistent styles
|
||||||
|
*/
|
||||||
|
const NavbarLink = ({
|
||||||
|
children,
|
||||||
|
to,
|
||||||
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
href: string;
|
to: string;
|
||||||
|
onClick?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ExternalLink href={href}>
|
<N.Link asChild={true}>
|
||||||
<span className="flex items-center gap-2">
|
<NavLink
|
||||||
<span>{children}</span>
|
to={to}
|
||||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
className={classNames(
|
||||||
</span>
|
'block lg:flex lg:h-full flex-col justify-center',
|
||||||
</ExternalLink>
|
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
|
||||||
|
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{({ isActive }) => {
|
||||||
|
const borderClasses = {
|
||||||
|
'border-b-2': true,
|
||||||
|
'border-transparent': !isActive,
|
||||||
|
'border-vega-yellow lg:group-[.navbar-content]:border-transparent':
|
||||||
|
isActive,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={classNames('lg:border-0', borderClasses, {
|
||||||
|
'text-vega-clight-50 dark:text-vega-cdark-50': isActive,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'hidden lg:block absolute left-0 bottom-0 w-full h-0',
|
||||||
|
borderClasses
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</NavLink>
|
||||||
|
</N.Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NavbarItem = (props: N.NavigationMenuItemProps) => {
|
||||||
|
return <N.Item {...props} className="relative" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NavbarSubItem = (props: LiHTMLAttributes<HTMLElement>) => {
|
||||||
|
return <li {...props} className="lg:mb-4 lg:last:mb-0" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NavbarList = (props: N.NavigationMenuListProps) => {
|
||||||
|
return <N.List {...props} className="lg:flex lg:h-full gap-6" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content that gets rendered when a sub section of the navbar is shown
|
||||||
|
*/
|
||||||
|
const NavbarContent = (props: N.NavigationMenuContentProps) => {
|
||||||
|
return (
|
||||||
|
<N.Content
|
||||||
|
{...props}
|
||||||
|
className={classNames(
|
||||||
|
'group navbar-content',
|
||||||
|
'lg:absolute lg:mt-2 pl-2 lg:pl-0 z-20 lg:min-w-[290px]',
|
||||||
|
'lg:bg-vega-clight-700 lg:dark:bg-vega-cdark-700',
|
||||||
|
'lg:border border-vega-clight-500 dark:border-vega-cdark-500 lg:rounded'
|
||||||
|
)}
|
||||||
|
onPointerEnter={preventHover}
|
||||||
|
onPointerLeave={preventHover}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NavbarLink with OPEN_EXTERNAL icon
|
||||||
|
*/
|
||||||
|
const NavbarLinkExternal = ({
|
||||||
|
children,
|
||||||
|
to,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
to: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<N.Link asChild={true}>
|
||||||
|
<NavLink
|
||||||
|
to={to}
|
||||||
|
className={classNames(
|
||||||
|
'flex lg:inline-flex gap-2 justify-between items-center relative',
|
||||||
|
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
|
||||||
|
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span>{children}</span>
|
||||||
|
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
||||||
|
</NavLink>
|
||||||
|
</N.Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BurgerIcon = () => (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
className="w-full stroke-current"
|
||||||
|
>
|
||||||
|
<line x1={0.5} x2={15.5} y1={3.5} y2={3.5} />
|
||||||
|
<line x1={0.5} x2={15.5} y1={11.5} y2={11.5} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const NavbarListDivider = () => {
|
||||||
|
return (
|
||||||
|
<div className="py-2 px-6 lg:px-0" role="separator">
|
||||||
|
<div className="h-px lg:h-full w-full lg:w-px bg-vega-clight-500 dark:bg-vega-cdark-500" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button component to avoid repeating styles for buttons shown on small screens
|
||||||
|
*/
|
||||||
|
const NavbarMobileButton = (props: ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
className={classNames(
|
||||||
|
'w-8 h-8 lg:hidden flex items-center p-1 rounded ',
|
||||||
|
'hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500',
|
||||||
|
'hover:text-vega-clight-50 dark:hover:text-vega-cdark-50'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const envNameMapping: Record<Networks, string> = {
|
||||||
|
[Networks.VALIDATOR_TESTNET]: t('VALIDATOR_TESTNET'),
|
||||||
|
[Networks.CUSTOM]: t('Custom'),
|
||||||
|
[Networks.DEVNET]: t('Devnet'),
|
||||||
|
[Networks.STAGNET1]: t('Stagnet'),
|
||||||
|
[Networks.TESTNET]: t('Fairground testnet'),
|
||||||
|
[Networks.MAINNET_MIRROR]: t('Mirror'),
|
||||||
|
[Networks.MAINNET]: t('Mainnet'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://github.com/radix-ui/primitives/issues/1630
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const preventHover = (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { OrderbookManager } from '@vegaprotocol/market-depth';
|
import { OrderbookManager } from '@vegaprotocol/market-depth';
|
||||||
import { useCreateOrderStore } from '@vegaprotocol/orders';
|
import { useCreateOrderStore } from '@vegaprotocol/orders';
|
||||||
import { ViewType, useSidebar } from '../sidebar';
|
import { ViewType, useSidebar } from '../sidebar';
|
||||||
|
import { useStopOrderFormValues } from '@vegaprotocol/deal-ticket';
|
||||||
|
|
||||||
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
||||||
const useOrderStoreRef = useCreateOrderStore();
|
const useOrderStoreRef = useCreateOrderStore();
|
||||||
const updateOrder = useOrderStoreRef((store) => store.update);
|
const updateOrder = useOrderStoreRef((store) => store.update);
|
||||||
|
const updateStoredFormValues = useStopOrderFormValues(
|
||||||
|
(state) => state.update
|
||||||
|
);
|
||||||
const setView = useSidebar((store) => store.setView);
|
const setView = useSidebar((store) => store.setView);
|
||||||
return (
|
return (
|
||||||
<OrderbookManager
|
<OrderbookManager
|
||||||
@@ -12,9 +16,11 @@ export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
|||||||
onClick={({ price, size }) => {
|
onClick={({ price, size }) => {
|
||||||
if (price) {
|
if (price) {
|
||||||
updateOrder(marketId, { price });
|
updateOrder(marketId, { price });
|
||||||
|
updateStoredFormValues(marketId, { price });
|
||||||
}
|
}
|
||||||
if (size) {
|
if (size) {
|
||||||
updateOrder(marketId, { size });
|
updateOrder(marketId, { size });
|
||||||
|
updateStoredFormValues(marketId, { size });
|
||||||
}
|
}
|
||||||
setView({ type: ViewType.Order });
|
setView({ type: ViewType.Order });
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -51,9 +51,10 @@ type SidebarView =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
|
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 h-full py-1" data-testid="sidebar">
|
<div className="flex lg:flex-col gap-2 h-full p-1" data-testid="sidebar">
|
||||||
<nav className="flex flex-col items-center gap-4 p-1">
|
<nav className={navClasses}>
|
||||||
{/* sidebar options that always show */}
|
{/* sidebar options that always show */}
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
view={ViewType.Deposit}
|
view={ViewType.Deposit}
|
||||||
@@ -102,7 +103,7 @@ export const Sidebar = () => {
|
|||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</nav>
|
</nav>
|
||||||
<nav className="mt-auto flex flex-col items-center gap-4 p-1">
|
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
view={ViewType.Settings}
|
view={ViewType.Settings}
|
||||||
icon={VegaIconNames.COG}
|
icon={VegaIconNames.COG}
|
||||||
@@ -161,7 +162,7 @@ const SidebarButton = ({
|
|||||||
const SidebarDivider = () => {
|
const SidebarDivider = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="bg-vega-clight-600 dark:bg-vega-cdark-600 w-4 h-px"
|
className="bg-vega-clight-600 dark:bg-vega-cdark-600 w-px h-4 lg:w-4 lg:h-px"
|
||||||
role="separator"
|
role="separator"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './stop-orders-container';
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { StopOrdersManager } from '@vegaprotocol/orders';
|
||||||
|
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
import type { DataGridSlice } from '../../stores/datagrid-store-slice';
|
||||||
|
import { createDataGridSlice } from '../../stores/datagrid-store-slice';
|
||||||
|
|
||||||
|
export const StopOrdersContainer = () => {
|
||||||
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
const onMarketClick = useMarketClickHandler(true);
|
||||||
|
|
||||||
|
const gridStore = useStopOrdersStore((store) => store.gridStore);
|
||||||
|
const updateGridStore = useStopOrdersStore((store) => store.updateGridStore);
|
||||||
|
|
||||||
|
const gridStoreCallbacks = useDataGridEvents(gridStore, (colState) => {
|
||||||
|
updateGridStore(colState);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pubKey) {
|
||||||
|
return <Splash>{t('Please connect Vega wallet')}</Splash>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StopOrdersManager
|
||||||
|
partyId={pubKey}
|
||||||
|
onMarketClick={onMarketClick}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
gridProps={gridStoreCallbacks}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStopOrdersStore = create<DataGridSlice>()(
|
||||||
|
persist(createDataGridSlice, {
|
||||||
|
name: 'vega_fills_store',
|
||||||
|
})
|
||||||
|
);
|
||||||
+1
-1
@@ -28,7 +28,7 @@ describe('VegaWalletConnectButton', () => {
|
|||||||
render(generateJsx({ pubKey: null } as VegaWalletContextShape));
|
render(generateJsx({ pubKey: null } as VegaWalletContextShape));
|
||||||
|
|
||||||
const button = screen.getByTestId('connect-vega-wallet');
|
const button = screen.getByTestId('connect-vega-wallet');
|
||||||
expect(button).toHaveTextContent('Connect Vega wallet');
|
expect(button).toHaveTextContent('Connect');
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
expect(mockUpdateDialogOpen).toHaveBeenCalled();
|
expect(mockUpdateDialogOpen).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
+96
-252
@@ -1,148 +1,26 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { truncateByChars } from '@vegaprotocol/utils';
|
import { truncateByChars } from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuItemIndicator,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
Drawer,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
VegaIcon,
|
VegaIcon,
|
||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
|
TradingButton as Button,
|
||||||
|
Intent,
|
||||||
|
TradingDropdown,
|
||||||
|
TradingDropdownTrigger,
|
||||||
|
TradingDropdownContent,
|
||||||
|
TradingDropdownRadioGroup,
|
||||||
|
TradingDropdownSeparator,
|
||||||
|
TradingDropdownItem,
|
||||||
|
TradingDropdownRadioItem,
|
||||||
|
TradingDropdownItemIndicator,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import type { PubKey } from '@vegaprotocol/wallet';
|
import type { PubKey } from '@vegaprotocol/wallet';
|
||||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
|
||||||
import { WalletIcon } from '../icons/wallet';
|
|
||||||
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
|
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
|
||||||
import { ViewType, useSidebar } from '../sidebar';
|
import { ViewType, useSidebar } from '../sidebar';
|
||||||
|
import classNames from 'classnames';
|
||||||
const MobileWalletButton = ({
|
|
||||||
isConnected,
|
|
||||||
activeKey,
|
|
||||||
}: {
|
|
||||||
isConnected?: boolean;
|
|
||||||
activeKey?: PubKey;
|
|
||||||
}) => {
|
|
||||||
const { pubKeys, selectPubKey, disconnect, fetchPubKeys } = useVegaWallet();
|
|
||||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
|
||||||
(store) => store.openVegaWalletDialog
|
|
||||||
);
|
|
||||||
const setView = useSidebar((store) => store.setView);
|
|
||||||
const { VEGA_ENV } = useEnvironment();
|
|
||||||
const isYellow = VEGA_ENV === Networks.TESTNET;
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
||||||
const mobileDisconnect = useCallback(() => {
|
|
||||||
setDrawerOpen(false);
|
|
||||||
disconnect();
|
|
||||||
}, [disconnect]);
|
|
||||||
const openDrawer = useCallback(() => {
|
|
||||||
if (!isConnected) {
|
|
||||||
openVegaWalletDialog();
|
|
||||||
setDrawerOpen(false);
|
|
||||||
} else {
|
|
||||||
if (fetchPubKeys) {
|
|
||||||
fetchPubKeys();
|
|
||||||
}
|
|
||||||
setDrawerOpen(!drawerOpen);
|
|
||||||
}
|
|
||||||
}, [drawerOpen, fetchPubKeys, isConnected, openVegaWalletDialog]);
|
|
||||||
|
|
||||||
const iconClass = drawerOpen
|
|
||||||
? 'hidden'
|
|
||||||
: isYellow
|
|
||||||
? 'fill-black'
|
|
||||||
: 'fill-white';
|
|
||||||
const [container, setContainer] = useState<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
const walletButton = (
|
|
||||||
<button
|
|
||||||
className="my-2 transition-all flex flex-col justify-around gap-3 p-2 relative h-[34px]"
|
|
||||||
onClick={openDrawer}
|
|
||||||
data-testid="connect-vega-wallet-mobile"
|
|
||||||
>
|
|
||||||
<WalletIcon className={iconClass} />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
const onSelectItem = useCallback(
|
|
||||||
(pubkey: string) => {
|
|
||||||
setDrawerOpen(false);
|
|
||||||
selectPubKey(pubkey);
|
|
||||||
},
|
|
||||||
[selectPubKey]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className="lg:hidden overflow-hidden flex" ref={setContainer}>
|
|
||||||
<Drawer
|
|
||||||
dataTestId="wallets-drawer"
|
|
||||||
open={drawerOpen}
|
|
||||||
onChange={setDrawerOpen}
|
|
||||||
container={container}
|
|
||||||
trigger={walletButton}
|
|
||||||
>
|
|
||||||
<div className="border-l border-default p-2 gap-4 flex flex-col w-full h-full bg-white dark:bg-black dark:text-white justify-between">
|
|
||||||
<div className="flex h-5 justify-end">
|
|
||||||
<button
|
|
||||||
className="transition-all flex flex-col justify-around gap-3 p-2 relative h-[34px]"
|
|
||||||
onClick={() => setDrawerOpen(false)}
|
|
||||||
data-testid="connect-vega-wallet-mobile-close"
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'w-[26px] h-[2px] bg-black dark:bg-white transition-all translate-y-[7.5px] rotate-45',
|
|
||||||
{
|
|
||||||
hidden: !drawerOpen,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'w-[26px] h-[2px] bg-black dark:bg-white transition-all -translate-y-[7.5px] -rotate-45',
|
|
||||||
{
|
|
||||||
hidden: !drawerOpen,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="grow my-4" role="list">
|
|
||||||
{(pubKeys || []).map((pk) => (
|
|
||||||
<KeypairListItem
|
|
||||||
key={pk.publicKey}
|
|
||||||
pk={pk}
|
|
||||||
isActive={activeKey?.publicKey === pk.publicKey}
|
|
||||||
onSelectItem={onSelectItem}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2 m-4">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setDrawerOpen(false);
|
|
||||||
setView({ type: ViewType.Transfer });
|
|
||||||
}}
|
|
||||||
fill
|
|
||||||
>
|
|
||||||
{t('Transfer')}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={mobileDisconnect} fill>
|
|
||||||
{t('Disconnect')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VegaWalletConnectButton = () => {
|
export const VegaWalletConnectButton = () => {
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
@@ -166,96 +44,101 @@ export const VegaWalletConnectButton = () => {
|
|||||||
|
|
||||||
if (isConnected && pubKeys) {
|
if (isConnected && pubKeys) {
|
||||||
return (
|
return (
|
||||||
<>
|
<TradingDropdown
|
||||||
<div className="hidden lg:block">
|
open={dropdownOpen}
|
||||||
<DropdownMenu
|
trigger={
|
||||||
open={dropdownOpen}
|
<TradingDropdownTrigger
|
||||||
trigger={
|
data-testid="manage-vega-wallet"
|
||||||
<DropdownMenuTrigger
|
onClick={() => {
|
||||||
data-testid="manage-vega-wallet"
|
if (fetchPubKeys) {
|
||||||
|
fetchPubKeys();
|
||||||
|
}
|
||||||
|
setDropdownOpen(!dropdownOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />}
|
||||||
|
>
|
||||||
|
{activeKey && <span className="uppercase">{activeKey.name}</span>}
|
||||||
|
{' | '}
|
||||||
|
{truncateByChars(pubKey)}
|
||||||
|
</Button>
|
||||||
|
</TradingDropdownTrigger>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TradingDropdownContent
|
||||||
|
onInteractOutside={() => setDropdownOpen(false)}
|
||||||
|
sideOffset={12}
|
||||||
|
side="bottom"
|
||||||
|
align="end"
|
||||||
|
onEscapeKeyDown={() => setDropdownOpen(false)}
|
||||||
|
>
|
||||||
|
<div className="min-w-[340px]" data-testid="keypair-list">
|
||||||
|
<TradingDropdownRadioGroup
|
||||||
|
value={pubKey}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
selectPubKey(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pubKeys.map((pk) => (
|
||||||
|
<KeypairItem
|
||||||
|
key={pk.publicKey}
|
||||||
|
pk={pk}
|
||||||
|
active={pk.publicKey === pubKey}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TradingDropdownRadioGroup>
|
||||||
|
<TradingDropdownSeparator />
|
||||||
|
{!isReadOnly && (
|
||||||
|
<TradingDropdownItem
|
||||||
|
data-testid="wallet-transfer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (fetchPubKeys) {
|
setView({ type: ViewType.Transfer });
|
||||||
fetchPubKeys();
|
setDropdownOpen(false);
|
||||||
}
|
|
||||||
setDropdownOpen(!dropdownOpen);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{activeKey && (
|
{t('Transfer')}
|
||||||
<span className="uppercase">{activeKey.name}</span>
|
</TradingDropdownItem>
|
||||||
)}
|
)}
|
||||||
{': '}
|
<TradingDropdownItem data-testid="disconnect" onClick={disconnect}>
|
||||||
{truncateByChars(pubKey)}
|
{t('Disconnect')}
|
||||||
</DropdownMenuTrigger>
|
</TradingDropdownItem>
|
||||||
}
|
</div>
|
||||||
>
|
</TradingDropdownContent>
|
||||||
<DropdownMenuContent
|
</TradingDropdown>
|
||||||
onInteractOutside={() => setDropdownOpen(false)}
|
|
||||||
sideOffset={17}
|
|
||||||
side="bottom"
|
|
||||||
align="end"
|
|
||||||
onEscapeKeyDown={() => setDropdownOpen(false)}
|
|
||||||
>
|
|
||||||
<div className="min-w-[340px]" data-testid="keypair-list">
|
|
||||||
<DropdownMenuRadioGroup
|
|
||||||
value={pubKey}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
selectPubKey(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{pubKeys.map((pk) => (
|
|
||||||
<KeypairItem key={pk.publicKey} pk={pk} />
|
|
||||||
))}
|
|
||||||
</DropdownMenuRadioGroup>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
{!isReadOnly && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
data-testid="wallet-transfer"
|
|
||||||
onClick={() => {
|
|
||||||
setView({ type: ViewType.Transfer });
|
|
||||||
setDropdownOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Transfer')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropdownMenuItem data-testid="disconnect" onClick={disconnect}>
|
|
||||||
{t('Disconnect')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
<MobileWalletButton isConnected activeKey={activeKey} />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Button
|
||||||
<Button
|
data-testid="connect-vega-wallet"
|
||||||
data-testid="connect-vega-wallet"
|
onClick={openVegaWalletDialog}
|
||||||
onClick={openVegaWalletDialog}
|
size="small"
|
||||||
size="sm"
|
intent={Intent.None}
|
||||||
className="hidden lg:block"
|
icon={<VegaIcon name={VegaIconNames.ARROW_RIGHT} size={14} />}
|
||||||
>
|
>
|
||||||
<span className="whitespace-nowrap">{t('Connect Vega wallet')}</span>
|
<span className="whitespace-nowrap uppercase">{t('Connect')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<MobileWalletButton />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const KeypairItem = ({ pk }: { pk: PubKey }) => {
|
const KeypairItem = ({ pk, active }: { pk: PubKey; active: boolean }) => {
|
||||||
const [copied, setCopied] = useCopyTimeout();
|
const [copied, setCopied] = useCopyTimeout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuRadioItem value={pk.publicKey}>
|
<TradingDropdownRadioItem value={pk.publicKey}>
|
||||||
<div className="flex-1 mr-2" data-testid={`key-${pk.publicKey}`}>
|
<div
|
||||||
<span className="mr-2">
|
className={classNames('flex-1 mr-2', {
|
||||||
<span>
|
'text-default': active,
|
||||||
<span className="uppercase">{pk.name}</span>:{' '}
|
'text-muted': !active,
|
||||||
{truncateByChars(pk.publicKey)}
|
})}
|
||||||
</span>
|
data-testid={`key-${pk.publicKey}`}
|
||||||
|
>
|
||||||
|
<span className={classNames('mr-2 uppercase')}>
|
||||||
|
{pk.name}
|
||||||
|
{' | '}
|
||||||
|
{truncateByChars(pk.publicKey)}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
<CopyToClipboard text={pk.publicKey} onCopy={() => setCopied(true)}>
|
<CopyToClipboard text={pk.publicKey} onCopy={() => setCopied(true)}>
|
||||||
@@ -270,46 +153,7 @@ const KeypairItem = ({ pk }: { pk: PubKey }) => {
|
|||||||
{copied && <span className="text-xs">{t('Copied')}</span>}
|
{copied && <span className="text-xs">{t('Copied')}</span>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuItemIndicator />
|
<TradingDropdownItemIndicator />
|
||||||
</DropdownMenuRadioItem>
|
</TradingDropdownRadioItem>
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const KeypairListItem = ({
|
|
||||||
pk,
|
|
||||||
isActive,
|
|
||||||
onSelectItem,
|
|
||||||
}: {
|
|
||||||
pk: PubKey;
|
|
||||||
isActive: boolean;
|
|
||||||
onSelectItem: (pk: string) => void;
|
|
||||||
}) => {
|
|
||||||
const [copied, setCopied] = useCopyTimeout();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex flex-col w-full ml-4 mr-2 mb-4"
|
|
||||||
data-testid={`key-${pk.publicKey}-mobile`}
|
|
||||||
>
|
|
||||||
<span className="flex gap-2 items-center mr-2">
|
|
||||||
<button onClick={() => onSelectItem(pk.publicKey)}>
|
|
||||||
<span className="uppercase">{pk.name}</span>
|
|
||||||
</button>
|
|
||||||
{isActive && <VegaIcon name={VegaIconNames.TICK} />}
|
|
||||||
</span>
|
|
||||||
<span className="flex gap-2 items-center">
|
|
||||||
{truncateByChars(pk.publicKey)}{' '}
|
|
||||||
<CopyToClipboard text={pk.publicKey} onCopy={() => setCopied(true)}>
|
|
||||||
<button
|
|
||||||
data-testid="copy-vega-public-key"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<span className="sr-only">{t('Copy')}</span>
|
|
||||||
<VegaIcon name={VegaIconNames.COPY} />
|
|
||||||
</button>
|
|
||||||
</CopyToClipboard>
|
|
||||||
{copied && <span className="text-xs">{t('Copied')}</span>}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { VegaWalletMenu } from './vega-wallet-menu';
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
|
||||||
|
import {
|
||||||
|
TradingButton as Button,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { truncateByChars } from '@vegaprotocol/utils';
|
||||||
|
import { useVegaWallet, type PubKey } from '@vegaprotocol/wallet';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
|
import { ViewType, useSidebar } from '../sidebar';
|
||||||
|
|
||||||
|
export const VegaWalletMenu = ({
|
||||||
|
setMenu,
|
||||||
|
}: {
|
||||||
|
setMenu: (open: 'nav' | 'wallet' | null) => void;
|
||||||
|
}) => {
|
||||||
|
const { pubKey, pubKeys, selectPubKey, disconnect } = useVegaWallet();
|
||||||
|
const setView = useSidebar((store) => store.setView);
|
||||||
|
|
||||||
|
const activeKey = useMemo(() => {
|
||||||
|
return pubKeys?.find((pk) => pk.publicKey === pubKey);
|
||||||
|
}, [pubKey, pubKeys]);
|
||||||
|
|
||||||
|
const onSelectItem = useCallback(
|
||||||
|
(pubkey: string) => {
|
||||||
|
selectPubKey(pubkey);
|
||||||
|
},
|
||||||
|
[selectPubKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="grow my-4" role="list">
|
||||||
|
{(pubKeys || []).map((pk) => (
|
||||||
|
<KeypairListItem
|
||||||
|
key={pk.publicKey}
|
||||||
|
pk={pk}
|
||||||
|
isActive={activeKey?.publicKey === pk.publicKey}
|
||||||
|
onSelectItem={onSelectItem}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 m-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setView({ type: ViewType.Transfer });
|
||||||
|
setMenu(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Transfer')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
await disconnect();
|
||||||
|
setMenu(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Disconnect')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const KeypairListItem = ({
|
||||||
|
pk,
|
||||||
|
isActive,
|
||||||
|
onSelectItem,
|
||||||
|
}: {
|
||||||
|
pk: PubKey;
|
||||||
|
isActive: boolean;
|
||||||
|
onSelectItem: (pk: string) => void;
|
||||||
|
}) => {
|
||||||
|
const [copied, setCopied] = useCopyTimeout();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex flex-col w-full ml-4 mr-2 mb-4"
|
||||||
|
data-testid={`key-${pk.publicKey}-mobile`}
|
||||||
|
>
|
||||||
|
<span className="flex gap-2 items-center mr-2">
|
||||||
|
<button type="button" onClick={() => onSelectItem(pk.publicKey)}>
|
||||||
|
<span className="uppercase">{pk.name}</span>
|
||||||
|
</button>
|
||||||
|
{isActive && <VegaIcon name={VegaIconNames.TICK} />}
|
||||||
|
</span>
|
||||||
|
<span className="flex gap-2 items-center">
|
||||||
|
{truncateByChars(pk.publicKey)}{' '}
|
||||||
|
<CopyToClipboard text={pk.publicKey} onCopy={() => setCopied(true)}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="copy-vega-public-key"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">{t('Copy')}</span>
|
||||||
|
<VegaIcon name={VegaIconNames.COPY} />
|
||||||
|
</button>
|
||||||
|
</CopyToClipboard>
|
||||||
|
{copied && <span className="text-xs">{t('Copied')}</span>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '@vegaprotocol/web3';
|
} from '@vegaprotocol/web3';
|
||||||
import {
|
import {
|
||||||
envTriggerMapping,
|
envTriggerMapping,
|
||||||
|
Networks,
|
||||||
NodeSwitcherDialog,
|
NodeSwitcherDialog,
|
||||||
useEnvironment,
|
useEnvironment,
|
||||||
useInitializeEnv,
|
useInitializeEnv,
|
||||||
@@ -25,7 +26,13 @@ import './styles.css';
|
|||||||
import { usePageTitleStore } from '../stores';
|
import { usePageTitleStore } from '../stores';
|
||||||
import DialogsContainer from './dialogs-container';
|
import DialogsContainer from './dialogs-container';
|
||||||
import ToastsManager from './toasts-manager';
|
import ToastsManager from './toasts-manager';
|
||||||
import { HashRouter, useLocation, useSearchParams } from 'react-router-dom';
|
import {
|
||||||
|
HashRouter,
|
||||||
|
useLocation,
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
|
useSearchParams,
|
||||||
|
} from 'react-router-dom';
|
||||||
import { Connectors } from '../lib/vega-connectors';
|
import { Connectors } from '../lib/vega-connectors';
|
||||||
import { AppLoader, DynamicLoader } from '../components/app-loader';
|
import { AppLoader, DynamicLoader } from '../components/app-loader';
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
@@ -39,6 +46,8 @@ import {
|
|||||||
ProtocolUpgradeProposalNotification,
|
ProtocolUpgradeProposalNotification,
|
||||||
} from '@vegaprotocol/proposals';
|
} from '@vegaprotocol/proposals';
|
||||||
import { ViewingBanner } from '../components/viewing-banner';
|
import { ViewingBanner } from '../components/viewing-banner';
|
||||||
|
import { NavHeader } from '../components/navbar/nav-header';
|
||||||
|
import { Routes as AppRoutes } from './client-router';
|
||||||
|
|
||||||
const DEFAULT_TITLE = t('Welcome to Vega trading!');
|
const DEFAULT_TITLE = t('Welcome to Vega trading!');
|
||||||
|
|
||||||
@@ -74,6 +83,7 @@ const InitializeHandlers = () => {
|
|||||||
|
|
||||||
function AppBody({ Component }: AppProps) {
|
function AppBody({ Component }: AppProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { VEGA_ENV } = useEnvironment();
|
||||||
const gridClasses = classNames(
|
const gridClasses = classNames(
|
||||||
'h-full relative z-0 grid',
|
'h-full relative z-0 grid',
|
||||||
'grid-rows-[repeat(3,min-content),minmax(0,1fr)]'
|
'grid-rows-[repeat(3,min-content),minmax(0,1fr)]'
|
||||||
@@ -87,7 +97,16 @@ function AppBody({ Component }: AppProps) {
|
|||||||
<Title />
|
<Title />
|
||||||
<div className={gridClasses}>
|
<div className={gridClasses}>
|
||||||
<AnnouncementBanner />
|
<AnnouncementBanner />
|
||||||
<Navbar theme="system" />
|
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'}>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path={AppRoutes.MARKETS}
|
||||||
|
// render nothing for markets/all, otherwise markets/:marketId will match with markets/all
|
||||||
|
element={null}
|
||||||
|
/>
|
||||||
|
<Route path={AppRoutes.MARKET} element={<NavHeader />} />
|
||||||
|
</Routes>
|
||||||
|
</Navbar>
|
||||||
<div data-testid="banners">
|
<div data-testid="banners">
|
||||||
<ProtocolUpgradeProposalNotification
|
<ProtocolUpgradeProposalNotification
|
||||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const useAccountBalance = (assetId?: string) => {
|
|||||||
},
|
},
|
||||||
[assetId]
|
[assetId]
|
||||||
);
|
);
|
||||||
useDataProvider({
|
const { loading, error } = useDataProvider({
|
||||||
dataProvider: accountsDataProvider,
|
dataProvider: accountsDataProvider,
|
||||||
variables,
|
variables,
|
||||||
skip: !pubKey || !assetId,
|
skip: !pubKey || !assetId,
|
||||||
@@ -34,7 +34,9 @@ export const useAccountBalance = (assetId?: string) => {
|
|||||||
() => ({
|
() => ({
|
||||||
accountBalance: pubKey ? accountBalance : '',
|
accountBalance: pubKey ? accountBalance : '',
|
||||||
accountDecimals: pubKey ? accountDecimals : null,
|
accountDecimals: pubKey ? accountDecimals : null,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
}),
|
}),
|
||||||
[accountBalance, accountDecimals, pubKey]
|
[accountBalance, accountDecimals, pubKey, loading, error]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const useMarketAccountBalance = (marketId: string) => {
|
|||||||
},
|
},
|
||||||
[marketId]
|
[marketId]
|
||||||
);
|
);
|
||||||
useDataProvider({
|
const { loading, error } = useDataProvider({
|
||||||
dataProvider: accountsDataProvider,
|
dataProvider: accountsDataProvider,
|
||||||
variables: { partyId: pubKey || '' },
|
variables: { partyId: pubKey || '' },
|
||||||
skip: !pubKey || !marketId,
|
skip: !pubKey || !marketId,
|
||||||
@@ -33,7 +33,9 @@ export const useMarketAccountBalance = (marketId: string) => {
|
|||||||
() => ({
|
() => ({
|
||||||
accountBalance: pubKey ? accountBalance : '',
|
accountBalance: pubKey ? accountBalance : '',
|
||||||
accountDecimals: pubKey ? accountDecimals : null,
|
accountDecimals: pubKey ? accountDecimals : null,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
}),
|
}),
|
||||||
[accountBalance, accountDecimals, pubKey]
|
[accountBalance, accountDecimals, pubKey, loading, error]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "@vegaprotocol/announcements",
|
"name": "@vegaprotocol/announcements",
|
||||||
"version": "0.0.1"
|
"version": "0.0.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export function createLog(name: string) {
|
export function createLog(name: string) {
|
||||||
return (message: string) => {
|
return (message: string) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(`[${name}]: ${message}`);
|
console.log(`[${name}]: ${message}`);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export function waitForProposal(id: string): Promise<{ id: string }> {
|
|||||||
resolve(res.proposal);
|
resolve(res.proposal);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
tick++;
|
tick++;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const addImportNodeWallets = () => {
|
|||||||
.its('stdout')
|
.its('stdout')
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const obj = JSON.parse(result);
|
const obj = JSON.parse(result);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(obj);
|
console.log(obj);
|
||||||
cy.writeFile(
|
cy.writeFile(
|
||||||
'./src/fixtures/wallet/node0RecoveryPhrase',
|
'./src/fixtures/wallet/node0RecoveryPhrase',
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const addValidatorsSelfDelegate = () => {
|
|||||||
.its('stdout')
|
.its('stdout')
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const obj = JSON.parse(result);
|
const obj = JSON.parse(result);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(obj);
|
console.log(obj);
|
||||||
cy.writeFile(
|
cy.writeFile(
|
||||||
'./src/fixtures/wallet/node0RecoveryPhrase',
|
'./src/fixtures/wallet/node0RecoveryPhrase',
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ export function addVegaWalletTopUpRewardsPool() {
|
|||||||
transferStartEpoch = Number(epochText.replace('Epoch', '')) + 5;
|
transferStartEpoch = Number(epochText.replace('Epoch', '')) + 5;
|
||||||
transferEndEpoch = transferStartEpoch + 100;
|
transferEndEpoch = transferStartEpoch + 100;
|
||||||
|
|
||||||
|
/* eslint-disable no-console */
|
||||||
console.log(transferStartEpoch);
|
console.log(transferStartEpoch);
|
||||||
console.log(transferEndEpoch);
|
console.log(transferEndEpoch);
|
||||||
|
/* eslint-enable */
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ export class CustomizedBridge extends Eip1193Bridge {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
async sendAsync(...args: any) {
|
async sendAsync(...args: any) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.debug('sendAsync called', ...args);
|
console.debug('sendAsync called', ...args);
|
||||||
return this.send(...args);
|
return this.send(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
override async send(...args: any) {
|
override async send(...args: any) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.debug('send called', ...args);
|
console.debug('send called', ...args);
|
||||||
const isCallbackForm =
|
const isCallbackForm =
|
||||||
typeof args[0] === 'object' && typeof args[1] === 'function';
|
typeof args[0] === 'object' && typeof args[1] === 'function';
|
||||||
@@ -89,6 +91,7 @@ export class CustomizedBridge extends Eip1193Bridge {
|
|||||||
// All other transactions the base class works for
|
// All other transactions the base class works for
|
||||||
result = await super.send(method, params);
|
result = await super.send(method, params);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.debug('result received', method, params, result);
|
console.debug('result received', method, params, result);
|
||||||
if (isCallbackForm) {
|
if (isCallbackForm) {
|
||||||
callback(null, { result });
|
callback(null, { result });
|
||||||
@@ -96,6 +99,7 @@ export class CustomizedBridge extends Eip1193Bridge {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
if (isCallbackForm) {
|
if (isCallbackForm) {
|
||||||
callback(error, null);
|
callback(error, null);
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ export const CenteredGridCellWrapper = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames('flex h-[20px] p-0 justify-items-center', className)}
|
className={classNames('flex h-[20px] p-0 justify-items-center', className)}
|
||||||
>
|
>
|
||||||
<div className="self-center">{children}</div>
|
<div className="w-full self-center">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useCallback } from 'react';
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
|
||||||
interface MarketNameCellProps {
|
interface MarketNameCellProps {
|
||||||
value?: string;
|
value?: string | null;
|
||||||
data?: { id?: string; marketId?: string; market?: { id: string } };
|
data?: { id?: string; marketId?: string; market?: { id: string } };
|
||||||
idPath?: string;
|
idPath?: string;
|
||||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Control } from 'react-hook-form';
|
import type { Control } from 'react-hook-form';
|
||||||
import type { Market, MarketData } from '@vegaprotocol/markets';
|
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
||||||
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
|
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
|
||||||
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
|
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
@@ -9,7 +9,8 @@ import type { OrderFormFields } from '../../hooks/use-order-form';
|
|||||||
export interface DealTicketAmountProps {
|
export interface DealTicketAmountProps {
|
||||||
control: Control<OrderFormFields>;
|
control: Control<OrderFormFields>;
|
||||||
orderType: Schema.OrderType;
|
orderType: Schema.OrderType;
|
||||||
marketData: MarketData;
|
marketData: StaticMarketData;
|
||||||
|
marketPrice?: string;
|
||||||
market: Market;
|
market: Market;
|
||||||
sizeError?: string;
|
sizeError?: string;
|
||||||
priceError?: string;
|
priceError?: string;
|
||||||
@@ -21,11 +22,18 @@ export interface DealTicketAmountProps {
|
|||||||
export const DealTicketAmount = ({
|
export const DealTicketAmount = ({
|
||||||
orderType,
|
orderType,
|
||||||
marketData,
|
marketData,
|
||||||
|
marketPrice,
|
||||||
...props
|
...props
|
||||||
}: DealTicketAmountProps) => {
|
}: DealTicketAmountProps) => {
|
||||||
switch (orderType) {
|
switch (orderType) {
|
||||||
case Schema.OrderType.TYPE_MARKET:
|
case Schema.OrderType.TYPE_MARKET:
|
||||||
return <DealTicketMarketAmount {...props} marketData={marketData} />;
|
return (
|
||||||
|
<DealTicketMarketAmount
|
||||||
|
{...props}
|
||||||
|
marketData={marketData}
|
||||||
|
marketPrice={marketPrice}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case Schema.OrderType.TYPE_LIMIT:
|
case Schema.OrderType.TYPE_LIMIT:
|
||||||
return <DealTicketLimitAmount {...props} />;
|
return <DealTicketLimitAmount {...props} />;
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
side: Side;
|
side: Side;
|
||||||
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DealTicketButton = ({ side }: Props) => {
|
export const DealTicketButton = ({ side, label }: Props) => {
|
||||||
const buttonClasses = classNames(
|
const buttonClasses = classNames(
|
||||||
'px-10 py-2 uppercase rounded-md text-white w-full',
|
'px-10 py-2 uppercase rounded-md text-white w-full',
|
||||||
{
|
{
|
||||||
@@ -17,7 +18,7 @@ export const DealTicketButton = ({ side }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<button type="submit" data-testid="place-order" className={buttonClasses}>
|
<button type="submit" data-testid="place-order" className={buttonClasses}>
|
||||||
{t('Place order')}
|
{label || t('Place order')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
|
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
||||||
|
import {
|
||||||
|
DealTicketType,
|
||||||
|
useDealTicketTypeStore,
|
||||||
|
} from '../../hooks/use-type-store';
|
||||||
|
import { StopOrder } from './deal-ticket-stop-order';
|
||||||
|
import {
|
||||||
|
useStaticMarketData,
|
||||||
|
useMarket,
|
||||||
|
useMarketPrice,
|
||||||
|
} from '@vegaprotocol/markets';
|
||||||
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
|
||||||
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
|
||||||
import { useMarket, marketDataProvider } from '@vegaprotocol/markets';
|
|
||||||
import { DealTicket } from './deal-ticket';
|
import { DealTicket } from './deal-ticket';
|
||||||
|
import { FLAGS } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
export interface DealTicketContainerProps {
|
interface DealTicketContainerProps {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
onClickCollateral?: () => void;
|
onClickCollateral?: () => void;
|
||||||
@@ -14,10 +23,9 @@ export interface DealTicketContainerProps {
|
|||||||
|
|
||||||
export const DealTicketContainer = ({
|
export const DealTicketContainer = ({
|
||||||
marketId,
|
marketId,
|
||||||
onMarketClick,
|
...props
|
||||||
onClickCollateral,
|
|
||||||
onDeposit,
|
|
||||||
}: DealTicketContainerProps) => {
|
}: DealTicketContainerProps) => {
|
||||||
|
const type = useDealTicketTypeStore((state) => state.type[marketId]);
|
||||||
const {
|
const {
|
||||||
data: market,
|
data: market,
|
||||||
error: marketError,
|
error: marketError,
|
||||||
@@ -29,15 +37,9 @@ export const DealTicketContainer = ({
|
|||||||
error: marketDataError,
|
error: marketDataError,
|
||||||
loading: marketDataLoading,
|
loading: marketDataLoading,
|
||||||
reload,
|
reload,
|
||||||
} = useThrottledDataProvider(
|
} = useStaticMarketData(marketId);
|
||||||
{
|
const { data: marketPrice } = useMarketPrice(market?.id);
|
||||||
dataProvider: marketDataProvider,
|
|
||||||
variables: { marketId },
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
const create = useVegaTransactionStore((state) => state.create);
|
const create = useVegaTransactionStore((state) => state.create);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
data={market && marketData}
|
data={market && marketData}
|
||||||
@@ -46,14 +48,23 @@ export const DealTicketContainer = ({
|
|||||||
reload={reload}
|
reload={reload}
|
||||||
>
|
>
|
||||||
{market && marketData ? (
|
{market && marketData ? (
|
||||||
<DealTicket
|
FLAGS.STOP_ORDERS &&
|
||||||
market={market}
|
(type === DealTicketType.StopLimit ||
|
||||||
marketData={marketData}
|
type === DealTicketType.StopMarket) ? (
|
||||||
submit={(orderSubmission) => create({ orderSubmission })}
|
<StopOrder
|
||||||
onClickCollateral={onClickCollateral}
|
market={market}
|
||||||
onMarketClick={onMarketClick}
|
marketPrice={marketPrice}
|
||||||
onDeposit={onDeposit}
|
submit={(stopOrdersSubmission) => create({ stopOrdersSubmission })}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<DealTicket
|
||||||
|
{...props}
|
||||||
|
market={market}
|
||||||
|
marketPrice={marketPrice}
|
||||||
|
marketData={marketData}
|
||||||
|
submit={(orderSubmission) => create({ orderSubmission })}
|
||||||
|
/>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<Splash>
|
<Splash>
|
||||||
<p>{t('Could not load market')}</p>
|
<p>{t('Could not load market')}</p>
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import classnames from 'classnames';
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { FeesBreakdown } from '@vegaprotocol/markets';
|
import { FeesBreakdown } from '@vegaprotocol/markets';
|
||||||
|
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
|
||||||
import type { Market } from '@vegaprotocol/markets';
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
||||||
import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder';
|
|
||||||
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
||||||
|
|
||||||
import { formatRange, formatValue } from '@vegaprotocol/utils';
|
import { formatRange, formatValue } from '@vegaprotocol/utils';
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
|
import { useEstimateFees } from '../../hooks';
|
||||||
|
|
||||||
const emptyValue = '-';
|
const emptyValue = '-';
|
||||||
|
|
||||||
@@ -76,26 +77,82 @@ export const DealTicketFeeDetail = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface DealTicketFeeDetailsProps {
|
export interface DealTicketFeeDetailsProps {
|
||||||
|
assetSymbol: string;
|
||||||
|
order: OrderSubmissionBody['orderSubmission'];
|
||||||
|
market: Market;
|
||||||
|
notionalSize: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DealTicketFeeDetails = ({
|
||||||
|
assetSymbol,
|
||||||
|
order,
|
||||||
|
market,
|
||||||
|
notionalSize,
|
||||||
|
}: DealTicketFeeDetailsProps) => {
|
||||||
|
const feeEstimate = useEstimateFees(order);
|
||||||
|
const { settlementAsset: asset } =
|
||||||
|
market.tradableInstrument.instrument.product;
|
||||||
|
const { decimals: assetDecimals, quantum } = asset;
|
||||||
|
const marketDecimals = market.decimalPlaces;
|
||||||
|
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DealTicketFeeDetail
|
||||||
|
label={t('Notional')}
|
||||||
|
value={formatValue(notionalSize, marketDecimals)}
|
||||||
|
formattedValue={formatValue(notionalSize, marketDecimals)}
|
||||||
|
symbol={quoteName}
|
||||||
|
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
|
||||||
|
/>
|
||||||
|
<DealTicketFeeDetail
|
||||||
|
label={t('Fees')}
|
||||||
|
value={
|
||||||
|
feeEstimate?.totalFeeAmount &&
|
||||||
|
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
|
||||||
|
}
|
||||||
|
formattedValue={
|
||||||
|
feeEstimate?.totalFeeAmount &&
|
||||||
|
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
|
||||||
|
}
|
||||||
|
labelDescription={
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
{t(
|
||||||
|
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<FeesBreakdown
|
||||||
|
fees={feeEstimate?.fees}
|
||||||
|
feeFactors={market.fees.factors}
|
||||||
|
symbol={assetSymbol}
|
||||||
|
decimals={assetDecimals}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
symbol={assetSymbol}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DealTicketMarginDetailsProps {
|
||||||
generalAccountBalance?: string;
|
generalAccountBalance?: string;
|
||||||
marginAccountBalance?: string;
|
marginAccountBalance?: string;
|
||||||
market: Market;
|
market: Market;
|
||||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
assetSymbol: string;
|
assetSymbol: string;
|
||||||
notionalSize: string | null;
|
|
||||||
feeEstimate: EstimateFeesQuery['estimateFees'] | undefined;
|
|
||||||
positionEstimate: EstimatePositionQuery['estimatePosition'];
|
positionEstimate: EstimatePositionQuery['estimatePosition'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DealTicketFeeDetails = ({
|
export const DealTicketMarginDetails = ({
|
||||||
marginAccountBalance,
|
marginAccountBalance,
|
||||||
generalAccountBalance,
|
generalAccountBalance,
|
||||||
assetSymbol,
|
assetSymbol,
|
||||||
feeEstimate,
|
|
||||||
market,
|
market,
|
||||||
onMarketClick,
|
onMarketClick,
|
||||||
notionalSize,
|
|
||||||
positionEstimate,
|
positionEstimate,
|
||||||
}: DealTicketFeeDetailsProps) => {
|
}: DealTicketMarginDetailsProps) => {
|
||||||
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
||||||
const { pubKey: partyId } = useVegaWallet();
|
const { pubKey: partyId } = useVegaWallet();
|
||||||
const { data: currentMargins } = useDataProvider({
|
const { data: currentMargins } = useDataProvider({
|
||||||
@@ -110,7 +167,6 @@ export const DealTicketFeeDetails = ({
|
|||||||
const { settlementAsset: asset } =
|
const { settlementAsset: asset } =
|
||||||
market.tradableInstrument.instrument.product;
|
market.tradableInstrument.instrument.product;
|
||||||
const { decimals: assetDecimals, quantum } = asset;
|
const { decimals: assetDecimals, quantum } = asset;
|
||||||
const marketDecimals = market.decimalPlaces;
|
|
||||||
let marginRequiredBestCase: string | undefined = undefined;
|
let marginRequiredBestCase: string | undefined = undefined;
|
||||||
let marginRequiredWorstCase: string | undefined = undefined;
|
let marginRequiredWorstCase: string | undefined = undefined;
|
||||||
if (marginEstimate) {
|
if (marginEstimate) {
|
||||||
@@ -251,41 +307,7 @@ export const DealTicketFeeDetails = ({
|
|||||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<DealTicketFeeDetail
|
|
||||||
label={t('Notional')}
|
|
||||||
value={formatValue(notionalSize, marketDecimals)}
|
|
||||||
formattedValue={formatValue(notionalSize, marketDecimals)}
|
|
||||||
symbol={quoteName}
|
|
||||||
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
|
|
||||||
/>
|
|
||||||
<DealTicketFeeDetail
|
|
||||||
label={t('Fees')}
|
|
||||||
value={
|
|
||||||
feeEstimate?.totalFeeAmount &&
|
|
||||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
|
|
||||||
}
|
|
||||||
formattedValue={
|
|
||||||
feeEstimate?.totalFeeAmount &&
|
|
||||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
|
|
||||||
}
|
|
||||||
labelDescription={
|
|
||||||
<>
|
|
||||||
<span>
|
|
||||||
{t(
|
|
||||||
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<FeesBreakdown
|
|
||||||
fees={feeEstimate?.fees}
|
|
||||||
feeFactors={market.fees.factors}
|
|
||||||
symbol={assetSymbol}
|
|
||||||
decimals={assetDecimals}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
symbol={assetSymbol}
|
|
||||||
/>
|
|
||||||
<DealTicketFeeDetail
|
<DealTicketFeeDetail
|
||||||
label={t('Margin required')}
|
label={t('Margin required')}
|
||||||
value={formatRange(
|
value={formatRange(
|
||||||
@@ -351,6 +373,6 @@ export const DealTicketFeeDetails = ({
|
|||||||
onClose={onAccountBreakdownDialogClose}
|
onClose={onAccountBreakdownDialogClose}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ export const DealTicketLimitAmount = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t('Size')}
|
label={t('Size')}
|
||||||
labelFor="input-order-size-limit"
|
labelFor="input-order-size-limit"
|
||||||
className="!mb-1"
|
className="!mb-0"
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="size"
|
name="size"
|
||||||
@@ -78,16 +78,13 @@ export const DealTicketLimitAmount = ({
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-0 items-center">
|
<div className="pt-7 leading-10">@</div>
|
||||||
<div className="flex"> </div>
|
|
||||||
<div className="flex">@</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<FormGroup
|
<FormGroup
|
||||||
labelFor="input-price-quote"
|
labelFor="input-price-quote"
|
||||||
label={t(`Price (${quoteName})`)}
|
label={t(`Price (${quoteName})`)}
|
||||||
labelAlign="right"
|
labelAlign="right"
|
||||||
className="!mb-1"
|
className="!mb-0"
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="price"
|
name="price"
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import {
|
|||||||
} from '@vegaprotocol/utils';
|
} from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
|
import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
import { isMarketInAuction } from '../../utils';
|
import { isMarketInAuction } from '@vegaprotocol/markets';
|
||||||
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||||
import { getMarketPrice } from '../../utils/get-price';
|
|
||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export type DealTicketMarketAmountProps = Omit<
|
export type DealTicketMarketAmountProps = Omit<
|
||||||
DealTicketAmountProps,
|
DealTicketAmountProps,
|
||||||
@@ -19,37 +19,26 @@ export const DealTicketMarketAmount = ({
|
|||||||
control,
|
control,
|
||||||
market,
|
market,
|
||||||
marketData,
|
marketData,
|
||||||
|
marketPrice,
|
||||||
sizeError,
|
sizeError,
|
||||||
update,
|
update,
|
||||||
size,
|
size,
|
||||||
}: DealTicketMarketAmountProps) => {
|
}: DealTicketMarketAmountProps) => {
|
||||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||||
const price = getMarketPrice(marketData);
|
const price = marketPrice;
|
||||||
|
|
||||||
const priceFormatted = price
|
const priceFormatted = price
|
||||||
? addDecimalsFormatNumber(price, market.decimalPlaces)
|
? addDecimalsFormatNumber(price, market.decimalPlaces)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const inAuction = isMarketInAuction(marketData.marketTradingMode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="flex items-end gap-4 mb-2">
|
<div className="flex items-start gap-4">
|
||||||
<div className="flex-1 text-sm">{t('Size')}</div>
|
|
||||||
<div />
|
|
||||||
<div className="flex-2 text-sm text-right">
|
|
||||||
{isMarketInAuction(marketData.marketTradingMode) && (
|
|
||||||
<Tooltip
|
|
||||||
description={t(
|
|
||||||
'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div>{t(`Indicative price`)}</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
<div className="mb-2 text-sm">{t('Size')}</div>
|
||||||
<Controller
|
<Controller
|
||||||
name="size"
|
name="size"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -76,15 +65,29 @@ export const DealTicketMarketAmount = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>@</div>
|
<div className="pt-7 leading-10">@</div>
|
||||||
<div className="flex-1 text-sm text-right" data-testid="last-price">
|
<div className="flex-1 text-sm text-right">
|
||||||
{priceFormatted && quoteName ? (
|
{inAuction && (
|
||||||
<>
|
<Tooltip
|
||||||
~{priceFormatted} {quoteName}
|
description={t(
|
||||||
</>
|
'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
|
||||||
) : (
|
)}
|
||||||
'-'
|
>
|
||||||
|
<div className="mb-2">{t(`Indicative price`)}</div>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
<div
|
||||||
|
data-testid="last-price"
|
||||||
|
className={classNames('leading-10', { 'pt-7': !inAuction })}
|
||||||
|
>
|
||||||
|
{priceFormatted && quoteName ? (
|
||||||
|
<>
|
||||||
|
~{priceFormatted} {quoteName}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{sizeError && (
|
{sizeError && (
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { generateMarket } from '../../test-helpers';
|
||||||
|
import { StopOrder } from './deal-ticket-stop-order';
|
||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import type { StopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||||
|
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||||
|
import type { FeatureFlags } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
|
jest.mock('zustand');
|
||||||
|
jest.mock('./deal-ticket-fee-details', () => ({
|
||||||
|
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/environment', () => {
|
||||||
|
const actual = jest.requireActual('@vegaprotocol/environment');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
FLAGS: {
|
||||||
|
...actual.FLAGS,
|
||||||
|
STOP_ORDERS: true,
|
||||||
|
} as FeatureFlags,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketPrice = '200';
|
||||||
|
const market = generateMarket();
|
||||||
|
const submit = jest.fn();
|
||||||
|
|
||||||
|
function generateJsx(pubKey: string | null = 'pubKey', isReadOnly = false) {
|
||||||
|
return (
|
||||||
|
<MockedProvider>
|
||||||
|
<VegaWalletContext.Provider value={{ pubKey, isReadOnly } as any}>
|
||||||
|
<StopOrder market={market} marketPrice={marketPrice} submit={submit} />
|
||||||
|
</VegaWalletContext.Provider>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitButton = 'place-order';
|
||||||
|
const sizeInput = 'order-size';
|
||||||
|
const priceInput = 'order-price';
|
||||||
|
const triggerPriceInput = 'triggerPrice';
|
||||||
|
const triggerTrailingPercentOffsetInput = 'triggerTrailingPercentOffset';
|
||||||
|
|
||||||
|
const orderTypeTrigger = 'order-type-Stop';
|
||||||
|
const orderTypeLimit = 'order-type-StopLimit';
|
||||||
|
const orderTypeMarket = 'order-type-StopMarket';
|
||||||
|
|
||||||
|
const orderSideBuy = 'order-side-SIDE_BUY';
|
||||||
|
const orderSideSell = 'order-side-SIDE_SELL';
|
||||||
|
|
||||||
|
const triggerDirectionRisesAbove = 'triggerDirection-risesAbove';
|
||||||
|
// const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow';
|
||||||
|
|
||||||
|
const expiryStrategySubmit = 'expiryStrategy-submit';
|
||||||
|
const expiryStrategyCancel = 'expiryStrategy-cancel';
|
||||||
|
|
||||||
|
const triggerTypePrice = 'triggerType-price';
|
||||||
|
const triggerTypeTrailingPercentOffset = 'triggerType-trailingPercentOffset';
|
||||||
|
|
||||||
|
const expire = 'expire';
|
||||||
|
const datePicker = 'date-picker-field';
|
||||||
|
const timeInForce = 'order-tif';
|
||||||
|
|
||||||
|
const sizeErrorMessage = 'stop-order-error-message-size';
|
||||||
|
const priceErrorMessage = 'stop-order-error-message-price';
|
||||||
|
const triggerPriceErrorMessage = 'stop-order-error-message-trigger-price';
|
||||||
|
const triggerTrailingPercentOffsetErrorMessage =
|
||||||
|
'stop-order-error-message-trigger-trailing-percent-offset';
|
||||||
|
|
||||||
|
describe('StopOrder', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display ticket defaults', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
// place order button should always be enabled
|
||||||
|
expect(screen.getByTestId(submitButton)).toBeEnabled();
|
||||||
|
|
||||||
|
// Assert defaults are used
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||||
|
expect(screen.getByTestId(orderTypeLimit).dataset.state).toEqual('checked');
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeLimit));
|
||||||
|
expect(screen.getByTestId(orderSideBuy).dataset.state).toEqual('checked');
|
||||||
|
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue('0');
|
||||||
|
expect(screen.getByTestId(timeInForce)).toHaveValue(
|
||||||
|
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
|
||||||
|
).toEqual('checked');
|
||||||
|
expect(screen.getByTestId(triggerTypePrice).dataset.state).toEqual(
|
||||||
|
'checked'
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId(expire).dataset.state).toEqual('unchecked');
|
||||||
|
await userEvent.click(screen.getByTestId(expire));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
|
||||||
|
'checked'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display trigger price as price for market type order', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||||
|
await userEvent.type(screen.getByTestId(triggerPriceInput), '10');
|
||||||
|
expect(screen.getByTestId('price')).toHaveTextContent('10.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use local storage state for initial values', async () => {
|
||||||
|
const values: Partial<StopOrderFormValues> = {
|
||||||
|
type: Schema.OrderType.TYPE_LIMIT,
|
||||||
|
side: Schema.Side.SIDE_SELL,
|
||||||
|
size: '0.1',
|
||||||
|
price: '300.22',
|
||||||
|
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||||
|
expire: true,
|
||||||
|
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS,
|
||||||
|
expiresAt: '2023-07-27T16:43:27.000',
|
||||||
|
};
|
||||||
|
|
||||||
|
useStopOrderFormValues.setState({
|
||||||
|
formValues: {
|
||||||
|
[market.id]: values,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(generateJsx());
|
||||||
|
// Assert correct defaults are used from store
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||||
|
expect(screen.queryByTestId(orderTypeLimit)).toBeChecked();
|
||||||
|
expect(screen.getByTestId(orderSideSell).dataset.state).toEqual('checked');
|
||||||
|
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue(
|
||||||
|
values.size as string
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('order-tif')).toHaveValue(values.timeInForce);
|
||||||
|
expect(screen.getByTestId(priceInput)).toHaveDisplayValue(
|
||||||
|
values.price as string
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId(expire).dataset.state).toEqual('checked');
|
||||||
|
expect(screen.getByTestId(expiryStrategyCancel).dataset.state).toEqual(
|
||||||
|
'checked'
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId(datePicker)).toHaveDisplayValue(
|
||||||
|
values.expiresAt as string
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows no wallet warning and do not submit if no wallet connected', async () => {
|
||||||
|
render(generateJsx(null));
|
||||||
|
await userEvent.type(screen.getByTestId(sizeInput), '1');
|
||||||
|
await userEvent.type(screen.getByTestId(priceInput), '1');
|
||||||
|
await userEvent.type(screen.getByTestId(triggerPriceInput), '1');
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
|
expect(submit).not.toBeCalled();
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('deal-ticket-connect-wallet')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls submit if form is valid', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
await userEvent.type(screen.getByTestId(sizeInput), '1');
|
||||||
|
await userEvent.type(screen.getByTestId(priceInput), '1');
|
||||||
|
await userEvent.type(screen.getByTestId(triggerPriceInput), '1');
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
|
expect(submit).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates size field', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
|
|
||||||
|
// default value should be invalid
|
||||||
|
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
|
||||||
|
// to small value should be invalid
|
||||||
|
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
|
||||||
|
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// clear and fill using valid value
|
||||||
|
await userEvent.clear(screen.getByTestId(sizeInput));
|
||||||
|
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
|
||||||
|
expect(screen.queryByTestId(sizeErrorMessage)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates price field', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
|
// price error message should not show if size has error
|
||||||
|
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||||
|
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
|
||||||
|
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||||
|
await userEvent.type(screen.getByTestId(priceInput), '0.001');
|
||||||
|
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// switch to market order type error should disappear
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||||
|
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||||
|
|
||||||
|
// switch back to limit type
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeLimit));
|
||||||
|
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// to small value should be invalid
|
||||||
|
await userEvent.type(screen.getByTestId(priceInput), '0.001');
|
||||||
|
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// clear and fill using valid value
|
||||||
|
await userEvent.clear(screen.getByTestId(priceInput));
|
||||||
|
await userEvent.type(screen.getByTestId(priceInput), '0.01');
|
||||||
|
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates trigger price field', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
|
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// switch to trailing percentage offset trigger type
|
||||||
|
await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
|
||||||
|
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
|
||||||
|
|
||||||
|
// switch back to price trigger type
|
||||||
|
await userEvent.click(screen.getByTestId(triggerTypePrice));
|
||||||
|
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// to small value should be invalid
|
||||||
|
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001');
|
||||||
|
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// clear and fill using valid value
|
||||||
|
await userEvent.clear(screen.getByTestId(triggerPriceInput));
|
||||||
|
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.01');
|
||||||
|
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates trigger trailing percentage offset field', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
|
||||||
|
// should not show error with default form values
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||||
|
).toBeNull();
|
||||||
|
|
||||||
|
// switch to trailing percentage offset trigger type
|
||||||
|
await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
|
||||||
|
expect(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// to small value should be invalid
|
||||||
|
await userEvent.type(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput),
|
||||||
|
'0.09'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// clear and fill using valid value
|
||||||
|
await userEvent.clear(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput)
|
||||||
|
);
|
||||||
|
await userEvent.type(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput),
|
||||||
|
'0.1'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||||
|
).toBeNull();
|
||||||
|
|
||||||
|
// to big value should be invalid
|
||||||
|
await userEvent.clear(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput)
|
||||||
|
);
|
||||||
|
await userEvent.type(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput),
|
||||||
|
'99.91'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// clear and fill using valid value
|
||||||
|
await userEvent.clear(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput)
|
||||||
|
);
|
||||||
|
await userEvent.type(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput),
|
||||||
|
'99.9'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
import type { FormEventHandler } from 'react';
|
||||||
|
import { useRef, useCallback, useEffect } from 'react';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
|
||||||
|
import {
|
||||||
|
formatNumber,
|
||||||
|
removeDecimal,
|
||||||
|
toDecimal,
|
||||||
|
validateAmount,
|
||||||
|
} from '@vegaprotocol/utils';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import {
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Input,
|
||||||
|
Checkbox,
|
||||||
|
FormGroup,
|
||||||
|
InputError,
|
||||||
|
Select,
|
||||||
|
Tooltip,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { ExpirySelector } from './expiry-selector';
|
||||||
|
import { SideSelector } from './side-selector';
|
||||||
|
import { timeInForceLabel, useOrder } from '@vegaprotocol/orders';
|
||||||
|
import {
|
||||||
|
NoWalletWarning,
|
||||||
|
REDUCE_ONLY_TOOLTIP,
|
||||||
|
useNotionalSize,
|
||||||
|
} from './deal-ticket';
|
||||||
|
import { TypeToggle } from './type-selector';
|
||||||
|
import {
|
||||||
|
useStopOrderFormValues,
|
||||||
|
type StopOrderFormValues,
|
||||||
|
} from '../../hooks/use-stop-order-form-values';
|
||||||
|
import {
|
||||||
|
DealTicketType,
|
||||||
|
useDealTicketTypeStore,
|
||||||
|
} from '../../hooks/use-type-store';
|
||||||
|
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-stop-order-submission';
|
||||||
|
import { DealTicketButton } from './deal-ticket-button';
|
||||||
|
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
||||||
|
import { validateExpiration } from '../../utils';
|
||||||
|
|
||||||
|
export interface StopOrderProps {
|
||||||
|
market: Market;
|
||||||
|
marketPrice?: string | null;
|
||||||
|
submit: (order: StopOrdersSubmission) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues: Partial<StopOrderFormValues> = {
|
||||||
|
type: Schema.OrderType.TYPE_LIMIT,
|
||||||
|
side: Schema.Side.SIDE_BUY,
|
||||||
|
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||||
|
triggerType: 'price',
|
||||||
|
triggerDirection:
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||||
|
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
|
||||||
|
size: '0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopSubmit: FormEventHandler = (e) => e.preventDefault();
|
||||||
|
|
||||||
|
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||||
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
|
||||||
|
const [, updateOrder] = useOrder(market.id);
|
||||||
|
const updateStoredFormValues = useStopOrderFormValues(
|
||||||
|
(state) => state.update
|
||||||
|
);
|
||||||
|
const storedFormValues = useStopOrderFormValues(
|
||||||
|
(state) => state.formValues[market.id]
|
||||||
|
);
|
||||||
|
const { handleSubmit, setValue, watch, control, formState } =
|
||||||
|
useForm<StopOrderFormValues>({
|
||||||
|
defaultValues: { ...defaultValues, ...storedFormValues },
|
||||||
|
});
|
||||||
|
const { errors } = formState;
|
||||||
|
const lastSubmitTime = useRef(0);
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(data: StopOrderFormValues) => {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submit(
|
||||||
|
mapFormValuesToStopOrdersSubmission(
|
||||||
|
data,
|
||||||
|
market.id,
|
||||||
|
market.decimalPlaces,
|
||||||
|
market.positionDecimalPlaces
|
||||||
|
)
|
||||||
|
);
|
||||||
|
lastSubmitTime.current = now;
|
||||||
|
},
|
||||||
|
[market.id, market.decimalPlaces, market.positionDecimalPlaces, submit]
|
||||||
|
);
|
||||||
|
const side = watch('side');
|
||||||
|
const expire = watch('expire');
|
||||||
|
const triggerType = watch('triggerType');
|
||||||
|
const triggerPrice = watch('triggerPrice');
|
||||||
|
const timeInForce = watch('timeInForce');
|
||||||
|
const type = watch('type');
|
||||||
|
const rawPrice = watch('price');
|
||||||
|
const rawSize = watch('size');
|
||||||
|
|
||||||
|
if (storedFormValues?.size && rawSize !== storedFormValues?.size) {
|
||||||
|
setValue('size', storedFormValues.size);
|
||||||
|
}
|
||||||
|
if (storedFormValues?.price && rawPrice !== storedFormValues?.price) {
|
||||||
|
setValue('price', storedFormValues.price);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPriceTrigger = triggerType === 'price';
|
||||||
|
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
|
||||||
|
const price =
|
||||||
|
marketPrice &&
|
||||||
|
getDerivedPrice(
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
price: rawPrice && removeDecimal(rawPrice, market.decimalPlaces),
|
||||||
|
},
|
||||||
|
type === Schema.OrderType.TYPE_MARKET && isPriceTrigger && triggerPrice
|
||||||
|
? removeDecimal(triggerPrice, market.decimalPlaces)
|
||||||
|
: marketPrice
|
||||||
|
);
|
||||||
|
|
||||||
|
const notionalSize = useNotionalSize(
|
||||||
|
price,
|
||||||
|
size,
|
||||||
|
market.decimalPlaces,
|
||||||
|
market.positionDecimalPlaces
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = watch((value, { name, type }) => {
|
||||||
|
updateStoredFormValues(market.id, value);
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [watch, market.id, updateStoredFormValues]);
|
||||||
|
|
||||||
|
const { quoteName, settlementAsset: asset } =
|
||||||
|
market.tradableInstrument.instrument.product;
|
||||||
|
|
||||||
|
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||||
|
const priceStep = toDecimal(market?.decimalPlaces);
|
||||||
|
const trailingPercentOffsetStep = '0.1';
|
||||||
|
|
||||||
|
const priceFormatted =
|
||||||
|
isPriceTrigger && triggerPrice
|
||||||
|
? formatNumber(triggerPrice, market.decimalPlaces)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={isReadOnly || !pubKey ? stopSubmit : handleSubmit(onSubmit)}
|
||||||
|
noValidate
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="type"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { value } = field;
|
||||||
|
return (
|
||||||
|
<TypeToggle
|
||||||
|
value={
|
||||||
|
value === Schema.OrderType.TYPE_LIMIT
|
||||||
|
? DealTicketType.StopLimit
|
||||||
|
: DealTicketType.StopMarket
|
||||||
|
}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const type = value as DealTicketType;
|
||||||
|
setDealTicketType(market.id, type);
|
||||||
|
if (
|
||||||
|
type === DealTicketType.Limit ||
|
||||||
|
type === DealTicketType.Market
|
||||||
|
) {
|
||||||
|
updateOrder({
|
||||||
|
type:
|
||||||
|
type === DealTicketType.Limit
|
||||||
|
? Schema.OrderType.TYPE_LIMIT
|
||||||
|
: Schema.OrderType.TYPE_MARKET,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue(
|
||||||
|
'type',
|
||||||
|
type === DealTicketType.StopLimit
|
||||||
|
? Schema.OrderType.TYPE_LIMIT
|
||||||
|
: Schema.OrderType.TYPE_MARKET
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.type && (
|
||||||
|
<InputError testId="stop-order-error-message-type">
|
||||||
|
{errors.type.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="side"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<SideSelector value={field.value} onValueChange={field.onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormGroup label={t('Trigger')} compact={true} labelFor="">
|
||||||
|
<Controller
|
||||||
|
name="triggerDirection"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { onChange, value } = field;
|
||||||
|
return (
|
||||||
|
<RadioGroup
|
||||||
|
name="triggerDirection"
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
orientation="horizontal"
|
||||||
|
className="mb-2"
|
||||||
|
>
|
||||||
|
<Radio
|
||||||
|
value={
|
||||||
|
Schema.StopOrderTriggerDirection
|
||||||
|
.TRIGGER_DIRECTION_RISES_ABOVE
|
||||||
|
}
|
||||||
|
id="triggerDirection-risesAbove"
|
||||||
|
label={'Rises above'}
|
||||||
|
/>
|
||||||
|
<Radio
|
||||||
|
value={
|
||||||
|
Schema.StopOrderTriggerDirection
|
||||||
|
.TRIGGER_DIRECTION_FALLS_BELOW
|
||||||
|
}
|
||||||
|
id="triggerDirection-fallsBelow"
|
||||||
|
label={'Falls below'}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isPriceTrigger && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<Controller
|
||||||
|
name="triggerPrice"
|
||||||
|
rules={{
|
||||||
|
required: t('You need provide a price'),
|
||||||
|
min: {
|
||||||
|
value: priceStep,
|
||||||
|
message: t('Price cannot be lower than ' + priceStep),
|
||||||
|
},
|
||||||
|
validate: validateAmount(priceStep, 'Price'),
|
||||||
|
}}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { value, ...props } = field;
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<Input
|
||||||
|
data-testid="triggerPrice"
|
||||||
|
type="number"
|
||||||
|
step={priceStep}
|
||||||
|
appendElement={asset.symbol}
|
||||||
|
value={value || ''}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.triggerPrice && (
|
||||||
|
<InputError testId="stop-order-error-message-trigger-price">
|
||||||
|
{errors.triggerPrice.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isPriceTrigger && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<Controller
|
||||||
|
name="triggerTrailingPercentOffset"
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
required: t('You need provide a trailing percent offset'),
|
||||||
|
min: {
|
||||||
|
value: trailingPercentOffsetStep,
|
||||||
|
message: t(
|
||||||
|
'Trailing percent offset cannot be lower than ' +
|
||||||
|
trailingPercentOffsetStep
|
||||||
|
),
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
value: '99.9',
|
||||||
|
message: t(
|
||||||
|
'Trailing percent offset cannot be higher than 99.9'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
validate: validateAmount(
|
||||||
|
trailingPercentOffsetStep,
|
||||||
|
'Trailing percentage offset'
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { value, ...props } = field;
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step={trailingPercentOffsetStep}
|
||||||
|
appendElement="%"
|
||||||
|
data-testid="triggerTrailingPercentOffset"
|
||||||
|
value={value || ''}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.triggerTrailingPercentOffset && (
|
||||||
|
<InputError testId="stop-order-error-message-trigger-trailing-percent-offset">
|
||||||
|
{errors.triggerTrailingPercentOffset.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
name="triggerType"
|
||||||
|
control={control}
|
||||||
|
rules={{ deps: ['triggerTrailingPercentOffset', 'triggerPrice'] }}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { onChange, value } = field;
|
||||||
|
return (
|
||||||
|
<RadioGroup
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
orientation="horizontal"
|
||||||
|
>
|
||||||
|
<Radio value="price" id="triggerType-price" label={'Price'} />
|
||||||
|
<Radio
|
||||||
|
value="trailingPercentOffset"
|
||||||
|
id="triggerType-trailingPercentOffset"
|
||||||
|
label={'Trailing Percent Offset'}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<FormGroup
|
||||||
|
labelFor="input-price-quote"
|
||||||
|
label={t(`Size`)}
|
||||||
|
className="!mb-0 flex-1"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="size"
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
required: t('You need to provide a size'),
|
||||||
|
min: {
|
||||||
|
value: sizeStep,
|
||||||
|
message: t('Size cannot be lower than ' + sizeStep),
|
||||||
|
},
|
||||||
|
validate: validateAmount(sizeStep, 'Size'),
|
||||||
|
}}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { value, ...props } = field;
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
id="order-size"
|
||||||
|
className="w-full"
|
||||||
|
type="number"
|
||||||
|
step={sizeStep}
|
||||||
|
min={sizeStep}
|
||||||
|
onWheel={(e) => e.currentTarget.blur()}
|
||||||
|
data-testid="order-size"
|
||||||
|
value={value || ''}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<div className="pt-7 leading-10">@</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
{type === Schema.OrderType.TYPE_LIMIT ? (
|
||||||
|
<FormGroup
|
||||||
|
labelFor="input-price-quote"
|
||||||
|
label={t(`Price (${quoteName})`)}
|
||||||
|
labelAlign="right"
|
||||||
|
className="!mb-0"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="price"
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
deps: 'type',
|
||||||
|
required: t('You need provide a price'),
|
||||||
|
min: {
|
||||||
|
value: priceStep,
|
||||||
|
message: t('Price cannot be lower than ' + priceStep),
|
||||||
|
},
|
||||||
|
validate: validateAmount(priceStep, 'Price'),
|
||||||
|
}}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { value, ...props } = field;
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
id="input-price-quote"
|
||||||
|
className="w-full"
|
||||||
|
type="number"
|
||||||
|
step={priceStep}
|
||||||
|
data-testid="order-price"
|
||||||
|
onWheel={(e) => e.currentTarget.blur()}
|
||||||
|
value={value || ''}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="text-sm text-right pt-7 leading-10"
|
||||||
|
data-testid="price"
|
||||||
|
>
|
||||||
|
{priceFormatted && quoteName
|
||||||
|
? `~${priceFormatted} ${quoteName}`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{errors.size && (
|
||||||
|
<InputError testId="stop-order-error-message-size">
|
||||||
|
{errors.size.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!errors.size &&
|
||||||
|
errors.price &&
|
||||||
|
type === Schema.OrderType.TYPE_LIMIT && (
|
||||||
|
<InputError testId="stop-order-error-message-price">
|
||||||
|
{errors.price.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<FormGroup
|
||||||
|
label={t('Time in force')}
|
||||||
|
labelFor="select-time-in-force"
|
||||||
|
compact={true}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="timeInForce"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
id="select-time-in-force"
|
||||||
|
className="w-full"
|
||||||
|
data-testid="order-tif"
|
||||||
|
{...field}
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
key={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||||
|
value={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||||
|
>
|
||||||
|
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_IOC)}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
key={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||||
|
value={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||||
|
>
|
||||||
|
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
|
||||||
|
</option>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
{errors.timeInForce && (
|
||||||
|
<InputError testId="stop-error-message-tif">
|
||||||
|
{errors.timeInForce.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pb-2 justify-end">
|
||||||
|
<Checkbox
|
||||||
|
name="reduce-only"
|
||||||
|
checked={true}
|
||||||
|
disabled={true}
|
||||||
|
label={
|
||||||
|
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
|
||||||
|
<span className="text-xs">{t('Reduce only')}</span>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<Controller
|
||||||
|
name="expire"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { onChange: onCheckedChange, value } = field;
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
onCheckedChange={onCheckedChange}
|
||||||
|
checked={value}
|
||||||
|
name="expire"
|
||||||
|
label={'Expire'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{expire && (
|
||||||
|
<>
|
||||||
|
<FormGroup
|
||||||
|
label={t('Strategy')}
|
||||||
|
labelFor="expiryStrategy"
|
||||||
|
compact={true}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="expiryStrategy"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<RadioGroup orientation="horizontal" {...field}>
|
||||||
|
<Radio
|
||||||
|
value={
|
||||||
|
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
||||||
|
}
|
||||||
|
id="expiryStrategy-submit"
|
||||||
|
label={'Submit'}
|
||||||
|
/>
|
||||||
|
<Radio
|
||||||
|
value={
|
||||||
|
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
|
||||||
|
}
|
||||||
|
id="expiryStrategy-cancel"
|
||||||
|
label={'Cancel'}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<div className="mb-2">
|
||||||
|
<Controller
|
||||||
|
name="expiresAt"
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
validate: validateExpiration,
|
||||||
|
}}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { value, onChange: onSelect } = field;
|
||||||
|
return (
|
||||||
|
<ExpirySelector
|
||||||
|
value={value}
|
||||||
|
onSelect={onSelect}
|
||||||
|
errorMessage={errors.expiresAt?.message}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<NoWalletWarning pubKey={pubKey} isReadOnly={isReadOnly} asset={asset} />
|
||||||
|
<DealTicketButton side={side} label={t('Submit Stop Order')} />
|
||||||
|
<DealTicketFeeDetails
|
||||||
|
order={{
|
||||||
|
marketId: market.id,
|
||||||
|
price: price || undefined,
|
||||||
|
side,
|
||||||
|
size,
|
||||||
|
timeInForce,
|
||||||
|
type,
|
||||||
|
}}
|
||||||
|
notionalSize={notionalSize}
|
||||||
|
assetSymbol={asset.symbol}
|
||||||
|
market={market}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -22,8 +22,12 @@ import { OrdersDocument } from '@vegaprotocol/orders';
|
|||||||
jest.mock('zustand');
|
jest.mock('zustand');
|
||||||
jest.mock('./deal-ticket-fee-details', () => ({
|
jest.mock('./deal-ticket-fee-details', () => ({
|
||||||
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
|
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
|
||||||
|
DealTicketMarginDetails: () => (
|
||||||
|
<div data-testid="deal-ticket-margin-details" />
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const marketPrice = '200';
|
||||||
const pubKey = 'pubKey';
|
const pubKey = 'pubKey';
|
||||||
const market = generateMarket();
|
const market = generateMarket();
|
||||||
const marketData = generateMarketData();
|
const marketData = generateMarketData();
|
||||||
@@ -36,6 +40,7 @@ function generateJsx(mocks: MockedResponse[] = []) {
|
|||||||
<DealTicket
|
<DealTicket
|
||||||
market={market}
|
market={market}
|
||||||
marketData={marketData}
|
marketData={marketData}
|
||||||
|
marketPrice={marketPrice}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
onDeposit={jest.fn()}
|
onDeposit={jest.fn()}
|
||||||
/>
|
/>
|
||||||
@@ -114,30 +119,22 @@ describe('DealTicket', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should display ticket defaults', () => {
|
it('should display ticket defaults', () => {
|
||||||
const { container } = render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// place order button should always be enabled
|
// place order button should always be enabled
|
||||||
expect(screen.getByTestId('place-order')).toBeEnabled();
|
expect(screen.getByTestId('place-order')).toBeEnabled();
|
||||||
|
|
||||||
// Assert defaults are used
|
// Assert defaults are used
|
||||||
expect(
|
expect(screen.getByTestId('order-type-Market')).toBeInTheDocument();
|
||||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_MARKET}`)
|
expect(screen.getByTestId('order-type-Limit')).toBeInTheDocument();
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
|
||||||
).toBeInTheDocument();
|
|
||||||
|
|
||||||
const oderTypeLimitToggle = container.querySelector(
|
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||||
`[data-testid="order-type-${Schema.OrderType.TYPE_LIMIT}"] input[type="radio"]`
|
'checked'
|
||||||
);
|
);
|
||||||
expect(oderTypeLimitToggle).toBeChecked();
|
|
||||||
|
|
||||||
expect(
|
expect(screen.getByTestId('order-side-SIDE_BUY').dataset.state).toEqual(
|
||||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
'checked'
|
||||||
).toBeChecked();
|
);
|
||||||
expect(
|
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue('0');
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue('0');
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
||||||
@@ -147,12 +144,12 @@ describe('DealTicket', () => {
|
|||||||
it('should display last price for market type order', () => {
|
it('should display last price for market type order', () => {
|
||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
act(() => {
|
act(() => {
|
||||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_MARKET}`).click();
|
screen.getByTestId('order-type-Market').click();
|
||||||
});
|
});
|
||||||
// Assert last price is shown
|
// Assert last price is shown
|
||||||
expect(screen.getByTestId('last-price')).toHaveTextContent(
|
expect(screen.getByTestId('last-price')).toHaveTextContent(
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
`~${addDecimal(marketData.markPrice, market.decimalPlaces)} ${
|
`~${addDecimal(marketPrice, market.decimalPlaces)} ${
|
||||||
market.tradableInstrument.instrument.product.quoteName
|
market.tradableInstrument.instrument.product.quoteName
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
@@ -178,17 +175,12 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// Assert correct defaults are used from store
|
// Assert correct defaults are used from store
|
||||||
expect(
|
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||||
screen
|
'checked'
|
||||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
);
|
||||||
.querySelector('input')
|
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||||
).toBeChecked();
|
'checked'
|
||||||
expect(
|
);
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
|
||||||
).toBeChecked();
|
|
||||||
expect(
|
|
||||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||||
expectedOrder.size
|
expectedOrder.size
|
||||||
);
|
);
|
||||||
@@ -221,17 +213,12 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// Assert correct defaults are used from store
|
// Assert correct defaults are used from store
|
||||||
expect(
|
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||||
screen
|
'checked'
|
||||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
);
|
||||||
.querySelector('input')
|
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||||
).toBeChecked();
|
'checked'
|
||||||
expect(
|
);
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
|
||||||
).toBeChecked();
|
|
||||||
expect(
|
|
||||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||||
expectedOrder.size
|
expectedOrder.size
|
||||||
);
|
);
|
||||||
@@ -269,17 +256,12 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// Assert correct defaults are used from store
|
// Assert correct defaults are used from store
|
||||||
expect(
|
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||||
screen
|
'checked'
|
||||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
);
|
||||||
.querySelector('input')
|
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||||
).toBeChecked();
|
'checked'
|
||||||
expect(
|
);
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
|
||||||
).toBeChecked();
|
|
||||||
expect(
|
|
||||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||||
expectedOrder.size
|
expectedOrder.size
|
||||||
);
|
);
|
||||||
@@ -322,17 +304,12 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// Assert correct defaults are used from store
|
// Assert correct defaults are used from store
|
||||||
expect(
|
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||||
screen
|
'checked'
|
||||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
);
|
||||||
.querySelector('input')
|
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||||
).toBeChecked();
|
'checked'
|
||||||
expect(
|
);
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
|
||||||
).toBeChecked();
|
|
||||||
expect(
|
|
||||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||||
expectedOrder.size
|
expectedOrder.size
|
||||||
);
|
);
|
||||||
@@ -371,17 +348,12 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// Assert correct defaults are used from store
|
// Assert correct defaults are used from store
|
||||||
expect(
|
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||||
screen
|
'checked'
|
||||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
);
|
||||||
.querySelector('input')
|
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||||
).toBeChecked();
|
'checked'
|
||||||
expect(
|
);
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
|
||||||
).toBeChecked();
|
|
||||||
expect(
|
|
||||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||||
expectedOrder.size
|
expectedOrder.size
|
||||||
);
|
);
|
||||||
@@ -402,7 +374,7 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_MARKET}`).click();
|
screen.getByTestId('order-type-Market').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only FOK and IOC should be present for type market order
|
// Only FOK and IOC should be present for type market order
|
||||||
@@ -427,7 +399,7 @@ describe('DealTicket', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Switch to type limit order -> all TIF options should be shown
|
// Switch to type limit order -> all TIF options should be shown
|
||||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
await userEvent.click(screen.getByTestId('order-type-Limit'));
|
||||||
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
||||||
Object.keys(Schema.OrderTimeInForce).length
|
Object.keys(Schema.OrderTimeInForce).length
|
||||||
);
|
);
|
||||||
@@ -447,7 +419,7 @@ describe('DealTicket', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Switch back to type market order -> FOK should be preserved from previous selection
|
// Switch back to type market order -> FOK should be preserved from previous selection
|
||||||
await userEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
await userEvent.click(screen.getByTestId('order-type-Market'));
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||||
);
|
);
|
||||||
@@ -462,7 +434,7 @@ describe('DealTicket', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Switch back type limit order -> GTT should be preserved
|
// Switch back type limit order -> GTT should be preserved
|
||||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
await userEvent.click(screen.getByTestId('order-type-Limit'));
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||||
);
|
);
|
||||||
@@ -477,7 +449,7 @@ describe('DealTicket', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Switch to type market order -> IOC should be preserved
|
// Switch to type market order -> IOC should be preserved
|
||||||
await userEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
await userEvent.click(screen.getByTestId('order-type-Market'));
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||||
);
|
);
|
||||||
@@ -487,9 +459,9 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// BUY is selected by default
|
// BUY is selected by default
|
||||||
expect(
|
expect(screen.getByTestId('order-side-SIDE_BUY').dataset.state).toEqual(
|
||||||
screen.getByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
'checked'
|
||||||
).toBeChecked();
|
);
|
||||||
|
|
||||||
await userEvent.type(screen.getByTestId('order-size'), '200');
|
await userEvent.type(screen.getByTestId('order-size'), '200');
|
||||||
|
|
||||||
@@ -504,7 +476,7 @@ describe('DealTicket', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Switch to limit order
|
// Switch to limit order
|
||||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
await userEvent.click(screen.getByTestId('order-type-Limit'));
|
||||||
|
|
||||||
// Check all TIF options shown
|
// Check all TIF options shown
|
||||||
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { memo, useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
|||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
import { DealTicketAmount } from './deal-ticket-amount';
|
import { DealTicketAmount } from './deal-ticket-amount';
|
||||||
import { DealTicketButton } from './deal-ticket-button';
|
import { DealTicketButton } from './deal-ticket-button';
|
||||||
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
import {
|
||||||
|
DealTicketFeeDetails,
|
||||||
|
DealTicketMarginDetails,
|
||||||
|
} from './deal-ticket-fee-details';
|
||||||
import { ExpirySelector } from './expiry-selector';
|
import { ExpirySelector } from './expiry-selector';
|
||||||
import { SideSelector } from './side-selector';
|
import { SideSelector } from './side-selector';
|
||||||
import { TimeInForceSelector } from './time-in-force-selector';
|
import { TimeInForceSelector } from './time-in-force-selector';
|
||||||
@@ -30,8 +33,7 @@ import {
|
|||||||
} from '@vegaprotocol/positions';
|
} from '@vegaprotocol/positions';
|
||||||
import { toBigNum, removeDecimal } from '@vegaprotocol/utils';
|
import { toBigNum, removeDecimal } from '@vegaprotocol/utils';
|
||||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
||||||
import { useEstimateFees } from '../../hooks/use-estimate-fees';
|
import { getDerivedPrice } from '@vegaprotocol/markets';
|
||||||
import { getDerivedPrice } from '../../utils/get-price';
|
|
||||||
import type { OrderInfo } from '@vegaprotocol/types';
|
import type { OrderInfo } from '@vegaprotocol/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -43,7 +45,11 @@ import {
|
|||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
|
import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
|
||||||
import { SummaryValidationType } from '../../constants';
|
import { SummaryValidationType } from '../../constants';
|
||||||
import type { Market, MarketData } from '@vegaprotocol/markets';
|
import type {
|
||||||
|
Market,
|
||||||
|
MarketData,
|
||||||
|
StaticMarketData,
|
||||||
|
} from '@vegaprotocol/markets';
|
||||||
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
||||||
import {
|
import {
|
||||||
useMarketAccountBalance,
|
useMarketAccountBalance,
|
||||||
@@ -53,26 +59,59 @@ import {
|
|||||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||||
import { useOrderForm } from '../../hooks/use-order-form';
|
import { useOrderForm } from '../../hooks/use-order-form';
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
|
import {
|
||||||
|
DealTicketType,
|
||||||
|
useDealTicketTypeStore,
|
||||||
|
} from '../../hooks/use-type-store';
|
||||||
|
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||||
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||||
|
import noop from 'lodash/noop';
|
||||||
|
|
||||||
|
export const REDUCE_ONLY_TOOLTIP =
|
||||||
|
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
|
||||||
|
|
||||||
export interface DealTicketProps {
|
export interface DealTicketProps {
|
||||||
market: Market;
|
market: Market;
|
||||||
marketData: MarketData;
|
marketData: StaticMarketData;
|
||||||
|
marketPrice?: string | null;
|
||||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
submit: (order: OrderSubmission) => void;
|
submit: (order: OrderSubmission) => void;
|
||||||
onClickCollateral?: () => void;
|
onClickCollateral?: () => void;
|
||||||
onDeposit: (assetId: string) => void;
|
onDeposit: (assetId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useNotionalSize = (
|
||||||
|
price: string | null | undefined,
|
||||||
|
size: string | undefined,
|
||||||
|
decimalPlaces: number,
|
||||||
|
positionDecimalPlaces: number
|
||||||
|
) =>
|
||||||
|
useMemo(() => {
|
||||||
|
if (price && size) {
|
||||||
|
return removeDecimal(
|
||||||
|
toBigNum(size, positionDecimalPlaces).multipliedBy(
|
||||||
|
toBigNum(price, decimalPlaces)
|
||||||
|
),
|
||||||
|
decimalPlaces
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [price, size, decimalPlaces, positionDecimalPlaces]);
|
||||||
|
|
||||||
export const DealTicket = ({
|
export const DealTicket = ({
|
||||||
market,
|
market,
|
||||||
onMarketClick,
|
onMarketClick,
|
||||||
marketData,
|
marketData,
|
||||||
|
marketPrice,
|
||||||
submit,
|
submit,
|
||||||
onClickCollateral,
|
onClickCollateral,
|
||||||
onDeposit,
|
onDeposit,
|
||||||
}: DealTicketProps) => {
|
}: DealTicketProps) => {
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
|
||||||
|
const updateStopOrderFormValues = useStopOrderFormValues(
|
||||||
|
(state) => state.update
|
||||||
|
);
|
||||||
// store last used tif for market so that when changing OrderType the previous TIF
|
// store last used tif for market so that when changing OrderType the previous TIF
|
||||||
// selection for that type is used when switching back
|
// selection for that type is used when switching back
|
||||||
|
|
||||||
@@ -95,11 +134,15 @@ export const DealTicket = ({
|
|||||||
|
|
||||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||||
|
|
||||||
const { accountBalance: marginAccountBalance } = useMarketAccountBalance(
|
const {
|
||||||
market.id
|
accountBalance: marginAccountBalance,
|
||||||
);
|
loading: loadingMarginAccountBalance,
|
||||||
|
} = useMarketAccountBalance(market.id);
|
||||||
|
|
||||||
const { accountBalance: generalAccountBalance } = useAccountBalance(asset.id);
|
const {
|
||||||
|
accountBalance: generalAccountBalance,
|
||||||
|
loading: loadingGeneralAccountBalance,
|
||||||
|
} = useAccountBalance(asset.id);
|
||||||
|
|
||||||
const balance = (
|
const balance = (
|
||||||
BigInt(marginAccountBalance) + BigInt(generalAccountBalance)
|
BigInt(marginAccountBalance) + BigInt(generalAccountBalance)
|
||||||
@@ -116,30 +159,20 @@ export const DealTicket = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const price = useMemo(() => {
|
const price = useMemo(() => {
|
||||||
return normalizedOrder && getDerivedPrice(normalizedOrder, marketData);
|
return (
|
||||||
}, [normalizedOrder, marketData]);
|
normalizedOrder &&
|
||||||
|
marketPrice &&
|
||||||
|
getDerivedPrice(normalizedOrder, marketPrice)
|
||||||
|
);
|
||||||
|
}, [normalizedOrder, marketPrice]);
|
||||||
|
|
||||||
const notionalSize = useMemo(() => {
|
const notionalSize = useNotionalSize(
|
||||||
if (price && normalizedOrder?.size) {
|
|
||||||
return removeDecimal(
|
|
||||||
toBigNum(
|
|
||||||
normalizedOrder.size,
|
|
||||||
market.positionDecimalPlaces
|
|
||||||
).multipliedBy(toBigNum(price, market.decimalPlaces)),
|
|
||||||
market.decimalPlaces
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [
|
|
||||||
price,
|
price,
|
||||||
normalizedOrder?.size,
|
normalizedOrder?.size,
|
||||||
market.decimalPlaces,
|
market.decimalPlaces,
|
||||||
market.positionDecimalPlaces,
|
market.positionDecimalPlaces
|
||||||
]);
|
|
||||||
|
|
||||||
const feeEstimate = useEstimateFees(
|
|
||||||
normalizedOrder && { ...normalizedOrder, price }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: activeOrders } = useDataProvider({
|
const { data: activeOrders } = useDataProvider({
|
||||||
dataProvider: activeOrdersProvider,
|
dataProvider: activeOrdersProvider,
|
||||||
variables: { partyId: pubKey || '', marketId: market.id },
|
variables: { partyId: pubKey || '', marketId: market.id },
|
||||||
@@ -197,7 +230,10 @@ export const DealTicket = ({
|
|||||||
|
|
||||||
const hasNoBalance =
|
const hasNoBalance =
|
||||||
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
|
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
|
||||||
if (hasNoBalance) {
|
if (
|
||||||
|
hasNoBalance &&
|
||||||
|
!(loadingMarginAccountBalance || loadingGeneralAccountBalance)
|
||||||
|
) {
|
||||||
setError('summary', {
|
setError('summary', {
|
||||||
message: SummaryValidationType.NoCollateral,
|
message: SummaryValidationType.NoCollateral,
|
||||||
type: SummaryValidationType.NoCollateral,
|
type: SummaryValidationType.NoCollateral,
|
||||||
@@ -221,6 +257,8 @@ export const DealTicket = ({
|
|||||||
marketTradingMode,
|
marketTradingMode,
|
||||||
generalAccountBalance,
|
generalAccountBalance,
|
||||||
marginAccountBalance,
|
marginAccountBalance,
|
||||||
|
loadingMarginAccountBalance,
|
||||||
|
loadingGeneralAccountBalance,
|
||||||
pubKey,
|
pubKey,
|
||||||
setError,
|
setError,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
@@ -265,11 +303,13 @@ export const DealTicket = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// if an order doesn't exist one will be created by the store immediately
|
// if an order doesn't exist one will be created by the store immediately
|
||||||
if (!order || !normalizedOrder) return null;
|
if (!order || !normalizedOrder) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={isReadOnly ? undefined : handleSubmit(onSubmit)}
|
onSubmit={isReadOnly ? noop : handleSubmit(onSubmit)}
|
||||||
noValidate
|
noValidate
|
||||||
data-testid="deal-ticket-form"
|
data-testid="deal-ticket-form"
|
||||||
>
|
>
|
||||||
@@ -284,9 +324,29 @@ export const DealTicket = ({
|
|||||||
}}
|
}}
|
||||||
render={() => (
|
render={() => (
|
||||||
<TypeSelector
|
<TypeSelector
|
||||||
value={order.type}
|
value={
|
||||||
onSelect={(type) => {
|
order.type === OrderType.TYPE_LIMIT
|
||||||
if (type === OrderType.TYPE_NETWORK) return;
|
? DealTicketType.Limit
|
||||||
|
: DealTicketType.Market
|
||||||
|
}
|
||||||
|
onValueChange={(dealTicketType) => {
|
||||||
|
setDealTicketType(market.id, dealTicketType);
|
||||||
|
if (
|
||||||
|
dealTicketType !== DealTicketType.Limit &&
|
||||||
|
dealTicketType !== DealTicketType.Market
|
||||||
|
) {
|
||||||
|
updateStopOrderFormValues(market.id, {
|
||||||
|
type:
|
||||||
|
dealTicketType === DealTicketType.StopLimit
|
||||||
|
? OrderType.TYPE_LIMIT
|
||||||
|
: OrderType.TYPE_MARKET,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const type =
|
||||||
|
dealTicketType === DealTicketType.Limit
|
||||||
|
? OrderType.TYPE_LIMIT
|
||||||
|
: OrderType.TYPE_MARKET;
|
||||||
update({
|
update({
|
||||||
type,
|
type,
|
||||||
// when changing type also update the TIF to what was last used of new type
|
// when changing type also update the TIF to what was last used of new type
|
||||||
@@ -333,7 +393,7 @@ export const DealTicket = ({
|
|||||||
render={() => (
|
render={() => (
|
||||||
<SideSelector
|
<SideSelector
|
||||||
value={order.side}
|
value={order.side}
|
||||||
onSelect={(side) => {
|
onValueChange={(side) => {
|
||||||
update({ side });
|
update({ side });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -344,6 +404,7 @@ export const DealTicket = ({
|
|||||||
orderType={order.type}
|
orderType={order.type}
|
||||||
market={market}
|
market={market}
|
||||||
marketData={marketData}
|
marketData={marketData}
|
||||||
|
marketPrice={marketPrice || undefined}
|
||||||
sizeError={errors.size?.message}
|
sizeError={errors.size?.message}
|
||||||
priceError={errors.price?.message}
|
priceError={errors.price?.message}
|
||||||
update={update}
|
update={update}
|
||||||
@@ -467,9 +528,7 @@ export const DealTicket = ({
|
|||||||
? t(
|
? t(
|
||||||
'"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".'
|
'"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".'
|
||||||
)
|
)
|
||||||
: t(
|
: t(REDUCE_ONLY_TOOLTIP)}
|
||||||
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.'
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -541,10 +600,16 @@ export const DealTicket = ({
|
|||||||
/>
|
/>
|
||||||
<DealTicketButton side={order.side} />
|
<DealTicketButton side={order.side} />
|
||||||
<DealTicketFeeDetails
|
<DealTicketFeeDetails
|
||||||
onMarketClick={onMarketClick}
|
order={
|
||||||
feeEstimate={feeEstimate}
|
normalizedOrder && { ...normalizedOrder, price: price || undefined }
|
||||||
|
}
|
||||||
notionalSize={notionalSize}
|
notionalSize={notionalSize}
|
||||||
assetSymbol={assetSymbol}
|
assetSymbol={assetSymbol}
|
||||||
|
market={market}
|
||||||
|
/>
|
||||||
|
<DealTicketMarginDetails
|
||||||
|
onMarketClick={onMarketClick}
|
||||||
|
assetSymbol={assetSymbol}
|
||||||
marginAccountBalance={marginAccountBalance}
|
marginAccountBalance={marginAccountBalance}
|
||||||
generalAccountBalance={generalAccountBalance}
|
generalAccountBalance={generalAccountBalance}
|
||||||
positionEstimate={positionEstimate?.estimatePosition}
|
positionEstimate={positionEstimate?.estimatePosition}
|
||||||
@@ -569,6 +634,55 @@ interface SummaryMessageProps {
|
|||||||
onClickCollateral?: () => void;
|
onClickCollateral?: () => void;
|
||||||
onDeposit: (assetId: string) => void;
|
onDeposit: (assetId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const NoWalletWarning = ({
|
||||||
|
isReadOnly,
|
||||||
|
pubKey,
|
||||||
|
asset,
|
||||||
|
}: Pick<SummaryMessageProps, 'isReadOnly' | 'pubKey' | 'asset'>) => {
|
||||||
|
const assetSymbol = asset.symbol;
|
||||||
|
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||||
|
(store) => store.openVegaWalletDialog
|
||||||
|
);
|
||||||
|
if (isReadOnly) {
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<InputError testId="deal-ticket-error-message-summary">
|
||||||
|
{
|
||||||
|
'You need to connect your own wallet to start trading on this market'
|
||||||
|
}
|
||||||
|
</InputError>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!pubKey) {
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<Notification
|
||||||
|
testId={'deal-ticket-connect-wallet'}
|
||||||
|
intent={Intent.Warning}
|
||||||
|
message={
|
||||||
|
<p className="text-sm pb-2">
|
||||||
|
You need a{' '}
|
||||||
|
<ExternalLink href="https://vega.xyz/wallet">
|
||||||
|
Vega wallet
|
||||||
|
</ExternalLink>{' '}
|
||||||
|
with {assetSymbol} to start trading in this market.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
buttonProps={{
|
||||||
|
text: t('Connect wallet'),
|
||||||
|
action: openVegaWalletDialog,
|
||||||
|
dataTestId: 'order-connect-wallet',
|
||||||
|
size: 'small',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const SummaryMessage = memo(
|
const SummaryMessage = memo(
|
||||||
({
|
({
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -583,46 +697,16 @@ const SummaryMessage = memo(
|
|||||||
}: SummaryMessageProps) => {
|
}: SummaryMessageProps) => {
|
||||||
// Specific error UI for if balance is so we can
|
// Specific error UI for if balance is so we can
|
||||||
// render a deposit dialog
|
// render a deposit dialog
|
||||||
const assetSymbol = asset.symbol;
|
if (isReadOnly || !pubKey) {
|
||||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
|
||||||
(store) => store.openVegaWalletDialog
|
|
||||||
);
|
|
||||||
if (isReadOnly) {
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<NoWalletWarning
|
||||||
<InputError testId="deal-ticket-error-message-summary">
|
isReadOnly={isReadOnly}
|
||||||
{
|
asset={asset}
|
||||||
'You need to connect your own wallet to start trading on this market'
|
pubKey={pubKey}
|
||||||
}
|
/>
|
||||||
</InputError>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!pubKey) {
|
|
||||||
return (
|
|
||||||
<div className="mb-2">
|
|
||||||
<Notification
|
|
||||||
testId={'deal-ticket-connect-wallet'}
|
|
||||||
intent={Intent.Warning}
|
|
||||||
message={
|
|
||||||
<p className="text-sm pb-2">
|
|
||||||
You need a{' '}
|
|
||||||
<ExternalLink href="https://vega.xyz/wallet">
|
|
||||||
Vega wallet
|
|
||||||
</ExternalLink>{' '}
|
|
||||||
with {assetSymbol} to start trading in this market.
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
buttonProps={{
|
|
||||||
text: t('Connect wallet'),
|
|
||||||
action: openVegaWalletDialog,
|
|
||||||
dataTestId: 'order-connect-wallet',
|
|
||||||
size: 'small',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorMessage === SummaryValidationType.NoCollateral) {
|
if (errorMessage === SummaryValidationType.NoCollateral) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||||
import { formatForInput } from '@vegaprotocol/utils';
|
import { formatForInput } from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
interface ExpirySelectorProps {
|
interface ExpirySelectorProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
@@ -13,7 +14,8 @@ export const ExpirySelector = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
}: ExpirySelectorProps) => {
|
}: ExpirySelectorProps) => {
|
||||||
const date = value ? new Date(value) : new Date();
|
const now = useRef(new Date());
|
||||||
|
const date = value ? new Date(value) : now.current;
|
||||||
const dateFormatted = formatForInput(date);
|
const dateFormatted = formatForInput(date);
|
||||||
const minDate = formatForInput(date);
|
const minDate = formatForInput(date);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export * from './deal-ticket-container';
|
|||||||
export * from './deal-ticket-limit-amount';
|
export * from './deal-ticket-limit-amount';
|
||||||
export * from './deal-ticket-market-amount';
|
export * from './deal-ticket-market-amount';
|
||||||
export * from './deal-ticket';
|
export * from './deal-ticket';
|
||||||
|
export * from './deal-ticket-stop-order';
|
||||||
|
export * from './deal-ticket-container';
|
||||||
export * from './expiry-selector';
|
export * from './expiry-selector';
|
||||||
export * from './side-selector';
|
export * from './side-selector';
|
||||||
export * from './time-in-force-selector';
|
export * from './time-in-force-selector';
|
||||||
|
|||||||
@@ -1,46 +1,41 @@
|
|||||||
import { FormGroup } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { Toggle } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import * as RadioGroup from '@radix-ui/react-radio-group';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
interface SideSelectorProps {
|
interface SideSelectorProps {
|
||||||
value: Schema.Side;
|
value: Schema.Side;
|
||||||
onSelect: (side: Schema.Side) => void;
|
onValueChange: (side: Schema.Side) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SideSelector = ({ value, onSelect }: SideSelectorProps) => {
|
const toggles = [
|
||||||
const toggles = [
|
{ label: t('Long'), value: Schema.Side.SIDE_BUY },
|
||||||
{ label: t('Long'), value: Schema.Side.SIDE_BUY },
|
{ label: t('Short'), value: Schema.Side.SIDE_SELL },
|
||||||
{ label: t('Short'), value: Schema.Side.SIDE_SELL },
|
];
|
||||||
];
|
|
||||||
|
|
||||||
const toggleType = (e: Schema.Side) => {
|
|
||||||
switch (e) {
|
|
||||||
case Schema.Side.SIDE_BUY:
|
|
||||||
return 'buy';
|
|
||||||
case Schema.Side.SIDE_SELL:
|
|
||||||
return 'sell';
|
|
||||||
default:
|
|
||||||
return 'primary';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
export const SideSelector = (props: SideSelectorProps) => {
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<RadioGroup.Root
|
||||||
label={t('Direction')}
|
name="order-side"
|
||||||
labelFor="order-side-toggle"
|
className="mb-2 flex h-10 leading-10"
|
||||||
compact={true}
|
{...props}
|
||||||
>
|
>
|
||||||
<Toggle
|
{toggles.map(({ label, value }) => (
|
||||||
id="order-side-toggle"
|
<RadioGroup.Item value={value} key={value} id={`side-${value}`} asChild>
|
||||||
name="order-side"
|
<button
|
||||||
toggles={toggles}
|
className="flex-1 relative font-alpha text-sm"
|
||||||
checkedValue={value}
|
data-testid={`order-side-${value}`}
|
||||||
type={toggleType(value)}
|
>
|
||||||
onChange={(e) => {
|
{label}
|
||||||
onSelect(e.target.value as Schema.Side);
|
<RadioGroup.Indicator
|
||||||
}}
|
className={classNames('absolute bottom-0 left-0 right-0 h-0.5', {
|
||||||
/>
|
'bg-market-red': props.value === Schema.Side.SIDE_SELL,
|
||||||
</FormGroup>
|
'bg-market-green-550': props.value === Schema.Side.SIDE_BUY,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
))}
|
||||||
|
</RadioGroup.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user