Compare commits
87
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa7c510e44 | ||
|
|
48b1ed8ca6 | ||
|
|
d671e51edf | ||
|
|
4f3295d3fc | ||
|
|
7be7bab1ca | ||
|
|
61380f06b9 | ||
|
|
c113856d63 | ||
|
|
90884beee8 | ||
|
|
d9d2e428f4 | ||
|
|
ba8d54b761 | ||
|
|
dce4edb943 | ||
|
|
b468a0fd84 | ||
|
|
c98fbe7453 | ||
|
|
d20928ec06 | ||
|
|
289559fe4f | ||
|
|
071fd6128c | ||
|
|
554d07a31b | ||
|
|
48b62380b0 | ||
|
|
a9f4604bad | ||
|
|
54f2519a62 | ||
|
|
260e7bf9cf | ||
|
|
83db796cdb | ||
|
|
2f6800212e | ||
|
|
77e16ad1b7 | ||
|
|
dcce983842 | ||
|
|
53ce9bfaa2 | ||
|
|
6ae34d296f | ||
|
|
ee2aafb99c | ||
|
|
e8fdb63323 | ||
|
|
3df9270b57 | ||
|
|
ab26667265 | ||
|
|
9fc6b255c8 | ||
|
|
91f61d8bb5 | ||
|
|
bbfda65bcc | ||
|
|
45f2e926c3 | ||
|
|
ccadbc2cb6 | ||
|
|
7128acb7b8 | ||
|
|
357bdaa4f3 | ||
|
|
f3fe43724e | ||
|
|
03f5ca3096 | ||
|
|
03d19349af | ||
|
|
1f53b2cbb5 | ||
|
|
b010c98346 | ||
|
|
29d57af2ba | ||
|
|
fd7940c4cf | ||
|
|
8ab7ac8414 | ||
|
|
ce7cd03c3e | ||
|
|
dfa3355a68 | ||
|
|
8553904d81 | ||
|
|
10afa3c1d4 | ||
|
|
ecd362615e | ||
|
|
0d9bd67465 | ||
|
|
b5aa92f007 | ||
|
|
2955790be1 | ||
|
|
89ff9309e9 | ||
|
|
e1df3489f6 | ||
|
|
f0fa9ca5e1 | ||
|
|
9d92477c2a | ||
|
|
b1a198be85 | ||
|
|
5fb385d43c | ||
|
|
8014b279fe | ||
|
|
f4bde57294 | ||
|
|
2e5ff07824 | ||
|
|
7accb42793 | ||
|
|
9229e7f686 | ||
|
|
b570b9c4c1 | ||
|
|
ed78261aad | ||
|
|
363ca9c6e1 | ||
|
|
bafd3c384c | ||
|
|
3b830abe07 | ||
|
|
1787e22b69 | ||
|
|
f105133959 | ||
|
|
aff0e46d23 | ||
|
|
7e4aafcb77 | ||
|
|
643d5408cd | ||
|
|
2a2af3ba9c | ||
|
|
8d4e4a1228 | ||
|
|
84068c8081 | ||
|
|
e1185b9a96 | ||
|
|
f390448c24 | ||
|
|
70943c523c | ||
|
|
dcb79e70d3 | ||
|
|
cc72cdbe16 | ||
|
|
3fd1817e1e | ||
|
|
b483c13f81 | ||
|
|
bde7a9fbf9 | ||
|
|
5fd93ee9c8 |
@@ -4,14 +4,54 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- release/*
|
||||
- develop
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
jobs:
|
||||
node-modules:
|
||||
runs-on: ubuntu-22.04
|
||||
name: 'Cache yarn modules'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Cache node modules
|
||||
id: cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
|
||||
# comment out "resotre-keys" if you need to rebuild yarn from 0
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cache-node-modules-
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
|
||||
cache: yarn
|
||||
|
||||
- name: yarn install
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --pure-lockfile
|
||||
|
||||
lint-pr-title:
|
||||
needs: node-modules
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
name: Verify PR title
|
||||
uses: ./.github/workflows/lint-pr.yml
|
||||
secrets: inherit
|
||||
|
||||
lint-test-build:
|
||||
timeout-minutes: 20
|
||||
needs: node-modules
|
||||
runs-on: ubuntu-22.04
|
||||
name: '(CI) lint + unit test + build'
|
||||
steps:
|
||||
@@ -27,8 +67,11 @@ jobs:
|
||||
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
|
||||
cache: yarn
|
||||
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: Derive appropriate SHAs for base and head for `nx affected` commands
|
||||
uses: nrwl/nx-set-shas@v3
|
||||
@@ -48,7 +91,7 @@ jobs:
|
||||
run: yarn nx affected:test
|
||||
|
||||
- name: Build affected
|
||||
run: yarn nx affected:build
|
||||
run: yarn nx affected:build || (yarn install && yarn nx affected:build)
|
||||
|
||||
# See affected apps
|
||||
- name: See affected apps
|
||||
@@ -95,6 +138,43 @@ jobs:
|
||||
with:
|
||||
projects: ${{ needs.lint-test-build.outputs.projects }}
|
||||
|
||||
dist-check:
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish-dist
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
name: '(CD) comment preview links'
|
||||
steps:
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v2
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-includes: Previews
|
||||
|
||||
- name: Inject slug/short variables
|
||||
if: ${{ steps.fc.outputs.comment-id == 0 }}
|
||||
uses: rlespinasse/github-slug-action@v4
|
||||
with:
|
||||
prefix: CI_
|
||||
|
||||
- name: Create comment
|
||||
if: ${{ steps.fc.outputs.comment-id == 0 }}
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
Previews
|
||||
- explorer https://explorer.${{ env.CI_GITHUB_REF_NAME }}.vega.rocks
|
||||
- trading https://trading.${{ env.CI_GITHUB_REF_NAME }}.vega.rocks
|
||||
- governance https://governance.${{ env.CI_GITHUB_REF_NAME }}.vega.rocks
|
||||
|
||||
cypress-check:
|
||||
name: '(CI) cypress - check'
|
||||
runs-on: ubuntu-latest
|
||||
needs: cypress
|
||||
steps:
|
||||
- run: echo Done!
|
||||
|
||||
# Report single result at the end, to avoid mess with required checks in PR
|
||||
cypress-result:
|
||||
if: ${{ always() }}
|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
name: Verify PR title
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- ready_for_review
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint_pr:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -23,8 +19,11 @@ jobs:
|
||||
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
|
||||
cache: yarn
|
||||
|
||||
- name: Install root dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: Check PR title
|
||||
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
|
||||
@@ -15,6 +15,7 @@ jobs:
|
||||
app: ${{ fromJSON(inputs.projects) }}
|
||||
name: ${{ matrix.app }}
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
@@ -36,66 +37,131 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||
- name: Check node version
|
||||
id: tags
|
||||
run: |
|
||||
nodeVersion=$(cat .nvmrc | head -n 1)
|
||||
echo ::set-output name=nodeVersion::${nodeVersion}
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
|
||||
cache: yarn
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||
- name: Define variables
|
||||
run: |
|
||||
envName=''
|
||||
dockerfile="dist.Dockerfile"
|
||||
if [[ "${{ github.event_name }}" = "push" ]]; then
|
||||
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
|
||||
bucketName="${{ github.event.repository.name }}-$envName"
|
||||
echo ::set-output name=bucketName::${bucketName}
|
||||
echo ::set-output name=envName::${envName}
|
||||
domain="vega.rocks"
|
||||
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
|
||||
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
|
||||
if [[ "${{ github.ref }}" =~ .*mainnet.* ]]; then
|
||||
domain="vega.community"
|
||||
if [[ "${{ matrix.app }}" = "trading" ]]; then
|
||||
dockerfile="ipfs.Dockerfile"
|
||||
fi
|
||||
fi
|
||||
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
|
||||
envName="stagnet3"
|
||||
fi
|
||||
bucketName="${{ matrix.app }}.${envName}.${domain}"
|
||||
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
|
||||
fi
|
||||
nodeVersion=$(cat .nvmrc | head -n 1)
|
||||
echo ENV_NAME=${envName} >> $GITHUB_ENV
|
||||
echo NODE_VERSION=${nodeVersion} >> $GITHUB_ENV
|
||||
echo DOCKERFILE=docker/${dockerfile} >> $GITHUB_ENV
|
||||
|
||||
- name: Build local dist
|
||||
if: ${{ env.DOCKERFILE != 'docker/ipfs.Dockerfile' }}
|
||||
run: |
|
||||
flags=""
|
||||
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
|
||||
if [[ "${{ env.ENV_NAME }}" != "ops-vega" ]]; then
|
||||
flags="--env=${{ env.ENV_NAME }}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${{ matrix.app }}" = "trading" ]; then
|
||||
yarn nx export trading $flags || (yarn install && yarn nx export trading $flags)
|
||||
DIST_LOCATION=dist/apps/trading/exported
|
||||
else
|
||||
yarn nx build ${{ matrix.app }} $flags || (yarn install && yarn nx build ${{ matrix.app }} $flags)
|
||||
DIST_LOCATION=dist/apps/${{ matrix.app }}
|
||||
fi
|
||||
mv $DIST_LOCATION dist-result
|
||||
tree dist-result
|
||||
|
||||
- name: Build and export to local Docker
|
||||
id: docker_build
|
||||
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: ${{ env.DOCKERFILE }}
|
||||
load: true
|
||||
build-args: |
|
||||
APP=${{ matrix.app }}
|
||||
NODE_VERSION=${{ steps.tags.outputs.nodeVersion }}
|
||||
ENV_NAME=${{ steps.tags.outputs.envName || '' }}
|
||||
NODE_VERSION=${{ env.NODE_VERSION }}
|
||||
ENV_NAME=${{ env.ENV_NAME }}
|
||||
tags: |
|
||||
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
|
||||
|
||||
- name: Image digest
|
||||
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
- name: Sanity check docker image
|
||||
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
|
||||
run: |
|
||||
echo "Check ipfs-hash"
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat ipfs-hash
|
||||
if [[ "${{ env.DOCKERFILE }}" = "docker/ipfs.Dockerfile" ]]; then
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
|
||||
fi
|
||||
|
||||
echo "List html directory"
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local ls -lah
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree .'
|
||||
|
||||
echo "Copy dist to local filesystem"
|
||||
- name: Copy dist to local filesystem
|
||||
if: ${{ env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' }}
|
||||
run: |
|
||||
docker create --name=dist ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
|
||||
docker cp dist:/usr/share/nginx/html dist
|
||||
|
||||
echo "Check local dist"
|
||||
ls -al dist
|
||||
echo "check local dist files"
|
||||
tree dist/html
|
||||
mv dist/html dist-result
|
||||
|
||||
- name: Publish dist as docker image
|
||||
uses: docker/build-push-action@v3
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
with:
|
||||
context: .
|
||||
file: ${{ env.DOCKERFILE }}
|
||||
push: true
|
||||
build-args: |
|
||||
APP=${{ matrix.app }}
|
||||
NODE_VERSION=${{ steps.tags.outputs.nodeVersion }}
|
||||
NODE_VERSION=${{ env.NODE_VERSION }}
|
||||
ENV_NAME=${{ env.ENV_NAME }}
|
||||
tags: |
|
||||
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
# - uses: shallwefootball/s3-upload-action@master
|
||||
# if: ${{ github.event_name == 'push' }}
|
||||
# name: Upload dist S3
|
||||
# with:
|
||||
# aws_key_id: ${{ secrets.AWS_KEY_ID }}
|
||||
# aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY}}
|
||||
# aws_bucket: ${{ steps.tags.outputs.bucketName }}
|
||||
# source_dir: 'dist'
|
||||
# bucket creation in github.com/vegaprotocol/terraform//frontend
|
||||
- name: Publish dist to s3
|
||||
uses: jakejarvis/s3-sync-action@master
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
with:
|
||||
args: --acl private --follow-symlinks --delete
|
||||
env:
|
||||
AWS_S3_BUCKET: ${{ env.BUCKET_NAME }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: 'eu-west-1'
|
||||
SOURCE_DIR: 'dist-result'
|
||||
|
||||
- name: Add preview label
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
@@ -103,6 +169,3 @@ jobs:
|
||||
with:
|
||||
labels: ${{ matrix.app }}-preview
|
||||
number: ${{ github.event.number }}
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
@@ -10,7 +10,7 @@ export const Footer = () => {
|
||||
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const showFullFeedbackLabel = useMemo(
|
||||
() => ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
|
||||
() => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
|
||||
[screenSize]
|
||||
);
|
||||
|
||||
|
||||
@@ -44,7 +44,8 @@ describe(
|
||||
function () {
|
||||
before('connect wallets and set approval limit', function () {
|
||||
cy.visit('/');
|
||||
vegaWalletSetSpecifiedApprovalAmount('1000');
|
||||
ethereumWalletConnect();
|
||||
cy.associateTokensToVegaWallet('1');
|
||||
});
|
||||
|
||||
beforeEach('visit proposals tab', function () {
|
||||
@@ -213,6 +214,7 @@ describe(
|
||||
|
||||
// 3001-VOTE-042, 3001-VOTE-057, 3001-VOTE-058, 3001-VOTE-059, 3001-VOTE-060
|
||||
it('Newly created proposal details - ability to increase associated tokens - by voting again after association', function () {
|
||||
vegaWalletSetSpecifiedApprovalAmount('1000');
|
||||
createRawProposal();
|
||||
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
|
||||
getSubmittedProposalFromProposalList(rawProposal.rationale.title)
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
createUpdateNetworkProposalTxBody,
|
||||
createFreeFormProposalTxBody,
|
||||
} from '../../support/proposal.functions';
|
||||
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
|
||||
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
|
||||
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
|
||||
|
||||
@@ -33,10 +32,6 @@ context(
|
||||
before('Connect wallets and set approval', function () {
|
||||
cy.visit('/');
|
||||
vegaWalletSetSpecifiedApprovalAmount('1000');
|
||||
cy.connectVegaWallet();
|
||||
ethereumWalletConnect();
|
||||
ensureSpecifiedUnstakedTokensAreAssociated('1');
|
||||
cy.clearLocalStorage();
|
||||
});
|
||||
|
||||
beforeEach('visit proposals', function () {
|
||||
@@ -114,7 +109,7 @@ context(
|
||||
navigateTo(navigation.proposals);
|
||||
cy.reload();
|
||||
waitForSpinner();
|
||||
cy.get(openProposals).within(() => {
|
||||
cy.get(openProposals, { timeout: 6000 }).within(() => {
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil('[data-testid="proposals-list-item"]')
|
||||
.within(() => cy.get(viewProposalButton).click());
|
||||
|
||||
@@ -232,7 +232,7 @@ context(
|
||||
|
||||
it('Unable to create a freeform proposal - when json parent section contains unexpected field', function () {
|
||||
const errorMsg =
|
||||
'Invalid params: the transaction does not use a valid Vega command: unknown field unexpected" in vega.commands.v1.ProposalSubmission';
|
||||
'Invalid params: the transaction does not use a valid Vega command: unknown field "unexpected" in vega.commands.v1.ProposalSubmission';
|
||||
|
||||
// 3001-VOTE-038 3002-PROP-013 3002-PROP-014
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
@@ -313,7 +313,7 @@ context(
|
||||
|
||||
it('Unable to vote on a proposal - when vega wallet disconnected - option to connect from within', function () {
|
||||
createRawProposal();
|
||||
cy.get('[data-testid="manage-vega-wallet"]').click();
|
||||
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
|
||||
cy.get('[data-testid="disconnect"]').click();
|
||||
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
|
||||
getSubmittedProposalFromProposalList(
|
||||
|
||||
@@ -218,7 +218,7 @@ context(
|
||||
|
||||
it('Unable to submit new market proposal with missing/invalid fields', function () {
|
||||
const errorMsg =
|
||||
'Invalid params: the transaction is not a valid Vega command: unknown field "filters" in vega.DataSourceDefinition';
|
||||
'Invalid params: the transaction does not use a valid Vega command: unknown field "invalid" in vega.NewMarket';
|
||||
|
||||
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
|
||||
cy.get(newProposalSubmitButton).should('be.visible').click();
|
||||
@@ -436,7 +436,7 @@ context(
|
||||
});
|
||||
});
|
||||
|
||||
it.only('Able to submit update asset proposal using max deadline', function () {
|
||||
it('Able to submit update asset proposal using max deadline', function () {
|
||||
goToMakeNewProposal(governanceProposalType.UPDATE_ASSET);
|
||||
enterUpdateAssetProposalDetails();
|
||||
cy.get(maxVoteDeadline).click();
|
||||
|
||||
@@ -25,11 +25,11 @@ const vegaWalletUnstakedBalance =
|
||||
'[data-testid="vega-wallet-balance-unstaked"]';
|
||||
const txTimeout = Cypress.env('txTimeout');
|
||||
const vegaWalletPublicKeyShort = Cypress.env('vegaWalletPublicKeyShort');
|
||||
const ethWalletAssociateButton = '[data-testid="associate-btn"]';
|
||||
const ethWalletAssociateButton = '[data-testid="associate-btn"]:visible';
|
||||
const associateWalletRadioButton = '[data-testid="associate-radio-wallet"]';
|
||||
const tokenAmountInputBox = '[data-testid="token-amount-input"]';
|
||||
const tokenSubmitButton = '[data-testid="token-input-submit-button"]';
|
||||
const ethWalletDissociateButton = '[href="/token/disassociate"]';
|
||||
const ethWalletDissociateButton = '[href="/token/disassociate"]:visible';
|
||||
const vestingContractSection = '[data-testid="vega-in-vesting-contract"]';
|
||||
const vegaInWalletSection = '[data-testid="vega-in-wallet"]';
|
||||
const connectedVegaKey = '[data-testid="connected-vega-key"]';
|
||||
@@ -78,12 +78,12 @@ context(
|
||||
|
||||
cy.getByTestId('currency-title', txTimeout).should(
|
||||
'have.length.above',
|
||||
3
|
||||
6
|
||||
);
|
||||
validateWalletCurrency('Associated', '0.00');
|
||||
validateWalletCurrency('Pending association', '2.00');
|
||||
validateWalletCurrency('Total associated after pending', '2.00');
|
||||
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
|
||||
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
|
||||
|
||||
// 0005-ETXN-002
|
||||
verifyEthWalletAssociatedBalance('2.0');
|
||||
@@ -111,12 +111,12 @@ context(
|
||||
stakingPageDisassociateTokens('2');
|
||||
cy.getByTestId('currency-title', txTimeout).should(
|
||||
'have.length.above',
|
||||
3
|
||||
6
|
||||
);
|
||||
validateWalletCurrency('Associated', '2.00');
|
||||
validateWalletCurrency('Pending association', '2.00');
|
||||
validateWalletCurrency('Total associated after pending', '0.00');
|
||||
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
|
||||
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
|
||||
cy.getByTestId('eth-wallet-associated-balances', txTimeout).should(
|
||||
'not.exist'
|
||||
);
|
||||
@@ -192,12 +192,12 @@ context(
|
||||
|
||||
cy.getByTestId('currency-title', txTimeout).should(
|
||||
'have.length.above',
|
||||
3
|
||||
6
|
||||
);
|
||||
validateWalletCurrency('Associated', '0.00');
|
||||
validateWalletCurrency('Pending association', '2.00');
|
||||
validateWalletCurrency('Total associated after pending', '2.00');
|
||||
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
|
||||
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
|
||||
verifyEthWalletAssociatedBalance('2.0');
|
||||
verifyEthWalletTotalAssociatedBalance('2.0');
|
||||
cy.get(vegaWallet).within(() => {
|
||||
@@ -210,12 +210,12 @@ context(
|
||||
});
|
||||
cy.getByTestId('currency-title', txTimeout).should(
|
||||
'have.length.above',
|
||||
3
|
||||
6
|
||||
);
|
||||
validateWalletCurrency('Associated', '2.00');
|
||||
validateWalletCurrency('Pending association', '1.00');
|
||||
validateWalletCurrency('Total associated after pending', '1.00');
|
||||
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
|
||||
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
|
||||
verifyEthWalletAssociatedBalance('1.0');
|
||||
verifyEthWalletTotalAssociatedBalance('1.0');
|
||||
});
|
||||
@@ -266,7 +266,7 @@ context(
|
||||
// 1004-ASSO-008
|
||||
// 1004-ASSO-010
|
||||
// No warning visible as described in AC, but the button is disabled
|
||||
cy.get(ethWalletAssociateButton).first().click();
|
||||
cy.get(ethWalletAssociateButton).click();
|
||||
cy.get(associateWalletRadioButton, { timeout: 30000 }).click();
|
||||
cy.get(tokenSubmitButton, txTimeout).should('be.disabled'); // button disabled with no input
|
||||
cy.get(tokenAmountInputBox, { timeout: 10000 }).type('6500000');
|
||||
@@ -278,12 +278,12 @@ context(
|
||||
vegaWalletAssociate('2');
|
||||
cy.getByTestId('currency-title', txTimeout).should(
|
||||
'have.length.above',
|
||||
3
|
||||
6
|
||||
);
|
||||
validateWalletCurrency('Associated', '0.00');
|
||||
validateWalletCurrency('Pending association', '2.00');
|
||||
validateWalletCurrency('Total associated after pending', '2.00');
|
||||
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
|
||||
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
|
||||
validateWalletCurrency('Associated', '2.00');
|
||||
});
|
||||
|
||||
@@ -294,24 +294,24 @@ context(
|
||||
});
|
||||
cy.getByTestId('currency-title', txTimeout).should(
|
||||
'have.length.above',
|
||||
3
|
||||
6
|
||||
);
|
||||
validateWalletCurrency('Associated', '2.00');
|
||||
validateWalletCurrency('Pending association', '2.00');
|
||||
validateWalletCurrency('Total associated after pending', '0.00');
|
||||
cy.getByTestId('currency-title', txTimeout).should('have.length', 3);
|
||||
cy.getByTestId('currency-title', txTimeout).should('have.length', 6);
|
||||
validateWalletCurrency('Associated', '0.00');
|
||||
});
|
||||
|
||||
it('Able to associate tokens to different public key of connected vega wallet', function () {
|
||||
cy.get(ethWalletAssociateButton).first().click();
|
||||
cy.get(ethWalletAssociateButton).click();
|
||||
cy.get(associateWalletRadioButton).click();
|
||||
cy.get(connectedVegaKey).should(
|
||||
'have.text',
|
||||
Cypress.env('vegaWalletPublicKey')
|
||||
);
|
||||
|
||||
cy.get('[data-testid="manage-vega-wallet"]').click();
|
||||
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
|
||||
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
|
||||
cy.get(connectedVegaKey).should(
|
||||
'have.text',
|
||||
|
||||
@@ -166,6 +166,7 @@ export function goToMakeNewProposal(proposalType: string) {
|
||||
navigateTo(navigation.proposals);
|
||||
cy.get(newProposalButton).should('be.visible').click();
|
||||
cy.url().should('include', '/proposals/propose');
|
||||
cy.get(navigation.pageSpinner, { timeout: 20000 }).should('not.exist');
|
||||
cy.get('li').should('contain.text', proposalType).and('be.visible');
|
||||
cy.get('li').contains(proposalType).click();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@vegaprotocol/smart-contracts';
|
||||
import { ethers, Wallet } from 'ethers';
|
||||
|
||||
const associatedAmountInWallet = '[data-testid="associated-amount"]:visible';
|
||||
const vegaWalletContainer = 'aside [data-testid="vega-wallet"]';
|
||||
const vegaWalletMnemonic = Cypress.env('vegaWalletMnemonic');
|
||||
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey');
|
||||
@@ -59,7 +60,7 @@ export async function faucetAsset(assetEthAddress: string) {
|
||||
}
|
||||
|
||||
export async function vegaWalletTeardown() {
|
||||
cy.get('[data-testid="associated-amount"]')
|
||||
cy.get(associatedAmountInWallet)
|
||||
.should('be.visible')
|
||||
.invoke('text')
|
||||
.then((associatedAmount) => {
|
||||
@@ -68,12 +69,12 @@ export async function vegaWalletTeardown() {
|
||||
$body.find('[data-testid="eth-wallet-associated-balances"]').length ||
|
||||
associatedAmount != '0.00'
|
||||
) {
|
||||
vegaWalletTeardownVesting(vestingContract);
|
||||
vegaWalletTeardownStaking(stakingBridgeContract);
|
||||
vegaWalletTeardownVesting(vestingContract);
|
||||
}
|
||||
});
|
||||
cy.get(vegaWalletContainer).within(() => {
|
||||
cy.getByTestId('associated-amount', {
|
||||
cy.get(associatedAmountInWallet, {
|
||||
timeout: transactionTimeout,
|
||||
}).contains('0.00', {
|
||||
timeout: transactionTimeout,
|
||||
@@ -90,7 +91,7 @@ export async function vegaWalletSetSpecifiedApprovalAmount(
|
||||
await promiseWithTimeout(
|
||||
token.approve(
|
||||
ethStakingBridgeContractAddress,
|
||||
resetAmount.concat('000000000000000000')
|
||||
resetAmount + '0'.repeat(18)
|
||||
),
|
||||
10 * 60 * 1000,
|
||||
'set approval amount'
|
||||
@@ -104,12 +105,23 @@ async function vegaWalletTeardownStaking(stakingBridgeContract: StakingBridge) {
|
||||
{ timeout: transactionTimeout, log: false }
|
||||
).then((stakeBalance) => {
|
||||
if (Number(stakeBalance) != 0) {
|
||||
cy.wrap(
|
||||
stakingBridgeContract.remove_stake(
|
||||
String(stakeBalance),
|
||||
vegaWalletPubKey
|
||||
),
|
||||
{ timeout: transactionTimeout, log: false }
|
||||
cy.get('[data-testid="vega-wallet-balance-unstaked"]:visible').within(
|
||||
() => {
|
||||
cy.get(associatedAmountInWallet)
|
||||
.invoke('text')
|
||||
.then(($walletAmount) => {
|
||||
cy.wrap(
|
||||
stakingBridgeContract.remove_stake(
|
||||
String(stakeBalance),
|
||||
vegaWalletPubKey
|
||||
),
|
||||
{ timeout: transactionTimeout, log: false }
|
||||
);
|
||||
cy.get(associatedAmountInWallet, {
|
||||
timeout: transactionTimeout,
|
||||
}).should('not.have.text', $walletAmount);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -124,7 +136,6 @@ async function vegaWalletTeardownVesting(vestingContract: TokenVesting) {
|
||||
if (Number(vestingAmount) != 0) {
|
||||
// Wait needed to allow time for ganache to process tx for stakingBridgeContract.remove_stake
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(1000);
|
||||
cy.wrap(
|
||||
vestingContract.remove_stake(String(vestingAmount), vegaWalletPubKey),
|
||||
{ timeout: transactionTimeout, log: false }
|
||||
|
||||
@@ -443,8 +443,8 @@
|
||||
"rewardType": "Reward type",
|
||||
"rewardsAndFeesReceived": "Rewards and fees received",
|
||||
"ThisDoesNotIncludeFeesReceivedForMakersOrLiquidityProviders": "This does not include fees received for makers or liquidity providers",
|
||||
"totalDistributed": "TOTAL DISTRIBUTED",
|
||||
"earnedByMe": "EARNED BY ME",
|
||||
"totalDistributed": "Total distributed",
|
||||
"earnedByMe": "Earned by me",
|
||||
"noRewardsHaveBeenDistributedYet": "NO REWARDS HAVE BEEN DISTRIBUTED YET",
|
||||
"rewardsColAssetHeader": "ASSET",
|
||||
"rewardsColStakingHeader": "STAKING",
|
||||
|
||||
@@ -114,7 +114,7 @@ export const RewardsPage = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-[440px]">
|
||||
<div className="w-[360px]">
|
||||
<Toggle
|
||||
name="epoch-reward-view-toggle"
|
||||
toggles={[
|
||||
|
||||
@@ -7,9 +7,10 @@ query PreviousEpoch($epochId: ID) {
|
||||
id
|
||||
rewardScore {
|
||||
rawValidatorScore
|
||||
performanceScore
|
||||
}
|
||||
rankingScore {
|
||||
performanceScore
|
||||
stakeScore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export type PreviousEpochQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string } | null, rankingScore: { __typename?: 'RankingScore', performanceScore: string } } } | null> | null } | null } };
|
||||
export type PreviousEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, validatorsConnection?: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, rewardScore?: { __typename?: 'RewardScore', rawValidatorScore: string, performanceScore: string } | null, rankingScore: { __typename?: 'RankingScore', stakeScore: string } } } | null> | null } | null } };
|
||||
|
||||
|
||||
export const PreviousEpochDocument = gql`
|
||||
@@ -21,9 +21,10 @@ export const PreviousEpochDocument = gql`
|
||||
id
|
||||
rewardScore {
|
||||
rawValidatorScore
|
||||
performanceScore
|
||||
}
|
||||
rankingScore {
|
||||
performanceScore
|
||||
stakeScore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -81,9 +81,10 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
|
||||
id: 'ccc022b7e63a4d0a6d3a193c3940c88574060e58a184964c994998d86835a1b4',
|
||||
rewardScore: {
|
||||
rawValidatorScore: '0.25',
|
||||
performanceScore: '0.9998677767864936',
|
||||
},
|
||||
rankingScore: {
|
||||
performanceScore: '0.9998677767864936',
|
||||
stakeScore: '0.2499583402766206',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -92,9 +93,10 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
|
||||
id: '966438c6bffac737cfb08173ffcb3f393c4692b099ad80cb45a82e2dc0a8cf99',
|
||||
rewardScore: {
|
||||
rawValidatorScore: '0.3',
|
||||
performanceScore: '1',
|
||||
},
|
||||
rankingScore: {
|
||||
performanceScore: '1',
|
||||
stakeScore: '0.25',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -103,9 +105,10 @@ const MOCK_PREVIOUS_EPOCH: PreviousEpochQuery = {
|
||||
id: '12c81b738e8051152e1afe44376ec37bca9216466e6d44cdd772194bad0ada81',
|
||||
rewardScore: {
|
||||
rawValidatorScore: '0.35',
|
||||
performanceScore: '0.999629748500531',
|
||||
},
|
||||
rankingScore: {
|
||||
performanceScore: '0.999629748500531',
|
||||
stakeScore: '0.2312',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+16
-20
@@ -10,7 +10,6 @@ import {
|
||||
getFormattedPerformanceScore,
|
||||
getLastEpochScoreAndPerformance,
|
||||
getNormalisedVotingPower,
|
||||
getOverstakedAmount,
|
||||
getOverstakingPenalty,
|
||||
getPerformancePenalty,
|
||||
getTotalPenalties,
|
||||
@@ -52,7 +51,6 @@ interface CanonisedConsensusNodeProps {
|
||||
[ValidatorFields.STAKED_BY_OPERATOR]: string;
|
||||
[ValidatorFields.PERFORMANCE_SCORE]: string;
|
||||
[ValidatorFields.PERFORMANCE_PENALTY]: string;
|
||||
[ValidatorFields.OVERSTAKED_AMOUNT]: string;
|
||||
[ValidatorFields.OVERSTAKING_PENALTY]: string;
|
||||
[ValidatorFields.TOTAL_PENALTIES]: string;
|
||||
[ValidatorFields.PENDING_STAKE]: string;
|
||||
@@ -164,14 +162,11 @@ export const ConsensusValidatorsTable = ({
|
||||
pendingUserStake,
|
||||
userStakeShare,
|
||||
}) => {
|
||||
const { rawValidatorScore, performanceScore } =
|
||||
getLastEpochScoreAndPerformance(previousEpochData, id);
|
||||
|
||||
const overstakedAmount = getOverstakedAmount(
|
||||
rawValidatorScore,
|
||||
stakedTotal,
|
||||
totalStake
|
||||
);
|
||||
const {
|
||||
rawValidatorScore: previousEpochValidatorScore,
|
||||
performanceScore: previousEpochPerformanceScore,
|
||||
stakeScore: previousEpochStakeScore,
|
||||
} = getLastEpochScoreAndPerformance(previousEpochData, id);
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -184,7 +179,7 @@ export const ConsensusValidatorsTable = ({
|
||||
[ValidatorFields.NORMALISED_VOTING_POWER]:
|
||||
getNormalisedVotingPower(votingPower),
|
||||
[ValidatorFields.UNNORMALISED_VOTING_POWER]:
|
||||
getUnnormalisedVotingPower(rawValidatorScore),
|
||||
getUnnormalisedVotingPower(previousEpochValidatorScore),
|
||||
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
|
||||
[ValidatorFields.STAKED_BY_DELEGATES]: formatNumber(
|
||||
toBigNum(stakedByDelegates, decimals),
|
||||
@@ -194,18 +189,19 @@ export const ConsensusValidatorsTable = ({
|
||||
toBigNum(stakedByOperator, decimals),
|
||||
2
|
||||
),
|
||||
[ValidatorFields.PERFORMANCE_SCORE]:
|
||||
getFormattedPerformanceScore(performanceScore).toString(),
|
||||
[ValidatorFields.PERFORMANCE_PENALTY]:
|
||||
getPerformancePenalty(performanceScore),
|
||||
[ValidatorFields.OVERSTAKED_AMOUNT]: overstakedAmount.toString(),
|
||||
[ValidatorFields.PERFORMANCE_SCORE]: getFormattedPerformanceScore(
|
||||
previousEpochPerformanceScore
|
||||
).toString(),
|
||||
[ValidatorFields.PERFORMANCE_PENALTY]: getPerformancePenalty(
|
||||
previousEpochPerformanceScore
|
||||
),
|
||||
[ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty(
|
||||
overstakedAmount,
|
||||
totalStake
|
||||
previousEpochValidatorScore,
|
||||
previousEpochStakeScore
|
||||
),
|
||||
[ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties(
|
||||
rawValidatorScore,
|
||||
performanceScore,
|
||||
previousEpochValidatorScore,
|
||||
previousEpochPerformanceScore,
|
||||
stakedTotal,
|
||||
totalStake
|
||||
),
|
||||
|
||||
+19
-19
@@ -7,7 +7,6 @@ import { BigNumber } from '../../../../lib/bignumber';
|
||||
import {
|
||||
getFormattedPerformanceScore,
|
||||
getLastEpochScoreAndPerformance,
|
||||
getOverstakedAmount,
|
||||
getOverstakingPenalty,
|
||||
getPerformancePenalty,
|
||||
getTotalPenalties,
|
||||
@@ -82,21 +81,21 @@ export const StandbyPendingValidatorsTable = ({
|
||||
pendingUserStake,
|
||||
userStakeShare,
|
||||
}) => {
|
||||
const { rawValidatorScore, performanceScore } =
|
||||
getLastEpochScoreAndPerformance(previousEpochData, id);
|
||||
const {
|
||||
rawValidatorScore: previousEpochValidatorScore,
|
||||
performanceScore: previousEpochPerformanceScore,
|
||||
stakeScore: previousEpochStakeScore,
|
||||
} = getLastEpochScoreAndPerformance(previousEpochData, id);
|
||||
|
||||
const overstakedAmount = getOverstakedAmount(
|
||||
rawValidatorScore,
|
||||
stakedTotal,
|
||||
totalStake
|
||||
);
|
||||
let individualStakeNeededForPromotion,
|
||||
individualStakeNeededForPromotionDescription;
|
||||
|
||||
if (stakeNeededForPromotion && performanceScore) {
|
||||
if (stakeNeededForPromotion && previousEpochPerformanceScore) {
|
||||
const stakedTotalBigNum = new BigNumber(stakedTotal);
|
||||
const stakeNeededBigNum = new BigNumber(stakeNeededForPromotion);
|
||||
const performanceScoreBigNum = new BigNumber(performanceScore);
|
||||
const performanceScoreBigNum = new BigNumber(
|
||||
previousEpochPerformanceScore
|
||||
);
|
||||
|
||||
const calc = stakeNeededBigNum
|
||||
.dividedBy(performanceScoreBigNum)
|
||||
@@ -142,18 +141,19 @@ export const StandbyPendingValidatorsTable = ({
|
||||
toBigNum(stakedByOperator, decimals),
|
||||
2
|
||||
),
|
||||
[ValidatorFields.PERFORMANCE_SCORE]:
|
||||
getFormattedPerformanceScore(performanceScore).toString(),
|
||||
[ValidatorFields.PERFORMANCE_PENALTY]:
|
||||
getPerformancePenalty(performanceScore),
|
||||
[ValidatorFields.OVERSTAKED_AMOUNT]: overstakedAmount.toString(),
|
||||
[ValidatorFields.PERFORMANCE_SCORE]: getFormattedPerformanceScore(
|
||||
previousEpochPerformanceScore
|
||||
).toString(),
|
||||
[ValidatorFields.PERFORMANCE_PENALTY]: getPerformancePenalty(
|
||||
previousEpochPerformanceScore
|
||||
),
|
||||
[ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty(
|
||||
overstakedAmount,
|
||||
totalStake
|
||||
previousEpochValidatorScore,
|
||||
previousEpochStakeScore
|
||||
),
|
||||
[ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties(
|
||||
rawValidatorScore,
|
||||
performanceScore,
|
||||
previousEpochValidatorScore,
|
||||
previousEpochPerformanceScore,
|
||||
stakedTotal,
|
||||
totalStake
|
||||
),
|
||||
|
||||
@@ -155,16 +155,16 @@ export const ValidatorTables = ({
|
||||
return (
|
||||
<section data-testid="validator-tables">
|
||||
<div className="grid w-full justify-end">
|
||||
<div className="w-[400px]">
|
||||
<div className="w-[340px]">
|
||||
<Toggle
|
||||
name="validators-view-toggle"
|
||||
toggles={[
|
||||
{
|
||||
label: t('ALL VALIDATORS'),
|
||||
label: t('All validators'),
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
label: t('STAKED BY ME'),
|
||||
label: t('Staked by me'),
|
||||
value: 'myStake',
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { SubHeading } from '../../../components/heading';
|
||||
import {
|
||||
getLastEpochScoreAndPerformance,
|
||||
getNormalisedVotingPower,
|
||||
getOverstakedAmount,
|
||||
getOverstakingPenalty,
|
||||
getPerformancePenalty,
|
||||
getTotalPenalties,
|
||||
@@ -75,15 +74,9 @@ export const ValidatorTable = ({
|
||||
|
||||
const stakedOnNode = toBigNum(node.stakedTotal, decimals);
|
||||
|
||||
const { rawValidatorScore, performanceScore } =
|
||||
const { rawValidatorScore, performanceScore, stakeScore } =
|
||||
getLastEpochScoreAndPerformance(previousEpochData, node.id);
|
||||
|
||||
const overstakedAmount = getOverstakedAmount(
|
||||
rawValidatorScore,
|
||||
stakedTotal,
|
||||
node.stakedTotal
|
||||
);
|
||||
|
||||
const stakePercentage = getStakePercentage(total, stakedOnNode);
|
||||
|
||||
const totalPenaltiesAmount = getTotalPenalties(
|
||||
@@ -245,7 +238,7 @@ export const ValidatorTable = ({
|
||||
|
||||
<Tooltip description={t('OverstakedPenaltyDescription')}>
|
||||
<span data-testid="overstaking-penalty">
|
||||
{getOverstakingPenalty(overstakedAmount, node.stakedTotal)}
|
||||
{getOverstakingPenalty(rawValidatorScore, stakeScore)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</KeyValueTableRow>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
getNormalisedVotingPower,
|
||||
getUnnormalisedVotingPower,
|
||||
getOverstakingPenalty,
|
||||
getOverstakedAmount,
|
||||
getFormattedPerformanceScore,
|
||||
getPerformancePenalty,
|
||||
getTotalPenalties,
|
||||
@@ -22,9 +21,10 @@ describe('getLastEpochScoreAndPerformance', () => {
|
||||
id: '0x123',
|
||||
rewardScore: {
|
||||
rawValidatorScore: '0.25',
|
||||
performanceScore: '0.75',
|
||||
},
|
||||
rankingScore: {
|
||||
performanceScore: '0.75',
|
||||
stakeScore: '0.25',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -33,9 +33,10 @@ describe('getLastEpochScoreAndPerformance', () => {
|
||||
id: '0x234',
|
||||
rewardScore: {
|
||||
rawValidatorScore: '0.35',
|
||||
performanceScore: '0.85',
|
||||
},
|
||||
rankingScore: {
|
||||
performanceScore: '0.85',
|
||||
stakeScore: '0.25',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -50,12 +51,14 @@ describe('getLastEpochScoreAndPerformance', () => {
|
||||
).toEqual({
|
||||
rawValidatorScore: '0.25',
|
||||
performanceScore: '0.75',
|
||||
stakeScore: '0.25',
|
||||
});
|
||||
expect(
|
||||
getLastEpochScoreAndPerformance(mockPreviousEpochData, '0x234')
|
||||
).toEqual({
|
||||
rawValidatorScore: '0.35',
|
||||
performanceScore: '0.85',
|
||||
stakeScore: '0.25',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -79,40 +82,34 @@ describe('getUnnormalisedVotingPower', () => {
|
||||
});
|
||||
|
||||
describe('getOverstakingPenalty', () => {
|
||||
it('should return the overstaking penalty', () => {
|
||||
expect(
|
||||
getOverstakingPenalty(new BigNumber(100), Number(1000).toString())
|
||||
).toEqual('10.00%');
|
||||
expect(
|
||||
getOverstakingPenalty(new BigNumber(500), Number(2000).toString())
|
||||
).toEqual('25.00%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOverstakedAmount', () => {
|
||||
it('should return the overstaked amount', () => {
|
||||
expect(
|
||||
// If a validator score is 0, any amount staked on the node is considered overstaked
|
||||
getOverstakedAmount('0', Number(100).toString(), Number(20).toString())
|
||||
).toEqual(new BigNumber(20));
|
||||
expect(
|
||||
getOverstakedAmount('0.05', Number(100).toString(), Number(20).toString())
|
||||
).toEqual(new BigNumber(15));
|
||||
expect(
|
||||
getOverstakedAmount('0.1', Number(100).toString(), Number(20).toString())
|
||||
).toEqual(new BigNumber(10));
|
||||
expect(
|
||||
getOverstakedAmount('0.15', Number(100).toString(), Number(20).toString())
|
||||
).toEqual(new BigNumber(5));
|
||||
expect(
|
||||
getOverstakedAmount('0.2', Number(100).toString(), Number(20).toString())
|
||||
).toEqual(new BigNumber(0));
|
||||
it('returns "0%" when both arguments are null or undefined', () => {
|
||||
expect(getOverstakingPenalty(null, null)).toBe('0%');
|
||||
expect(getOverstakingPenalty(undefined, undefined)).toBe('0%');
|
||||
expect(getOverstakingPenalty(null, undefined)).toBe('0%');
|
||||
expect(getOverstakingPenalty(undefined, null)).toBe('0%');
|
||||
});
|
||||
|
||||
it('should return 0 if the overstaked amount is negative', () => {
|
||||
expect(
|
||||
getOverstakedAmount('0.8', Number(100).toString(), Number(20).toString())
|
||||
).toEqual(new BigNumber(0));
|
||||
it('returns "0%" when one argument is null or undefined', () => {
|
||||
expect(getOverstakingPenalty('10', null)).toBe('0%');
|
||||
expect(getOverstakingPenalty(null, '20')).toBe('0%');
|
||||
expect(getOverstakingPenalty('10', undefined)).toBe('0%');
|
||||
expect(getOverstakingPenalty(undefined, '20')).toBe('0%');
|
||||
});
|
||||
|
||||
it('returns "0%" when validatorScore or stakeScore is zero', () => {
|
||||
expect(getOverstakingPenalty('0', '20')).toBe('0%');
|
||||
expect(getOverstakingPenalty('10', '0')).toBe('0%');
|
||||
});
|
||||
|
||||
it('returns the correct overstaking penalty', () => {
|
||||
expect(getOverstakingPenalty('0.18', '0.2')).toBe('10.00%');
|
||||
expect(getOverstakingPenalty('0.2', '0.2')).toBe('0.00%');
|
||||
expect(getOverstakingPenalty('0.04', '0.2')).toBe('80.00%');
|
||||
});
|
||||
|
||||
it('handles string numbers with decimals', () => {
|
||||
expect(getOverstakingPenalty('7.5', '15')).toBe('50.00%');
|
||||
expect(getOverstakingPenalty('12.5', '25')).toBe('50.00%');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ export const getLastEpochScoreAndPerformance = (
|
||||
|
||||
return {
|
||||
rawValidatorScore: validator?.rewardScore?.rawValidatorScore,
|
||||
performanceScore: validator?.rankingScore?.performanceScore,
|
||||
performanceScore: validator?.rewardScore?.performanceScore,
|
||||
stakeScore: validator?.rankingScore?.stakeScore,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -42,31 +43,26 @@ export const getPerformancePenalty = (performanceScore?: string) =>
|
||||
2
|
||||
);
|
||||
|
||||
export const getOverstakedAmount = (
|
||||
validatorScore: string | null | undefined,
|
||||
totalStake: string,
|
||||
stakedOnNode: string
|
||||
) => {
|
||||
const toReturn = validatorScore
|
||||
? new BigNumber(stakedOnNode).minus(
|
||||
new BigNumber(validatorScore).times(new BigNumber(totalStake))
|
||||
)
|
||||
: new BigNumber(0);
|
||||
|
||||
return toReturn.isNegative() ? new BigNumber(0) : toReturn;
|
||||
};
|
||||
|
||||
export const getOverstakingPenalty = (
|
||||
overstakedAmount: BigNumber,
|
||||
stakedOnNode: string
|
||||
validatorScore: string | null | undefined,
|
||||
stakeScore: string | null | undefined
|
||||
) => {
|
||||
if (!validatorScore || !stakeScore) {
|
||||
return '0%';
|
||||
}
|
||||
|
||||
// avoid division by zero
|
||||
if (new BigNumber(stakedOnNode).isZero() || overstakedAmount.isZero()) {
|
||||
return '0';
|
||||
if (
|
||||
new BigNumber(validatorScore).isZero() ||
|
||||
new BigNumber(stakeScore).isZero()
|
||||
) {
|
||||
return '0%';
|
||||
}
|
||||
|
||||
return formatNumberPercentage(
|
||||
overstakedAmount.dividedBy(new BigNumber(stakedOnNode)).times(100),
|
||||
new BigNumber(1)
|
||||
.minus(new BigNumber(validatorScore).dividedBy(new BigNumber(stakeScore)))
|
||||
.times(100),
|
||||
2
|
||||
);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -411,6 +411,7 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
|
||||
it('approved amount is less than deposit', function () {
|
||||
// 1001-DEPO-006
|
||||
// 1001-DEPO-007
|
||||
cy.getByTestId(depositsTab).click();
|
||||
cy.getByTestId('deposit-button').click();
|
||||
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
|
||||
@@ -430,8 +431,8 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
});
|
||||
|
||||
it('withdraw - delay verification', function () {
|
||||
// 1001-DEPO-007
|
||||
// 1001-DEPO-024
|
||||
// 1002-WITH-007
|
||||
|
||||
cy.visit('/#/portfolio');
|
||||
cy.get('main[data-testid="/portfolio"]').should('exist');
|
||||
|
||||
@@ -76,6 +76,7 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('insufficient funds', () => {
|
||||
// 1001-DEPO-004
|
||||
mockWeb3DepositCalls({
|
||||
allowance: '1000',
|
||||
depositLifetimeLimit: '1000',
|
||||
@@ -87,7 +88,10 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
.clear()
|
||||
.type('850')
|
||||
.next(`[data-testid="${formFieldError}"]`)
|
||||
.should('have.text', 'Insufficient amount in Ethereum wallet');
|
||||
.should(
|
||||
'have.text',
|
||||
"You can't deposit more than you have in your Ethereum wallet, 800 tEURO"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import { aliasGQLQuery, checkSorting } from '@vegaprotocol/cypress';
|
||||
import { marketsQuery } from '@vegaprotocol/mock';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
|
||||
@@ -85,6 +85,7 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('view-market-list-link')
|
||||
.should('have.attr', 'href', '#/markets/all')
|
||||
.click();
|
||||
|
||||
cy.get('[data-testid="All markets"]').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
@@ -117,6 +118,85 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
`${Cypress.env('VEGA_TOKEN_URL')}/proposals/propose/new-market`
|
||||
);
|
||||
});
|
||||
it('proposed markets tab should be sorted properly', () => {
|
||||
cy.getByTestId('view-market-list-link').click();
|
||||
cy.get('[data-testid="Proposed markets"]').click();
|
||||
const marketColDefault = [
|
||||
'ETHUSD',
|
||||
'LINKUSD',
|
||||
'ETHUSD',
|
||||
'ETHDAI.MF21',
|
||||
'AAPL.MF21',
|
||||
'BTCUSD.MF21',
|
||||
'TSLA.QM21',
|
||||
'AAVEDAI.MF21',
|
||||
'ETHBTC.QM21',
|
||||
'UNIDAI.MF21',
|
||||
];
|
||||
const marketColAsc = [
|
||||
'AAPL.MF21',
|
||||
'AAVEDAI.MF21',
|
||||
'BTCUSD.MF21',
|
||||
'ETHBTC.QM21',
|
||||
'ETHDAI.MF21',
|
||||
'ETHUSD',
|
||||
'ETHUSD',
|
||||
'LINKUSD',
|
||||
'TSLA.QM21',
|
||||
'UNIDAI.MF21',
|
||||
];
|
||||
const marketColDesc = [
|
||||
'UNIDAI.MF21',
|
||||
'TSLA.QM21',
|
||||
'LINKUSD',
|
||||
'ETHUSD',
|
||||
'ETHUSD',
|
||||
'ETHDAI.MF21',
|
||||
'ETHBTC.QM21',
|
||||
'BTCUSD.MF21',
|
||||
'AAVEDAI.MF21',
|
||||
'AAPL.MF21',
|
||||
];
|
||||
checkSorting('market', marketColDefault, marketColAsc, marketColDesc);
|
||||
|
||||
const stateColDefault = [
|
||||
'Open',
|
||||
'Passed',
|
||||
'Waiting for Node Vote',
|
||||
'Open',
|
||||
'Passed',
|
||||
'Open',
|
||||
'Passed',
|
||||
'Open',
|
||||
'Waiting for Node Vote',
|
||||
'Open',
|
||||
];
|
||||
const stateColAsc = [
|
||||
'Open',
|
||||
'Open',
|
||||
'Open',
|
||||
'Open',
|
||||
'Open',
|
||||
'Passed',
|
||||
'Passed',
|
||||
'Passed',
|
||||
'Waiting for Node Vote',
|
||||
'Waiting for Node Vote',
|
||||
];
|
||||
const stateColDesc = [
|
||||
'Waiting for Node Vote',
|
||||
'Waiting for Node Vote',
|
||||
'Passed',
|
||||
'Passed',
|
||||
'Passed',
|
||||
'Open',
|
||||
'Open',
|
||||
'Open',
|
||||
'Open',
|
||||
'Open',
|
||||
];
|
||||
checkSorting('state', stateColDefault, stateColAsc, stateColDesc);
|
||||
});
|
||||
|
||||
it('opening auction subsets should be properly displayed', () => {
|
||||
cy.mockTradingPage(
|
||||
|
||||
@@ -41,6 +41,12 @@ describe('accounts', { tags: '@smoke' }, () => {
|
||||
.should('have.text', '100,001.01');
|
||||
});
|
||||
|
||||
it('asset detail should be properly rendered', () => {
|
||||
cy.getByTestId('Collateral').click();
|
||||
cy.getByTestId('asset').contains('tEURO').click();
|
||||
cy.get('[data-testid$="_label"]').should('have.length', 16);
|
||||
});
|
||||
|
||||
describe('sorting by ag-grid columns should work well', () => {
|
||||
it('sorting by asset', () => {
|
||||
cy.getByTestId('Collateral').click();
|
||||
|
||||
+5
-252
@@ -1,5 +1,5 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { aliasGQLQuery, mockConnectWallet } from '@vegaprotocol/cypress';
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import { testOrderSubmission } from '../support/order-validation';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
@@ -13,8 +13,6 @@ const orderSizeField = 'order-size';
|
||||
const orderPriceField = 'order-price';
|
||||
const orderTIFDropDown = 'order-tif';
|
||||
const placeOrderBtn = 'place-order';
|
||||
const toggleShort = 'order-side-SIDE_SELL';
|
||||
const toggleLong = 'order-side-SIDE_BUY';
|
||||
const toggleLimit = 'order-type-TYPE_LIMIT';
|
||||
const toggleMarket = 'order-type-TYPE_MARKET';
|
||||
|
||||
@@ -32,34 +30,6 @@ const displayTomorrow = () => {
|
||||
return tomorrow.toISOString().substring(0, 16);
|
||||
};
|
||||
|
||||
describe('time in force default values', () => {
|
||||
before(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
it('must have market order set up to IOC by default', function () {
|
||||
// 7002-SORD-031
|
||||
cy.getByTestId(toggleMarket).click();
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
TIFlist.filter((item) => item.code === 'IOC')[0].text
|
||||
);
|
||||
});
|
||||
|
||||
it('must have time in force set to GTC for limit order', function () {
|
||||
// 7002-SORD-031
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
TIFlist.filter((item) => item.code === 'GTC')[0].text
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('must submit order', { tags: '@smoke' }, () => {
|
||||
// 7002-SORD-039
|
||||
before(() => {
|
||||
@@ -393,227 +363,6 @@ describe(
|
||||
}
|
||||
);
|
||||
|
||||
describe('deal ticket validation', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
it('must show place order button and connect wallet if wallet is not connected', () => {
|
||||
// 0003-WTXN-001
|
||||
cy.getByTestId('connect-vega-wallet'); // Not connected
|
||||
cy.getByTestId('order-connect-wallet').should('exist');
|
||||
cy.getByTestId(placeOrderBtn).should('exist');
|
||||
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
|
||||
});
|
||||
|
||||
it('must be able to select order direction - long/short', function () {
|
||||
// 7002-SORD-004
|
||||
cy.getByTestId(toggleShort).click().children('input').should('be.checked');
|
||||
cy.getByTestId(toggleLong).click().children('input').should('be.checked');
|
||||
});
|
||||
|
||||
it('must be able to select order type - limit/market', function () {
|
||||
// 7002-SORD-005
|
||||
// 7002-SORD-006
|
||||
// 7002-SORD-007
|
||||
cy.getByTestId(toggleLimit).click().children('input').should('be.checked');
|
||||
cy.getByTestId(toggleMarket).click().children('input').should('be.checked');
|
||||
});
|
||||
|
||||
it('order connect vega wallet button should connect', () => {
|
||||
mockConnectWallet();
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.getByTestId(orderPriceField).clear().type('101');
|
||||
cy.getByTestId('order-connect-wallet').click();
|
||||
cy.getByTestId('dialog-content').should('be.visible');
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-jsonRpc"]')
|
||||
.click();
|
||||
cy.wait('@walletReq');
|
||||
cy.getByTestId(placeOrderBtn).should('be.visible');
|
||||
cy.getByTestId(toggleLimit).children('input').should('be.checked');
|
||||
cy.getByTestId(orderPriceField).should('have.value', '101');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deal ticket size validation', { tags: '@smoke' }, function () {
|
||||
beforeEach(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
it('must warn if order size input has too many digits after the decimal place', function () {
|
||||
// 7002-SORD-016
|
||||
cy.getByTestId('order-type-TYPE_MARKET').click();
|
||||
cy.getByTestId(orderSizeField).clear().type('1.234');
|
||||
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId(placeOrderBtn).should('be.disabled');
|
||||
cy.getByTestId('dealticket-error-message-size-market').should(
|
||||
'have.text',
|
||||
'Size must be whole numbers for this market'
|
||||
);
|
||||
});
|
||||
|
||||
it('must warn if order size is set to 0', function () {
|
||||
cy.getByTestId('order-type-TYPE_MARKET').click();
|
||||
cy.getByTestId(orderSizeField).clear().type('0');
|
||||
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId(placeOrderBtn).should('be.disabled');
|
||||
cy.getByTestId('dealticket-error-message-size-market').should(
|
||||
'have.text',
|
||||
'Size cannot be lower than 1'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('limit order validations', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.setVegaWallet();
|
||||
});
|
||||
|
||||
it('must see the price unit', function () {
|
||||
// 7002-SORD-018
|
||||
cy.getByTestId(orderPriceField)
|
||||
.siblings('label')
|
||||
.should('have.text', 'Price (DAI)');
|
||||
});
|
||||
|
||||
it('must see warning when placing an order with expiry date in past', () => {
|
||||
const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
const expiresAtInputValue = expiresAt.toISOString().substring(0, 16);
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.getByTestId(orderPriceField).clear().type('0.1');
|
||||
cy.getByTestId(orderSizeField).clear().type('1');
|
||||
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
|
||||
|
||||
cy.log('choosing yesterday');
|
||||
cy.getByTestId('date-picker-field').type(expiresAtInputValue);
|
||||
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
|
||||
cy.getByTestId('dealticket-error-message-expiry').should(
|
||||
'have.text',
|
||||
'The expiry date that you have entered appears to be in the past'
|
||||
);
|
||||
});
|
||||
|
||||
it('must see warning if price has too many digits after decimal place', function () {
|
||||
// 7002-SORD-059
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
|
||||
cy.getByTestId(orderSizeField).clear().type('1');
|
||||
cy.getByTestId(orderPriceField).clear().type('1.123456');
|
||||
cy.getByTestId('dealticket-error-message-price-limit').should(
|
||||
'have.text',
|
||||
'Price accepts up to 5 decimal places'
|
||||
);
|
||||
});
|
||||
|
||||
describe('time in force validations', function () {
|
||||
const validTIF = TIFlist;
|
||||
validTIF.forEach((tif) => {
|
||||
// 7002-SORD-023
|
||||
// 7002-SORD-024
|
||||
// 7002-SORD-025
|
||||
// 7002-SORD-026
|
||||
// 7002-SORD-027
|
||||
// 7002-SORD-028
|
||||
|
||||
it(`must be able to select ${tif.code}`, function () {
|
||||
cy.getByTestId(orderTIFDropDown).select(tif.value);
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
tif.text
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('selections should be remembered', () => {
|
||||
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
|
||||
cy.getByTestId(toggleMarket).click();
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
TIFlist.filter((item) => item.code === 'IOC')[0].text
|
||||
);
|
||||
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_FOK');
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
TIFlist.filter((item) => item.code === 'GTT')[0].text
|
||||
);
|
||||
cy.getByTestId(toggleMarket).click();
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
TIFlist.filter((item) => item.code === 'FOK')[0].text
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('market order validations', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
cy.getByTestId(toggleMarket).click();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.setVegaWallet();
|
||||
});
|
||||
|
||||
it('must not see the price unit', function () {
|
||||
// 7002-SORD-019
|
||||
cy.getByTestId(orderPriceField).should('not.exist');
|
||||
});
|
||||
|
||||
describe('time in force validations', function () {
|
||||
const validTIF = TIFlist.filter((tif) => ['FOK', 'IOC'].includes(tif.code));
|
||||
const invalidTIF = TIFlist.filter(
|
||||
(tif) => !['FOK', 'IOC'].includes(tif.code)
|
||||
);
|
||||
|
||||
validTIF.forEach((tif) => {
|
||||
// 7002-SORD-025
|
||||
// 7002-SORD-026
|
||||
|
||||
it(`must be able to select ${tif.code}`, function () {
|
||||
cy.getByTestId(orderTIFDropDown).select(tif.value);
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
tif.text
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
invalidTIF.forEach((tif) => {
|
||||
// 7002-SORD-023
|
||||
// 7002-SORD-024
|
||||
// 7002-SORD-027
|
||||
// 7002-SORD-028
|
||||
it(`must not be able to select ${tif.code}`, function () {
|
||||
cy.getByTestId(orderTIFDropDown).should('not.contain', tif.text);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('suspended market validation', { tags: '@regression' }, () => {
|
||||
before(() => {
|
||||
cy.setVegaWallet();
|
||||
@@ -765,6 +514,8 @@ describe('account validation', { tags: '@regression' }, () => {
|
||||
it('must show error returned by wallet ', () => {
|
||||
// 0003-WTXN-009
|
||||
// 0003-WTXN-011
|
||||
// 0002-WCON-016
|
||||
// 0003-WTXN-008
|
||||
|
||||
//trigger error from the wallet
|
||||
cy.intercept('POST', 'http://localhost:1789/api/v2/requests', (req) => {
|
||||
@@ -788,6 +539,8 @@ describe('account validation', { tags: '@regression' }, () => {
|
||||
'contain.text',
|
||||
'The connection to your Vega Wallet has been lost.'
|
||||
);
|
||||
cy.getByTestId('connect-vega-wallet').click();
|
||||
cy.getByTestId('dialog-content').should('be.visible');
|
||||
});
|
||||
|
||||
it('must see that the order was rejected by the connected wallet', () => {
|
||||
@@ -0,0 +1,329 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { mockConnectWallet } from '@vegaprotocol/cypress';
|
||||
|
||||
const orderSizeField = 'order-size';
|
||||
const orderPriceField = 'order-price';
|
||||
const orderTIFDropDown = 'order-tif';
|
||||
const placeOrderBtn = 'place-order';
|
||||
const toggleShort = 'order-side-SIDE_SELL';
|
||||
const toggleLong = 'order-side-SIDE_BUY';
|
||||
const toggleLimit = 'order-type-TYPE_LIMIT';
|
||||
const toggleMarket = 'order-type-TYPE_MARKET';
|
||||
|
||||
const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
|
||||
return {
|
||||
code: Schema.OrderTimeInForceCode[value],
|
||||
value,
|
||||
text: Schema.OrderTimeInForceMapping[value],
|
||||
};
|
||||
});
|
||||
|
||||
describe('time in force default values', () => {
|
||||
before(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
it('must have market order set up to IOC by default', function () {
|
||||
// 7002-SORD-030
|
||||
cy.getByTestId(toggleMarket).click();
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
TIFlist.filter((item) => item.code === 'IOC')[0].text
|
||||
);
|
||||
});
|
||||
|
||||
it('must have time in force set to GTC for limit order', function () {
|
||||
// 7002-SORD-031
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
TIFlist.filter((item) => item.code === 'GTC')[0].text
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deal ticket validation', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
it('must show place order button and connect wallet if wallet is not connected', () => {
|
||||
// 0003-WTXN-001
|
||||
cy.getByTestId('connect-vega-wallet'); // Not connected
|
||||
cy.getByTestId('order-connect-wallet').should('exist');
|
||||
cy.getByTestId(placeOrderBtn).should('exist');
|
||||
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
|
||||
});
|
||||
|
||||
it('must be able to select order direction - long/short', function () {
|
||||
// 7002-SORD-004
|
||||
cy.getByTestId(toggleShort).click().children('input').should('be.checked');
|
||||
cy.getByTestId(toggleLong).click().children('input').should('be.checked');
|
||||
});
|
||||
|
||||
it('must be able to select order type - limit/market', function () {
|
||||
// 7002-SORD-005
|
||||
// 7002-SORD-006
|
||||
// 7002-SORD-007
|
||||
cy.getByTestId(toggleLimit).click().children('input').should('be.checked');
|
||||
cy.getByTestId(toggleMarket).click().children('input').should('be.checked');
|
||||
});
|
||||
|
||||
it('order connect vega wallet button should connect', () => {
|
||||
mockConnectWallet();
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.getByTestId(orderPriceField).clear().type('101');
|
||||
cy.getByTestId('order-connect-wallet').click();
|
||||
cy.getByTestId('dialog-content').should('be.visible');
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-jsonRpc"]')
|
||||
.click();
|
||||
cy.wait('@walletReq');
|
||||
cy.getByTestId(placeOrderBtn).should('be.visible');
|
||||
cy.getByTestId(toggleLimit).children('input').should('be.checked');
|
||||
cy.getByTestId(orderPriceField).should('have.value', '101');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deal ticket size validation', { tags: '@smoke' }, function () {
|
||||
beforeEach(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
it('must warn if order size input has too many digits after the decimal place', function () {
|
||||
// 7002-SORD-016
|
||||
cy.getByTestId('order-type-TYPE_MARKET').click();
|
||||
cy.getByTestId(orderSizeField).clear().type('1.234');
|
||||
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId(placeOrderBtn).should('be.disabled');
|
||||
cy.getByTestId('dealticket-error-message-size-market').should(
|
||||
'have.text',
|
||||
'Size must be whole numbers for this market'
|
||||
);
|
||||
});
|
||||
|
||||
it('must warn if order size is set to 0', function () {
|
||||
cy.getByTestId('order-type-TYPE_MARKET').click();
|
||||
cy.getByTestId(orderSizeField).clear().type('0');
|
||||
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
cy.getByTestId(placeOrderBtn).should('be.disabled');
|
||||
cy.getByTestId('dealticket-error-message-size-market').should(
|
||||
'have.text',
|
||||
'Size cannot be lower than 1'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('limit order validations', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.setVegaWallet();
|
||||
});
|
||||
|
||||
it('must see the price unit', function () {
|
||||
// 7002-SORD-018
|
||||
cy.getByTestId(orderPriceField)
|
||||
.siblings('label')
|
||||
.should('have.text', 'Price (DAI)');
|
||||
});
|
||||
|
||||
it('must see warning when placing an order with expiry date in past', () => {
|
||||
const expiresAt = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
const expiresAtInputValue = expiresAt.toISOString().substring(0, 16);
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.getByTestId(orderPriceField).clear().type('0.1');
|
||||
cy.getByTestId(orderSizeField).clear().type('1');
|
||||
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
|
||||
|
||||
cy.log('choosing yesterday');
|
||||
cy.getByTestId('date-picker-field').type(expiresAtInputValue);
|
||||
|
||||
cy.getByTestId(placeOrderBtn).click();
|
||||
|
||||
cy.getByTestId('dealticket-error-message-expiry').should(
|
||||
'have.text',
|
||||
'The expiry date that you have entered appears to be in the past'
|
||||
);
|
||||
});
|
||||
|
||||
it('must see warning if price has too many digits after decimal place', function () {
|
||||
// 7002-SORD-059
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
|
||||
cy.getByTestId(orderSizeField).clear().type('1');
|
||||
cy.getByTestId(orderPriceField).clear().type('1.123456');
|
||||
cy.getByTestId('dealticket-error-message-price-limit').should(
|
||||
'have.text',
|
||||
'Price accepts up to 5 decimal places'
|
||||
);
|
||||
});
|
||||
|
||||
describe('time in force validations', function () {
|
||||
const validTIF = TIFlist;
|
||||
validTIF.forEach((tif) => {
|
||||
// 7002-SORD-023
|
||||
// 7002-SORD-024
|
||||
// 7002-SORD-025
|
||||
// 7002-SORD-026
|
||||
// 7002-SORD-027
|
||||
// 7002-SORD-028
|
||||
|
||||
it(`must be able to select ${tif.code}`, function () {
|
||||
cy.getByTestId(orderTIFDropDown).select(tif.value);
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
tif.text
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('selections should be remembered', () => {
|
||||
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTT');
|
||||
cy.getByTestId(toggleMarket).click();
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
TIFlist.filter((item) => item.code === 'IOC')[0].text
|
||||
);
|
||||
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_FOK');
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
TIFlist.filter((item) => item.code === 'GTT')[0].text
|
||||
);
|
||||
cy.getByTestId(toggleMarket).click();
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
TIFlist.filter((item) => item.code === 'FOK')[0].text
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('market order validations', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
cy.getByTestId(toggleMarket).click();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.setVegaWallet();
|
||||
});
|
||||
|
||||
it('must not see the price unit', function () {
|
||||
// 7002-SORD-019
|
||||
cy.getByTestId(orderPriceField).should('not.exist');
|
||||
});
|
||||
|
||||
describe('time in force validations', function () {
|
||||
const validTIF = TIFlist.filter((tif) => ['FOK', 'IOC'].includes(tif.code));
|
||||
const invalidTIF = TIFlist.filter(
|
||||
(tif) => !['FOK', 'IOC'].includes(tif.code)
|
||||
);
|
||||
|
||||
validTIF.forEach((tif) => {
|
||||
// 7002-SORD-025
|
||||
// 7002-SORD-026
|
||||
|
||||
it(`must be able to select ${tif.code}`, function () {
|
||||
cy.getByTestId(orderTIFDropDown).select(tif.value);
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
tif.text
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
invalidTIF.forEach((tif) => {
|
||||
// 7002-SORD-023
|
||||
// 7002-SORD-024
|
||||
// 7002-SORD-027
|
||||
// 7002-SORD-028
|
||||
it(`must not be able to select ${tif.code}`, function () {
|
||||
cy.getByTestId(orderTIFDropDown).should('not.contain', tif.text);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('post and reduce order validations', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
cy.getByTestId(toggleMarket).click();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.setVegaWallet();
|
||||
});
|
||||
|
||||
const validTIF = TIFlist.filter((tif) => ['FOK', 'IOC'].includes(tif.code));
|
||||
|
||||
validTIF.forEach((tif) => {
|
||||
// 7002-SORD-025
|
||||
// 7002-SORD-026
|
||||
|
||||
it(`post and reduce order market for ${tif.code}`, function () {
|
||||
cy.getByTestId(orderTIFDropDown).select(tif.value);
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
tif.text
|
||||
);
|
||||
cy.getByTestId('post-only').should('be.disabled');
|
||||
cy.getByTestId('reduce-only').should('be.enabled');
|
||||
});
|
||||
});
|
||||
|
||||
validTIF.forEach((tif) => {
|
||||
it(`post and reduce order limit for ${tif.code}`, function () {
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.getByTestId(orderTIFDropDown).select(tif.value);
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
tif.text
|
||||
);
|
||||
cy.getByTestId('post-only').should('be.disabled');
|
||||
cy.getByTestId('reduce-only').should('be.enabled');
|
||||
});
|
||||
});
|
||||
|
||||
const validTIFLimit = TIFlist.filter((tif) =>
|
||||
['GFA', 'GFN', 'GTC', 'GTT'].includes(tif.code)
|
||||
);
|
||||
|
||||
validTIFLimit.forEach((tif) => {
|
||||
it(`post and reduce order for limit ${tif.code}`, function () {
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
cy.getByTestId(orderTIFDropDown).select(tif.value);
|
||||
cy.get(`[data-testid=${orderTIFDropDown}] option:selected`).should(
|
||||
'have.text',
|
||||
tif.text
|
||||
);
|
||||
cy.getByTestId('post-only').should('be.enabled');
|
||||
cy.getByTestId('reduce-only').should('be.disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -117,6 +117,7 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
|
||||
// 0002-WCON-002
|
||||
// 0002-WCON-005
|
||||
// 0002-WCON-007
|
||||
// 0002-WCON-009
|
||||
|
||||
mockConnectWallet();
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
@@ -124,6 +125,10 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
|
||||
.find('[data-testid="connector-jsonRpc"]')
|
||||
.click();
|
||||
cy.wait('@walletReq');
|
||||
cy.getByTestId(dialogContent).should(
|
||||
'contain.text',
|
||||
'Approve the connection from your Vega wallet app.'
|
||||
);
|
||||
cy.getByTestId(dialogContent).should('not.exist');
|
||||
cy.getByTestId(manageVegaBtn).should('exist');
|
||||
});
|
||||
@@ -132,6 +137,7 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
|
||||
// 0002-WCON-002
|
||||
// 0002-WCON-005
|
||||
// 0002-WCON-007
|
||||
// 0002-WCON-015
|
||||
|
||||
mockConnectWalletWithUserError();
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { tooltipMapping } from '@vegaprotocol/market-info';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
createDocsLinks,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
Tabs,
|
||||
Link as UiToolkitLink,
|
||||
Indicator,
|
||||
ExternalLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
@@ -37,6 +39,7 @@ import { Link, useParams } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
|
||||
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
const enum LiquidityTabs {
|
||||
Active = 'active',
|
||||
@@ -144,6 +147,7 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
|
||||
market?.tradableInstrument.instrument.product.settlementAsset.decimals || 0;
|
||||
const symbol =
|
||||
market?.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.market_liquidity_stakeToCcyVolume,
|
||||
@@ -211,6 +215,13 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
|
||||
<HeaderStat heading={t('Market ID')}>
|
||||
<div className="break-word">{marketId}</div>
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Learn more')}>
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}>
|
||||
{t('Providing liquidity')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</HeaderStat>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -301,7 +301,7 @@ export const AccountHistoryChart = ({
|
||||
asset: AssetFieldsFragment;
|
||||
}) => {
|
||||
const { theme } = useThemeSwitcher();
|
||||
const values: { cols: string[]; rows: [Date, ...number[]][] } | null =
|
||||
const values: { cols: [string, string]; rows: [Date, number][] } | null =
|
||||
useMemo(() => {
|
||||
if (!data?.balanceChanges.edges.length) {
|
||||
return null;
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { MarketData } from '@vegaprotocol/market-list';
|
||||
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
|
||||
import { HeaderStat } from '../header';
|
||||
import {
|
||||
ExternalLink,
|
||||
Indicator,
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
@@ -18,9 +19,11 @@ import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
|
||||
import { AuctionTrigger, MarketTradingMode } from '@vegaprotocol/types';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
createDocsLinks,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
interface Props {
|
||||
marketId?: string;
|
||||
@@ -44,6 +47,8 @@ export const MarketLiquiditySupplied = ({
|
||||
params.market_liquidity_targetstake_triggering_ratio
|
||||
);
|
||||
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
marketId: marketId || '',
|
||||
@@ -126,6 +131,14 @@ export const MarketLiquiditySupplied = ({
|
||||
<Link href={`/#/liquidity/${marketId}`} data-testid="view-liquidity-link">
|
||||
{t('View liquidity provision table')}
|
||||
</Link>
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}
|
||||
className="mt-2"
|
||||
>
|
||||
{t('Learn about providing liquidity')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
{showMessage && (
|
||||
<p className="mt-4">
|
||||
{t(
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import { createDocsLinks } from '@vegaprotocol/utils';
|
||||
|
||||
export const Navbar = ({
|
||||
theme = 'system',
|
||||
@@ -37,6 +38,7 @@ export const Navbar = ({
|
||||
const tradingPath = marketId
|
||||
? Links[Routes.MARKET](marketId)
|
||||
: Links[Routes.MARKET]();
|
||||
|
||||
return (
|
||||
<Navigation
|
||||
appName="Console"
|
||||
@@ -89,7 +91,9 @@ export const Navbar = ({
|
||||
<NavigationContent>
|
||||
<NavigationList>
|
||||
<NavigationItem>
|
||||
<NavExternalLink href={VEGA_DOCS_URL}>
|
||||
<NavExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).NEW_TO_VEGA}
|
||||
>
|
||||
{t('Docs')}
|
||||
</NavExternalLink>
|
||||
</NavigationItem>
|
||||
|
||||
@@ -185,7 +185,7 @@ export const columns = (
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onSelect(market.id, e.metaKey);
|
||||
onSelect(market.id, e.metaKey || e.ctrlKey);
|
||||
}}
|
||||
>
|
||||
<UILink>{market.tradableInstrument.instrument.code}</UILink>
|
||||
@@ -366,7 +366,7 @@ export const columnsPositionMarkets = (
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onSelect(market.id, e.metaKey);
|
||||
onSelect(market.id, e.metaKey || e.ctrlKey);
|
||||
}}
|
||||
>
|
||||
<UILink>{market.tradableInstrument.instrument.code}</UILink>
|
||||
|
||||
@@ -44,7 +44,7 @@ export const SelectMarketTableRow = ({
|
||||
<tr
|
||||
className={`hover:bg-neutral-200 dark:hover:bg-neutral-700 cursor-pointer relative h-[34px]`}
|
||||
onClick={(ev) => {
|
||||
onSelect(marketId, ev.metaKey);
|
||||
onSelect(marketId, ev.metaKey || ev.ctrlKey);
|
||||
}}
|
||||
data-testid={`market-link-${marketId}`}
|
||||
>
|
||||
|
||||
@@ -8,6 +8,10 @@ import type {
|
||||
MarketData,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import { SelectMarketLandingTable } from './welcome-landing-dialog';
|
||||
const mockMarketClickHandler = jest.fn();
|
||||
jest.mock('../../lib/hooks/use-market-click-handler', () => ({
|
||||
useMarketClickHandler: () => mockMarketClickHandler,
|
||||
}));
|
||||
|
||||
type Market = MarketMaybeWithCandles & MarketMaybeWithData;
|
||||
type PartialMarket = Partial<
|
||||
@@ -174,4 +178,25 @@ describe('WelcomeLandingDialog', () => {
|
||||
fireEvent.click(screen.getAllByTestId(`market-link-2`)[0]);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call onClose when metaKey is held', () => {
|
||||
const onClose = jest.fn();
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SelectMarketLandingTable
|
||||
markets={[MARKET_A as Market, MARKET_B as Market]}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0], {
|
||||
metaKey: true,
|
||||
});
|
||||
expect(mockMarketClickHandler).toHaveBeenCalled();
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,9 +12,10 @@ import {
|
||||
SelectMarketTableRow,
|
||||
} from '../select-market';
|
||||
import { WelcomeDialogHeader } from './welcome-dialog-header';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ProposedMarkets } from './proposed-markets';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
|
||||
export const SelectMarketLandingTable = ({
|
||||
markets,
|
||||
@@ -23,23 +24,13 @@ export const SelectMarketLandingTable = ({
|
||||
markets: MarketMaybeWithDataAndCandles[] | null;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const marketId = params.marketId;
|
||||
|
||||
const onSelect = useCallback(
|
||||
(id: string) => {
|
||||
if (id && id !== marketId) {
|
||||
navigate(Links[Routes.MARKET](id));
|
||||
}
|
||||
},
|
||||
[marketId, navigate]
|
||||
);
|
||||
|
||||
const onSelect = useMarketClickHandler();
|
||||
const onSelectMarket = useCallback(
|
||||
(id: string) => {
|
||||
onSelect(id);
|
||||
onClose();
|
||||
(id: string, metaKey?: boolean) => {
|
||||
onSelect(id, metaKey);
|
||||
if (!metaKey) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onSelect, onClose]
|
||||
);
|
||||
@@ -73,7 +64,7 @@ export const SelectMarketLandingTable = ({
|
||||
key={i}
|
||||
detailed={false}
|
||||
onSelect={onSelectMarket}
|
||||
columns={columns(market, onSelect, onCellClick)}
|
||||
columns={columns(market, onSelectMarket, onCellClick)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
} from '@vegaprotocol/types';
|
||||
import type { VegaStoredTxState } from '@vegaprotocol/wallet';
|
||||
import { VegaTxStatus } from '@vegaprotocol/wallet';
|
||||
import { VegaTransactionDetails } from './use-vega-transaction-toasts';
|
||||
import {
|
||||
VegaTransactionDetails,
|
||||
getVegaTransactionContentIntent,
|
||||
} from './use-vega-transaction-toasts';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
jest.mock('@vegaprotocol/assets', () => {
|
||||
const A1 = {
|
||||
@@ -278,3 +282,27 @@ describe('VegaTransactionDetails', () => {
|
||||
expect(queryByTestId('toast-panel')?.textContent).toEqual(details);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVegaTransactionContentIntent', () => {
|
||||
it('returns the correct intent for a transaction', () => {
|
||||
expect(getVegaTransactionContentIntent(withdraw).intent).toBe(
|
||||
Intent.Primary
|
||||
);
|
||||
expect(getVegaTransactionContentIntent(submitOrder).intent).toBe(
|
||||
Intent.Success
|
||||
);
|
||||
expect(getVegaTransactionContentIntent(editOrder).intent).toBe(
|
||||
Intent.Primary
|
||||
);
|
||||
expect(getVegaTransactionContentIntent(cancelOrder).intent).toBe(
|
||||
Intent.Primary
|
||||
);
|
||||
expect(getVegaTransactionContentIntent(cancelAll).intent).toBe(
|
||||
Intent.Primary
|
||||
);
|
||||
expect(getVegaTransactionContentIntent(closePosition).intent).toBe(
|
||||
Intent.Primary
|
||||
);
|
||||
expect(getVegaTransactionContentIntent(batch).intent).toBe(Intent.Primary);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,10 +39,14 @@ import { t } from '@vegaprotocol/i18n';
|
||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
|
||||
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
|
||||
import { getRejectionReason, useOrderByIdQuery } from '@vegaprotocol/orders';
|
||||
import {
|
||||
getOrderToastIntent,
|
||||
getOrderToastTitle,
|
||||
getRejectionReason,
|
||||
useOrderByIdQuery,
|
||||
} from '@vegaprotocol/orders';
|
||||
import { useMarketList } from '@vegaprotocol/market-list';
|
||||
import type { Side } from '@vegaprotocol/types';
|
||||
import { OrderStatus } from '@vegaprotocol/types';
|
||||
import { OrderStatusMapping } from '@vegaprotocol/types';
|
||||
import { Size } from '@vegaprotocol/react-helpers';
|
||||
|
||||
@@ -478,12 +482,10 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
getRejectionReason(tx.order) || tx.order.rejectionReason || '';
|
||||
return (
|
||||
<>
|
||||
<ToastHeading>{t('Order rejected')}</ToastHeading>
|
||||
{rejectionReason || tx.order.rejectionReason ? (
|
||||
<ToastHeading>{getOrderToastTitle(tx.order.status)}</ToastHeading>
|
||||
{rejectionReason ? (
|
||||
<p>
|
||||
{t('Your order has been rejected because: %s', [
|
||||
rejectionReason || tx.order.rejectionReason,
|
||||
])}
|
||||
{t('Your order has been rejected because: %s', [rejectionReason])}
|
||||
</p>
|
||||
) : (
|
||||
<p>{t('Your order has been rejected.')}</p>
|
||||
@@ -506,7 +508,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
if (isOrderSubmissionTransaction(tx.body) && tx.order?.rejectionReason) {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Order rejected')}</h3>
|
||||
<h3 className="font-bold">{getOrderToastTitle(tx.order.status)}</h3>
|
||||
<p>{t('Your order was rejected.')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
@@ -545,7 +547,11 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToastHeading>{t('Confirmed')}</ToastHeading>
|
||||
<ToastHeading>
|
||||
{tx.order?.status
|
||||
? getOrderToastTitle(tx.order.status)
|
||||
: t('Confirmed')}
|
||||
</ToastHeading>
|
||||
<p>{t('Your transaction has been confirmed ')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
@@ -580,7 +586,7 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
tx.error instanceof WalletError &&
|
||||
walletNoConnectionCodes.includes(tx.error.code);
|
||||
if (orderRejection) {
|
||||
label = t('Order rejected');
|
||||
label = getOrderToastTitle(tx.order?.status) || t('Order rejected');
|
||||
errorMessage = t('Your order has been rejected because: %s', [
|
||||
orderRejection || tx.order?.rejectionReason || ' ',
|
||||
]);
|
||||
@@ -632,30 +638,8 @@ export const useVegaTransactionToasts = () => {
|
||||
);
|
||||
|
||||
const fromVegaTransaction = (tx: VegaStoredTxState): Toast => {
|
||||
let content: ToastContent;
|
||||
const closeAfter = isFinal(tx) ? CLOSE_AFTER : undefined;
|
||||
if (tx.status === VegaTxStatus.Requested) {
|
||||
content = <VegaTxRequestedToastContent tx={tx} />;
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Pending) {
|
||||
content = <VegaTxPendingToastContentProps tx={tx} />;
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Complete) {
|
||||
content = <VegaTxCompleteToastsContent tx={tx} />;
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Error) {
|
||||
content = <VegaTxErrorToastContent tx={tx} />;
|
||||
}
|
||||
|
||||
// Transaction can be successful but the order can be rejected by the network
|
||||
const intent =
|
||||
(tx.order &&
|
||||
[OrderStatus.STATUS_REJECTED, OrderStatus.STATUS_STOPPED].includes(
|
||||
tx.order.status
|
||||
)) ||
|
||||
tx.order?.rejectionReason
|
||||
? Intent.Danger
|
||||
: intentMap[tx.status];
|
||||
const { intent, content } = getVegaTransactionContentIntent(tx);
|
||||
|
||||
return {
|
||||
id: `vega-${tx.id}`,
|
||||
@@ -679,3 +663,27 @@ export const useVegaTransactionToasts = () => {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const getVegaTransactionContentIntent = (tx: VegaStoredTxState) => {
|
||||
let content: ToastContent;
|
||||
if (tx.status === VegaTxStatus.Requested) {
|
||||
content = <VegaTxRequestedToastContent tx={tx} />;
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Pending) {
|
||||
content = <VegaTxPendingToastContentProps tx={tx} />;
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Complete) {
|
||||
content = <VegaTxCompleteToastsContent tx={tx} />;
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Error) {
|
||||
content = <VegaTxErrorToastContent tx={tx} />;
|
||||
}
|
||||
|
||||
// Transaction can be successful but the order can be rejected by the network
|
||||
const intent =
|
||||
(tx.order &&
|
||||
!isOrderAmendmentTransaction(tx.body) &&
|
||||
getOrderToastIntent(tx.order.status)) ||
|
||||
intentMap[tx.status];
|
||||
return { intent, content };
|
||||
};
|
||||
|
||||
@@ -34,6 +34,18 @@ html [data-theme='dark'] {
|
||||
/* sell candles only use stroke as the candle is solid (without border) */
|
||||
--pennant-color-sell-stroke: theme('colors.vega.pink.500');
|
||||
|
||||
/* studies */
|
||||
--pennant-color-eldar-ray-bear-power: theme('colors.vega.pink.500');
|
||||
--pennant-color-eldar-ray-bull-power: theme('colors.vega.green.650');
|
||||
|
||||
--pennant-color-macd-divergence-buy: theme('colors.vega.green.650');
|
||||
--pennant-color-macd-divergence-sell: theme('colors.vega.pink.500');
|
||||
--pennant-color-macd-signal: theme('colors.vega.blue.500');
|
||||
--pennant-color-macd-macd: theme('colors.vega.yellow.500');
|
||||
|
||||
--pennant-color-volume-buy: theme('colors.vega.green.650');
|
||||
--pennant-color-volume-sell: theme('colors.vega.pink.500');
|
||||
|
||||
/* depth chart */
|
||||
--pennant-color-depth-buy-fill: theme('colors.vega.green.650');
|
||||
--pennant-color-depth-buy-stroke: theme('colors.vega.green.500');
|
||||
@@ -50,6 +62,9 @@ html [data-theme='light'] {
|
||||
/* sell candles only use stroke as the candle is solid (without border) */
|
||||
--pennant-color-sell-stroke: theme('colors.vega.pink.400');
|
||||
|
||||
--pennant-color-volume-buy: theme('colors.vega.green.400');
|
||||
--pennant-color-volume-sell: theme('colors.vega.pink.400');
|
||||
|
||||
/* depth chart */
|
||||
--pennant-color-depth-buy-fill: theme('colors.vega.green.400');
|
||||
--pennant-color-depth-buy-stroke: theme('colors.vega.green.550');
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
FROM --platform=amd64 nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
|
||||
EXPOSE 80
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY ./dist-result/ /usr/share/nginx/html/
|
||||
@@ -5,7 +5,9 @@ export PATH="/app/node_modules/.bin:$PATH"
|
||||
flags="--network-timeout 100000 --pure-lockfile"
|
||||
|
||||
if [[ ! -z "${ENV_NAME}" ]]; then
|
||||
flags="--env=${ENV_NAME} $flags"
|
||||
if [[ "${ENV_NAME}" != "ops-vega" ]]; then
|
||||
flags="--env=${ENV_NAME} $flags"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${APP}" = "trading" ]; then
|
||||
@@ -16,7 +18,3 @@ if [ "${APP}" = "trading" ]; then
|
||||
else
|
||||
yarn nx build ${APP} $flags
|
||||
fi
|
||||
|
||||
env_vars_file="/app/dist/apps/${APP}/.env"
|
||||
# make sure there are no exposed .env files
|
||||
rm $env_vars_file || echo "No env vars file"
|
||||
@@ -13,7 +13,7 @@ RUN apk add --update --no-cache \
|
||||
COPY . ./
|
||||
RUN yarn --network-timeout 100000 --pure-lockfile
|
||||
# work around for different build process in trading
|
||||
RUN sh ./docker-build.sh
|
||||
RUN sh docker/docker-build.sh
|
||||
|
||||
# Server environment
|
||||
# if this fails you need to docker pull nginx:1.23-alpine and pin new SHA
|
||||
@@ -23,6 +23,7 @@ FROM --platform=amd64 nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a
|
||||
EXPOSE 80
|
||||
# Copy dist
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/dist/apps/${APP} /usr/share/nginx/html
|
||||
RUN apk add --no-cache go-ipfs; ipfs init && echo "$(ipfs add -rQ .)" > ipfs-hash; apk del go-ipfs
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY --from=build /app/dist/apps/${APP}/* /usr/share/nginx/html
|
||||
RUN apk add --no-cache go-ipfs; ipfs init && echo "$(ipfs add -rQ .)" > /ipfs-hash; apk del go-ipfs
|
||||
@@ -1,4 +1,10 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { AddressField, TransferFee, TransferForm } from './transfer-form';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
@@ -27,6 +33,34 @@ describe('TransferForm', () => {
|
||||
submitTransfer: jest.fn(),
|
||||
};
|
||||
|
||||
it('validates a manually entered address', async () => {
|
||||
render(<TransferForm {...props} />);
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
const toggle = screen.getByText('Enter manually');
|
||||
fireEvent.click(toggle);
|
||||
// has switched to input
|
||||
expect(toggle).toHaveTextContent('Select from wallet');
|
||||
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: 'invalid-address' },
|
||||
});
|
||||
await waitFor(() => {
|
||||
const errors = screen.getAllByTestId('input-error-text');
|
||||
expect(errors[0]).toHaveTextContent('Invalid Vega key');
|
||||
});
|
||||
|
||||
// same pubkey
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: pubKey },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const errors = screen.getAllByTestId('input-error-text');
|
||||
expect(errors[0]).toHaveTextContent('Vega key is the same');
|
||||
});
|
||||
});
|
||||
|
||||
it('validates fields and submits', async () => {
|
||||
render(<TransferForm {...props} />);
|
||||
|
||||
@@ -62,15 +96,17 @@ describe('TransferForm', () => {
|
||||
formatNumber(asset.balance, asset.decimals)
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
// Test amount validation
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: '0.00000001' },
|
||||
});
|
||||
expect(
|
||||
await screen.findByText('Value is below minimum')
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: '9999999' },
|
||||
});
|
||||
expect(
|
||||
@@ -78,7 +114,7 @@ describe('TransferForm', () => {
|
||||
).toBeInTheDocument();
|
||||
|
||||
// set valid amount
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: amount },
|
||||
});
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
|
||||
@@ -100,78 +136,191 @@ describe('TransferForm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('validates a manually entered address', async () => {
|
||||
render(<TransferForm {...props} />);
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
const toggle = screen.getByText('Enter manually');
|
||||
fireEvent.click(toggle);
|
||||
// has switched to input
|
||||
expect(toggle).toHaveTextContent('Select from wallet');
|
||||
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: 'invalid-address' },
|
||||
});
|
||||
await waitFor(() => {
|
||||
const errors = screen.getAllByTestId('input-error-text');
|
||||
expect(errors[0]).toHaveTextContent('Invalid Vega key');
|
||||
describe('IncludeFeesCheckbox', () => {
|
||||
it('validates fields and submits when checkbox is checked', async () => {
|
||||
render(<TransferForm {...props} />);
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
||||
expect(keySelect.children).toHaveLength(2);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
||||
'',
|
||||
props.pubKeys[1],
|
||||
]);
|
||||
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
|
||||
// Select a pubkey
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: props.pubKeys[1] },
|
||||
});
|
||||
|
||||
// Select asset
|
||||
fireEvent.change(
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
document.querySelector('select[name="asset"]')!,
|
||||
{ target: { value: asset.id } }
|
||||
);
|
||||
|
||||
// assert rich select as updated
|
||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||
asset.name
|
||||
);
|
||||
expect(screen.getByTestId('asset-balance')).toHaveTextContent(
|
||||
formatNumber(asset.balance, asset.decimals)
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
act(() => {
|
||||
/* fire events that update state */
|
||||
// set valid amount
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: amount },
|
||||
});
|
||||
// check include fees checkbox
|
||||
fireEvent.click(checkbox);
|
||||
});
|
||||
|
||||
expect(checkbox).toBeChecked();
|
||||
const expectedFee = new BigNumber(amount)
|
||||
.times(props.feeFactor)
|
||||
.toFixed();
|
||||
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
|
||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
|
||||
expectedAmount
|
||||
);
|
||||
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
|
||||
amount
|
||||
);
|
||||
|
||||
submit();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
||||
expect(props.submitTransfer).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
amount: removeDecimal(amount, asset.decimals),
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// same pubkey
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: pubKey },
|
||||
it('validates fields when checkbox is not checked', async () => {
|
||||
render(<TransferForm {...props} />);
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
||||
expect(keySelect.children).toHaveLength(2);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
||||
'',
|
||||
props.pubKeys[1],
|
||||
]);
|
||||
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
|
||||
// Select a pubkey
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: props.pubKeys[1] },
|
||||
});
|
||||
|
||||
// Select asset
|
||||
fireEvent.change(
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
document.querySelector('select[name="asset"]')!,
|
||||
{ target: { value: asset.id } }
|
||||
);
|
||||
|
||||
// assert rich select as updated
|
||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||
asset.name
|
||||
);
|
||||
expect(screen.getByTestId('asset-balance')).toHaveTextContent(
|
||||
formatNumber(asset.balance, asset.decimals)
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
act(() => {
|
||||
/* fire events that update state */
|
||||
// set valid amount
|
||||
fireEvent.change(amountInput, {
|
||||
target: { value: amount },
|
||||
});
|
||||
});
|
||||
expect(checkbox).not.toBeChecked();
|
||||
const expectedFee = new BigNumber(amount)
|
||||
.times(props.feeFactor)
|
||||
.toFixed();
|
||||
const total = new BigNumber(amount).plus(expectedFee).toFixed();
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
|
||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
|
||||
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AddressField', () => {
|
||||
const props = {
|
||||
pubKeys: ['pubkey-1', 'pubkey-2'],
|
||||
select: <div>select</div>,
|
||||
input: <div>input</div>,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
|
||||
it('toggles content and calls onChange', async () => {
|
||||
const mockOnChange = jest.fn();
|
||||
render(<AddressField {...props} onChange={mockOnChange} />);
|
||||
|
||||
// select should be shown as multiple pubkeys provided
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText('Enter manually'));
|
||||
expect(screen.queryByText('select')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(screen.getByText('Select from wallet'));
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const errors = screen.getAllByTestId('input-error-text');
|
||||
expect(errors[0]).toHaveTextContent('Vega key is the same');
|
||||
it('Does not provide select option if there is only a single key', () => {
|
||||
render(<AddressField {...props} pubKeys={['single-pubKey']} />);
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TransferFee', () => {
|
||||
const props = {
|
||||
amount: '200',
|
||||
feeFactor: '0.001',
|
||||
fee: '0.2',
|
||||
transferAmount: '200',
|
||||
decimals: 8,
|
||||
};
|
||||
it('calculates and renders the transfer fee', () => {
|
||||
render(<TransferFee {...props} />);
|
||||
|
||||
const expected = new BigNumber(props.amount)
|
||||
.times(props.feeFactor)
|
||||
.toFixed();
|
||||
const total = new BigNumber(props.amount).plus(expected).toFixed();
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
|
||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
|
||||
props.amount
|
||||
);
|
||||
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AddressField', () => {
|
||||
const props = {
|
||||
pubKeys: ['pubkey-1', 'pubkey-2'],
|
||||
select: <div>select</div>,
|
||||
input: <div>input</div>,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
|
||||
it('toggles content and calls onChange', async () => {
|
||||
const mockOnChange = jest.fn();
|
||||
render(<AddressField {...props} onChange={mockOnChange} />);
|
||||
|
||||
// select should be shown as multiple pubkeys provided
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText('Enter manually'));
|
||||
expect(screen.queryByText('select')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(screen.getByText('Select from wallet'));
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('Does not provide select option if there is only a single key', () => {
|
||||
render(<AddressField {...props} pubKeys={['single-pubKey']} />);
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TransferFee', () => {
|
||||
const props = {
|
||||
amount: '200',
|
||||
feeFactor: '0.001',
|
||||
};
|
||||
it('calculates and renders the transfer fee', () => {
|
||||
render(<TransferFee {...props} />);
|
||||
|
||||
const expected = new BigNumber(props.amount)
|
||||
.times(props.feeFactor)
|
||||
.toFixed();
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
RichSelect,
|
||||
Select,
|
||||
Tooltip,
|
||||
Checkbox,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { Transfer } from '@vegaprotocol/wallet';
|
||||
import { normalizeTransfer } from '@vegaprotocol/wallet';
|
||||
@@ -63,6 +64,26 @@ export const TransferForm = ({
|
||||
const amount = watch('amount');
|
||||
const assetId = watch('asset');
|
||||
|
||||
const [includeFee, setIncludeFee] = useState(false);
|
||||
|
||||
const transferAmount = useMemo(() => {
|
||||
if (!amount) return undefined;
|
||||
if (includeFee && feeFactor) {
|
||||
return new BigNumber(1).minus(feeFactor).times(amount).toString();
|
||||
}
|
||||
return amount;
|
||||
}, [amount, includeFee, feeFactor]);
|
||||
|
||||
const fee = useMemo(() => {
|
||||
if (!transferAmount) return undefined;
|
||||
if (includeFee) {
|
||||
return new BigNumber(amount).minus(transferAmount).toString();
|
||||
}
|
||||
return (
|
||||
feeFactor && new BigNumber(feeFactor).times(transferAmount).toString()
|
||||
);
|
||||
}, [amount, includeFee, transferAmount, feeFactor]);
|
||||
|
||||
const asset = useMemo(() => {
|
||||
return assets.find((a) => a.id === assetId);
|
||||
}, [assets, assetId]);
|
||||
@@ -72,13 +93,16 @@ export const TransferForm = ({
|
||||
if (!asset) {
|
||||
throw new Error('Submitted transfer with no asset selected');
|
||||
}
|
||||
const transfer = normalizeTransfer(fields.toAddress, fields.amount, {
|
||||
if (!transferAmount) {
|
||||
throw new Error('Submitted transfer with no amount selected');
|
||||
}
|
||||
const transfer = normalizeTransfer(fields.toAddress, transferAmount, {
|
||||
id: asset.id,
|
||||
decimals: asset.decimals,
|
||||
});
|
||||
submitTransfer(transfer);
|
||||
},
|
||||
[asset, submitTransfer]
|
||||
[asset, submitTransfer, transferAmount]
|
||||
);
|
||||
|
||||
const min = useMemo(() => {
|
||||
@@ -213,7 +237,32 @@ export const TransferForm = ({
|
||||
<InputError forInput="amount">{errors.amount.message}</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<TransferFee amount={amount} feeFactor={feeFactor} />
|
||||
<div className="mb-4">
|
||||
<Checkbox
|
||||
name="include-transfer-fee"
|
||||
disabled={!transferAmount}
|
||||
label={
|
||||
<Tooltip
|
||||
description={t(
|
||||
`The fee will be taken from the amount you are transferring.`
|
||||
)}
|
||||
>
|
||||
<div>{t('Include transfer fee')}</div>
|
||||
</Tooltip>
|
||||
}
|
||||
checked={includeFee}
|
||||
onCheckedChange={() => setIncludeFee(!includeFee)}
|
||||
/>
|
||||
</div>
|
||||
{transferAmount && fee && (
|
||||
<TransferFee
|
||||
amount={transferAmount}
|
||||
transferAmount={transferAmount}
|
||||
feeFactor={feeFactor}
|
||||
fee={fee}
|
||||
decimals={asset?.decimals}
|
||||
/>
|
||||
)}
|
||||
<Button type="submit" variant="primary" fill={true}>
|
||||
{t('Confirm transfer')}
|
||||
</Button>
|
||||
@@ -223,34 +272,71 @@ export const TransferForm = ({
|
||||
|
||||
export const TransferFee = ({
|
||||
amount,
|
||||
transferAmount,
|
||||
feeFactor,
|
||||
fee,
|
||||
decimals,
|
||||
}: {
|
||||
amount: string;
|
||||
transferAmount: string;
|
||||
feeFactor: string | null;
|
||||
fee?: string;
|
||||
decimals?: number;
|
||||
}) => {
|
||||
if (!feeFactor || !amount) return null;
|
||||
if (!feeFactor || !amount || !transferAmount || !fee) return null;
|
||||
|
||||
// using toFixed without an argument will always return a
|
||||
// number in normal notation without rounding, formatting functions
|
||||
// arent working in a way which won't round the decimal places
|
||||
const value = new BigNumber(amount).times(feeFactor).toFixed();
|
||||
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex justify-between items-center gap-4 flex-wrap">
|
||||
<div>
|
||||
<div className="mb-4 flex flex-col gap-2 text-xs">
|
||||
<div className="flex justify-between gap-1 items-center flex-wrap">
|
||||
<Tooltip
|
||||
description={t(
|
||||
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to ${feeFactor}`
|
||||
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to %s`,
|
||||
[feeFactor]
|
||||
)}
|
||||
>
|
||||
<div>{t('Transfer fee')}</div>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
data-testid="transfer-fee"
|
||||
className="text-neutral-500 dark:text-neutral-300"
|
||||
>
|
||||
{formatNumber(fee, decimals)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-testid="transfer-fee"
|
||||
className="text-neutral-500 dark:text-neutral-300"
|
||||
>
|
||||
{value}
|
||||
<div className="flex justify-between gap-1 items-center flex-wrap">
|
||||
<Tooltip
|
||||
description={t(
|
||||
`The total amount to be transferred (without the fee)`
|
||||
)}
|
||||
>
|
||||
<div>{t('Amount to be transferred')}</div>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
data-testid="transfer-amount"
|
||||
className="text-neutral-500 dark:text-neutral-300"
|
||||
>
|
||||
{formatNumber(amount, decimals)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between gap-1 items-center flex-wrap">
|
||||
<Tooltip
|
||||
description={t(
|
||||
`The total amount taken from your account. The amount to be transferred plus the fee.`
|
||||
)}
|
||||
>
|
||||
<div>{t('Total amount (with fee)')}</div>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
data-testid="total-transfer-fee"
|
||||
className="text-neutral-500 dark:text-neutral-300"
|
||||
>
|
||||
{formatNumber(totalValue, decimals)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { defaultFallbackInView } from 'react-intersection-observer';
|
||||
|
||||
defaultFallbackInView(true);
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
fragment AssetListFields on Asset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
source {
|
||||
__typename
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
lifetimeLimit
|
||||
withdrawThreshold
|
||||
}
|
||||
}
|
||||
status
|
||||
}
|
||||
|
||||
query Assets {
|
||||
assetsConnection {
|
||||
edges {
|
||||
node {
|
||||
...AssetFields
|
||||
...AssetListFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-5
@@ -1,26 +1,44 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import { AssetFieldsFragmentDoc } from './Asset';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type AssetListFieldsFragment = { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string } };
|
||||
|
||||
export type AssetsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type AssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string }, infrastructureFeeAccount?: { __typename?: 'AccountBalance', balance: string } | null, globalRewardPoolAccount?: { __typename?: 'AccountBalance', balance: string } | null, takerFeeRewardAccount?: { __typename?: 'AccountBalance', balance: string } | null, makerFeeRewardAccount?: { __typename?: 'AccountBalance', balance: string } | null, lpFeeRewardAccount?: { __typename?: 'AccountBalance', balance: string } | null, marketProposerRewardAccount?: { __typename?: 'AccountBalance', balance: string } | null } } | null> | null } | null };
|
||||
|
||||
export type AssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string } } } | null> | null } | null };
|
||||
|
||||
export const AssetListFieldsFragmentDoc = gql`
|
||||
fragment AssetListFields on Asset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
source {
|
||||
__typename
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
lifetimeLimit
|
||||
withdrawThreshold
|
||||
}
|
||||
}
|
||||
status
|
||||
}
|
||||
`;
|
||||
export const AssetsDocument = gql`
|
||||
query Assets {
|
||||
assetsConnection {
|
||||
edges {
|
||||
node {
|
||||
...AssetFields
|
||||
...AssetListFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AssetFieldsFragmentDoc}`;
|
||||
${AssetListFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useAssetsQuery__
|
||||
|
||||
@@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { AssetDetailsDialog } from './asset-details-dialog';
|
||||
import { AssetDetail, testId } from './asset-details-table';
|
||||
import { AssetsDocument } from './__generated__/Assets';
|
||||
import { AssetDocument } from './__generated__/Asset';
|
||||
import { generateBuiltinAsset, generateERC20Asset } from './test-helpers';
|
||||
|
||||
const mockedData = {
|
||||
@@ -39,15 +39,17 @@ const mockedData = {
|
||||
},
|
||||
};
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: AssetsDocument,
|
||||
variables: {},
|
||||
},
|
||||
result: mockedData,
|
||||
const mocks = mockedData.data.assetsConnection.edges.map((mock) => ({
|
||||
request: {
|
||||
query: AssetDocument,
|
||||
variables: { assetId: mock.node.id },
|
||||
},
|
||||
];
|
||||
result: {
|
||||
data: {
|
||||
assetsConnection: { edges: [mock] },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const WrappedAssetDetailsDialog = ({ assetId }: { assetId: string }) => (
|
||||
<MockedProvider mocks={mocks}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useAssetsDataProvider } from './assets-data-provider';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
import { create } from 'zustand';
|
||||
import { AssetDetailsTable } from './asset-details-table';
|
||||
import { AssetProposalNotification } from '@vegaprotocol/proposals';
|
||||
import { useAssetDataProvider } from './asset-data-provider';
|
||||
|
||||
export type AssetDetailsDialogStore = {
|
||||
isOpen: boolean;
|
||||
@@ -55,9 +55,8 @@ export const AssetDetailsDialog = ({
|
||||
onChange,
|
||||
asJson = false,
|
||||
}: AssetDetailsDialogProps) => {
|
||||
const { data } = useAssetsDataProvider();
|
||||
const { data: asset } = useAssetDataProvider(assetId);
|
||||
|
||||
const asset = data?.find((a) => a.id === assetId);
|
||||
const assetSymbol = asset?.symbol || '';
|
||||
|
||||
const content = asset ? (
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { Asset } from './asset-data-provider';
|
||||
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from './constants';
|
||||
|
||||
type Rows = {
|
||||
key: AssetDetail;
|
||||
@@ -121,9 +122,7 @@ export const rows: Rows = [
|
||||
{
|
||||
key: AssetDetail.WITHDRAWAL_THRESHOLD,
|
||||
label: t('Withdrawal threshold'),
|
||||
tooltip: t(
|
||||
'The maximum you can withdraw instantly. There’s no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them'
|
||||
),
|
||||
tooltip: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
|
||||
value: (asset) =>
|
||||
num(asset, (asset.source as Schema.ERC20).withdrawThreshold),
|
||||
},
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import merge from 'lodash/merge';
|
||||
import type { AssetsQuery } from './__generated__/Assets';
|
||||
import type {
|
||||
AssetsQuery,
|
||||
AssetListFieldsFragment,
|
||||
} from './__generated__/Assets';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { AssetFieldsFragment } from './__generated__/Asset';
|
||||
|
||||
export const assetsQuery = (
|
||||
override?: PartialDeep<AssetsQuery>
|
||||
@@ -18,7 +20,7 @@ export const assetsQuery = (
|
||||
return merge(defaultAssets, override);
|
||||
};
|
||||
|
||||
const assetFields: AssetFieldsFragment[] = [
|
||||
const assetFields: AssetListFieldsFragment[] = [
|
||||
{
|
||||
__typename: 'Asset',
|
||||
id: 'asset-id',
|
||||
@@ -33,30 +35,6 @@ const assetFields: AssetFieldsFragment[] = [
|
||||
},
|
||||
quantum: '1',
|
||||
status: Types.AssetStatus.STATUS_ENABLED,
|
||||
infrastructureFeeAccount: {
|
||||
balance: '1',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
globalRewardPoolAccount: {
|
||||
balance: '2',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
takerFeeRewardAccount: {
|
||||
balance: '3',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
makerFeeRewardAccount: {
|
||||
balance: '4',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
lpFeeRewardAccount: {
|
||||
balance: '5',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
marketProposerRewardAccount: {
|
||||
balance: '6',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Asset',
|
||||
@@ -72,30 +50,6 @@ const assetFields: AssetFieldsFragment[] = [
|
||||
},
|
||||
quantum: '1',
|
||||
status: Types.AssetStatus.STATUS_ENABLED,
|
||||
infrastructureFeeAccount: {
|
||||
balance: '1',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
globalRewardPoolAccount: {
|
||||
balance: '2',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
takerFeeRewardAccount: {
|
||||
balance: '3',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
makerFeeRewardAccount: {
|
||||
balance: '4',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
lpFeeRewardAccount: {
|
||||
balance: '5',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
marketProposerRewardAccount: {
|
||||
balance: '6',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Asset',
|
||||
@@ -104,20 +58,10 @@ const assetFields: AssetFieldsFragment[] = [
|
||||
decimals: 5,
|
||||
name: 'Asto',
|
||||
source: {
|
||||
maxFaucetAmountMint: '5000000000',
|
||||
__typename: 'BuiltinAsset',
|
||||
},
|
||||
quantum: '1',
|
||||
status: Types.AssetStatus.STATUS_ENABLED,
|
||||
infrastructureFeeAccount: {
|
||||
balance: '0',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
globalRewardPoolAccount: null,
|
||||
takerFeeRewardAccount: null,
|
||||
makerFeeRewardAccount: null,
|
||||
lpFeeRewardAccount: null,
|
||||
marketProposerRewardAccount: null,
|
||||
},
|
||||
{
|
||||
__typename: 'Asset',
|
||||
@@ -126,20 +70,10 @@ const assetFields: AssetFieldsFragment[] = [
|
||||
decimals: 5,
|
||||
name: 'tBTC TEST',
|
||||
source: {
|
||||
maxFaucetAmountMint: '5000000000',
|
||||
__typename: 'BuiltinAsset',
|
||||
},
|
||||
quantum: '1',
|
||||
status: Types.AssetStatus.STATUS_ENABLED,
|
||||
infrastructureFeeAccount: {
|
||||
balance: '0',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
globalRewardPoolAccount: null,
|
||||
takerFeeRewardAccount: null,
|
||||
makerFeeRewardAccount: null,
|
||||
lpFeeRewardAccount: null,
|
||||
marketProposerRewardAccount: null,
|
||||
},
|
||||
// NOTE: These assets ids and contract addresses are real assets on Sepolia, this is needed
|
||||
// because we don't currently mock our seplia infura provider. If we change network these will
|
||||
@@ -158,30 +92,6 @@ const assetFields: AssetFieldsFragment[] = [
|
||||
__typename: 'ERC20',
|
||||
},
|
||||
quantum: '1',
|
||||
infrastructureFeeAccount: {
|
||||
balance: '1',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
globalRewardPoolAccount: {
|
||||
balance: '2',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
takerFeeRewardAccount: {
|
||||
balance: '3',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
makerFeeRewardAccount: {
|
||||
balance: '4',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
lpFeeRewardAccount: {
|
||||
balance: '5',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
marketProposerRewardAccount: {
|
||||
balance: '6',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'Asset',
|
||||
@@ -197,29 +107,5 @@ const assetFields: AssetFieldsFragment[] = [
|
||||
__typename: 'ERC20',
|
||||
},
|
||||
quantum: '1',
|
||||
infrastructureFeeAccount: {
|
||||
balance: '1',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
globalRewardPoolAccount: {
|
||||
balance: '2',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
takerFeeRewardAccount: {
|
||||
balance: '3',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
makerFeeRewardAccount: {
|
||||
balance: '4',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
lpFeeRewardAccount: {
|
||||
balance: '5',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
marketProposerRewardAccount: {
|
||||
balance: '6',
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
export const WITHDRAW_THRESHOLD_TOOLTIP_TEXT = t(
|
||||
"The maximum you can withdraw instantly. There's no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them"
|
||||
);
|
||||
@@ -5,3 +5,4 @@ export * from './assets-data-provider';
|
||||
export * from './asset-details-dialog';
|
||||
export * from './asset-details-table';
|
||||
export * from './asset-option';
|
||||
export * from './constants';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'pennant/dist/style.css';
|
||||
import {
|
||||
Chart,
|
||||
CandlestickChart,
|
||||
ChartType,
|
||||
Interval,
|
||||
Overlay,
|
||||
@@ -234,7 +234,7 @@ export const CandlesChartContainer = ({
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Chart
|
||||
<CandlestickChart
|
||||
dataSource={dataSource}
|
||||
options={{
|
||||
chartType: chartType,
|
||||
|
||||
@@ -52,6 +52,9 @@ export const checkSorting = (
|
||||
cy.get(`[col-id="${column}"]`).click();
|
||||
});
|
||||
checkSortChange(orderTabDesc, column);
|
||||
cy.get('.ag-header-container').within(() => {
|
||||
cy.get(`[col-id="${column}"]`).click();
|
||||
});
|
||||
};
|
||||
|
||||
const checkSortChange = (tabsArr: string[], column: string) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { memo } from 'react';
|
||||
import { BID_COLOR, ASK_COLOR } from './vol-cell';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
|
||||
import { NumericCell } from './numeric-cell';
|
||||
|
||||
export interface CumulativeVolProps {
|
||||
@@ -55,7 +55,7 @@ export const CumulativeVol = memo(
|
||||
(
|
||||
<NumericCell
|
||||
value={Number(indicativeVolume)}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
valueFormatted={addDecimalsFixedFormatNumber(
|
||||
indicativeVolume,
|
||||
positionDecimalPlaces ?? 0
|
||||
)}
|
||||
@@ -67,7 +67,7 @@ export const CumulativeVol = memo(
|
||||
{ask ? (
|
||||
<NumericCell
|
||||
value={ask}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
valueFormatted={addDecimalsFixedFormatNumber(
|
||||
ask,
|
||||
positionDecimalPlaces ?? 0
|
||||
)}
|
||||
@@ -77,7 +77,7 @@ export const CumulativeVol = memo(
|
||||
{bid ? (
|
||||
<NumericCell
|
||||
value={ask}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
valueFormatted={addDecimalsFixedFormatNumber(
|
||||
bid,
|
||||
positionDecimalPlaces ?? 0
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const MarketNameCell = ({
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (onMarketClick) {
|
||||
onMarketClick(id, ev.metaKey);
|
||||
onMarketClick(id, ev.metaKey || ev.ctrlKey);
|
||||
}
|
||||
},
|
||||
[id, onMarketClick]
|
||||
|
||||
@@ -78,7 +78,9 @@ export const compileGridData = (
|
||||
label: (
|
||||
<Link
|
||||
to={`/liquidity/${market.id}`}
|
||||
onClick={(ev) => onSelect && onSelect(market.id, ev.metaKey)}
|
||||
onClick={(ev) =>
|
||||
onSelect && onSelect(market.id, ev.metaKey || ev.ctrlKey)
|
||||
}
|
||||
>
|
||||
<UILink>{t('Current liquidity')}</UILink>
|
||||
</Link>
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
export * from './__generated__/EstimateOrder';
|
||||
export * from './use-calculate-slippage';
|
||||
export * from './use-fee-deal-ticket-details';
|
||||
export * from './use-market-positions';
|
||||
export * from './use-maximum-position-size';
|
||||
export * from './use-order-closeout';
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useCalculateSlippage } from './use-calculate-slippage';
|
||||
|
||||
const mockData = {
|
||||
decimalPlaces: 0,
|
||||
positionDecimalPlaces: 0,
|
||||
depth: {
|
||||
buy: [
|
||||
{
|
||||
price: '5',
|
||||
volume: '2',
|
||||
},
|
||||
{
|
||||
price: '4',
|
||||
volume: '3',
|
||||
},
|
||||
{
|
||||
price: '3',
|
||||
volume: '2',
|
||||
},
|
||||
{
|
||||
price: '2',
|
||||
volume: '1',
|
||||
},
|
||||
{
|
||||
price: '1',
|
||||
volume: '1',
|
||||
},
|
||||
],
|
||||
sell: [
|
||||
{
|
||||
price: '6',
|
||||
volume: '1',
|
||||
},
|
||||
{
|
||||
price: '7',
|
||||
volume: '3',
|
||||
},
|
||||
{
|
||||
price: '8',
|
||||
volume: '2',
|
||||
},
|
||||
{
|
||||
price: '9',
|
||||
volume: '1',
|
||||
},
|
||||
{
|
||||
price: '10',
|
||||
volume: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
let mockOrderBookData = {
|
||||
data: mockData,
|
||||
};
|
||||
|
||||
jest.mock('@vegaprotocol/react-helpers', () => ({
|
||||
...jest.requireActual('@vegaprotocol/react-helpers'),
|
||||
useDataProvider: jest.fn(() => ({
|
||||
data: {
|
||||
marketsConnection: [],
|
||||
},
|
||||
})),
|
||||
useThrottledDataProvider: jest.fn(() => mockOrderBookData),
|
||||
}));
|
||||
|
||||
describe('useCalculateSlippage Hook', () => {
|
||||
describe('calculate proper result', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const market = {
|
||||
id: 'marketId',
|
||||
decimalPlaces: 0,
|
||||
positionDecimalPlaces: 0,
|
||||
} as Market;
|
||||
|
||||
it('long order', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useCalculateSlippage({
|
||||
market,
|
||||
order: {
|
||||
size: '10',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
}),
|
||||
{
|
||||
wrapper: MockedProvider,
|
||||
}
|
||||
);
|
||||
expect(result.current).toEqual('33.33');
|
||||
});
|
||||
|
||||
it('short order', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useCalculateSlippage({
|
||||
market,
|
||||
order: {
|
||||
size: '10',
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
}),
|
||||
{
|
||||
wrapper: MockedProvider,
|
||||
}
|
||||
);
|
||||
expect(result.current).toEqual('31.11');
|
||||
});
|
||||
|
||||
it('when no order book result should be null', () => {
|
||||
mockOrderBookData = {
|
||||
data: {
|
||||
...mockData,
|
||||
depth: {
|
||||
...mockData.depth,
|
||||
buy: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useCalculateSlippage({
|
||||
market,
|
||||
order: {
|
||||
size: '10',
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
}),
|
||||
{
|
||||
wrapper: MockedProvider,
|
||||
}
|
||||
);
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { marketDepthProvider } from '@vegaprotocol/market-depth';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
|
||||
import { useThrottledDataProvider } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface Props {
|
||||
market: Market;
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
}
|
||||
|
||||
export const useCalculateSlippage = ({ market, order }: Props) => {
|
||||
const { data } = useThrottledDataProvider(
|
||||
{
|
||||
dataProvider: marketDepthProvider,
|
||||
variables: { marketId: market.id },
|
||||
},
|
||||
1000
|
||||
);
|
||||
const volPriceArr =
|
||||
data?.depth[order.side === Schema.Side.SIDE_BUY ? 'sell' : 'buy'] || [];
|
||||
if (volPriceArr.length && market) {
|
||||
const decimals = market.decimalPlaces ?? 0;
|
||||
const positionDecimals = market.positionDecimalPlaces ?? 0;
|
||||
const bestPrice = toBigNum(volPriceArr[0].price, decimals);
|
||||
const { size } = order;
|
||||
let descSize = new BigNumber(size);
|
||||
let i = 0;
|
||||
const volPricePairs: Array<[BigNumber, BigNumber]> = [];
|
||||
while (!descSize.isZero() && i < volPriceArr.length) {
|
||||
const price = toBigNum(volPriceArr[i].price, decimals);
|
||||
const amount = BigNumber.min(
|
||||
descSize,
|
||||
toBigNum(volPriceArr[i].volume, positionDecimals)
|
||||
);
|
||||
volPricePairs.push([price, amount]);
|
||||
descSize = BigNumber.max(0, descSize.minus(amount));
|
||||
i++;
|
||||
}
|
||||
if (volPricePairs.length) {
|
||||
const volWeightAvPricePair = volPricePairs.reduce(
|
||||
(agg, item) => {
|
||||
agg[0] = agg[0].plus(item[0].multipliedBy(item[1]));
|
||||
agg[1] = agg[1].plus(item[1]);
|
||||
return agg;
|
||||
},
|
||||
[new BigNumber(0), new BigNumber(0)]
|
||||
);
|
||||
const volWeightAvPrice = volWeightAvPricePair[0].dividedBy(
|
||||
volWeightAvPricePair[1]
|
||||
);
|
||||
const slippage = volWeightAvPrice
|
||||
.minus(bestPrice)
|
||||
.absoluteValue()
|
||||
.dividedBy(bestPrice)
|
||||
.multipliedBy(100);
|
||||
return formatNumber(slippage, 2);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
||||
TOTAL_MARGIN_AVAILABLE,
|
||||
} from '../constants';
|
||||
import { useOrderCloseOut } from './use-order-closeout';
|
||||
import { useMarketAccountBalance } from '@vegaprotocol/accounts';
|
||||
import { getDerivedPrice } from '../utils/get-price';
|
||||
import { useEstimateOrderQuery } from './__generated__/EstimateOrder';
|
||||
@@ -49,12 +48,6 @@ export const useFeeDealTicketDetails = (
|
||||
skip: !pubKey || !market || !order.size || !price,
|
||||
});
|
||||
|
||||
const estCloseOut = useOrderCloseOut({
|
||||
order,
|
||||
market,
|
||||
marketData,
|
||||
});
|
||||
|
||||
const notionalSize = useMemo(() => {
|
||||
if (price && order.size) {
|
||||
return toBigNum(order.size, market.positionDecimalPlaces)
|
||||
@@ -74,16 +67,8 @@ export const useFeeDealTicketDetails = (
|
||||
notionalSize,
|
||||
accountBalance,
|
||||
estimateOrder: estMargin?.estimateOrder,
|
||||
estCloseOut,
|
||||
};
|
||||
}, [
|
||||
market,
|
||||
assetSymbol,
|
||||
notionalSize,
|
||||
accountBalance,
|
||||
estMargin,
|
||||
estCloseOut,
|
||||
]);
|
||||
}, [market, assetSymbol, notionalSize, accountBalance, estMargin]);
|
||||
};
|
||||
|
||||
export interface FeeDetails {
|
||||
@@ -92,7 +77,6 @@ export interface FeeDetails {
|
||||
market: Market;
|
||||
assetSymbol: string;
|
||||
notionalSize: string | null;
|
||||
estCloseOut: string | null;
|
||||
estimateOrder: EstimateOrderQuery['estimateOrder'] | undefined;
|
||||
estimatedInitialMargin: string;
|
||||
estimatedTotalInitialMargin: string;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { useMarketPositions } from './use-market-positions';
|
||||
|
||||
jest.mock('@vegaprotocol/wallet', () => ({
|
||||
...jest.requireActual('@vegaprotocol/wallet'),
|
||||
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
|
||||
}));
|
||||
let mockMarketAccountBalance: {
|
||||
accountBalance: string;
|
||||
accountDecimals: number | null;
|
||||
} = { accountBalance: '50001000000', accountDecimals: 5 };
|
||||
jest.mock('@vegaprotocol/accounts', () => ({
|
||||
...jest.requireActual('@vegaprotocol/accounts'),
|
||||
useMarketAccountBalance: jest.fn(() => mockMarketAccountBalance),
|
||||
}));
|
||||
|
||||
jest.mock('@vegaprotocol/positions', () => ({
|
||||
...jest.requireActual('@vegaprotocol/positions'),
|
||||
useMarketPositionOpenVolume: jest.fn(() => '100002'),
|
||||
}));
|
||||
|
||||
describe('useOrderPosition Hook', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should return proper positive value', () => {
|
||||
const { result } = renderHook(
|
||||
() => useMarketPositions({ marketId: 'marketId' }),
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
expect(result.current?.openVolume).toEqual('100002');
|
||||
expect(result.current?.balance).toEqual('50001000000');
|
||||
});
|
||||
|
||||
it('if balance equal 0 return null', () => {
|
||||
mockMarketAccountBalance = { accountBalance: '0', accountDecimals: 5 };
|
||||
const { result } = renderHook(
|
||||
() => useMarketPositions({ marketId: 'marketId' }),
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
|
||||
it('if no markets return null', () => {
|
||||
mockMarketAccountBalance = { accountBalance: '', accountDecimals: null };
|
||||
const { result } = renderHook(
|
||||
() => useMarketPositions({ marketId: 'marketId' }),
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { useMarketAccountBalance } from '@vegaprotocol/accounts';
|
||||
import { useMarketPositionOpenVolume } from '@vegaprotocol/positions';
|
||||
|
||||
interface Props {
|
||||
marketId: string;
|
||||
}
|
||||
|
||||
export type PositionMargin = {
|
||||
openVolume: string;
|
||||
balance: string;
|
||||
balanceDecimals?: number;
|
||||
} | null;
|
||||
|
||||
export const useMarketPositions = ({ marketId }: Props): PositionMargin => {
|
||||
const { accountBalance, accountDecimals } = useMarketAccountBalance(marketId);
|
||||
const openVolume = useMarketPositionOpenVolume(marketId);
|
||||
|
||||
return useMemo(() => {
|
||||
if (accountBalance && accountDecimals) {
|
||||
const balance = new BigNumber(accountBalance);
|
||||
const volume = new BigNumber(openVolume);
|
||||
if (!balance.isZero() && !volume.isZero()) {
|
||||
return {
|
||||
balance: accountBalance,
|
||||
balanceDecimals: accountDecimals,
|
||||
openVolume,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [accountBalance, accountDecimals, openVolume]);
|
||||
};
|
||||
@@ -1,120 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import type { PositionMargin } from './use-market-positions';
|
||||
import { useMaximumPositionSize } from './use-maximum-position-size';
|
||||
|
||||
jest.mock('@vegaprotocol/wallet', () => ({
|
||||
...jest.requireActual('@vegaprotocol/wallet'),
|
||||
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
|
||||
}));
|
||||
|
||||
let mockAccountBalance: {
|
||||
accountBalance: string;
|
||||
accountDecimals: number | null;
|
||||
} = { accountBalance: '200000', accountDecimals: 5 };
|
||||
jest.mock('@vegaprotocol/accounts', () => ({
|
||||
...jest.requireActual('@vegaprotocol/accounts'),
|
||||
useAccountBalance: jest.fn(() => mockAccountBalance),
|
||||
}));
|
||||
|
||||
const defaultMockMarketPositions = {
|
||||
openVolume: '1',
|
||||
balance: '100000',
|
||||
};
|
||||
|
||||
let mockMarketPositions: PositionMargin | null = defaultMockMarketPositions;
|
||||
|
||||
const mockOrder: OrderSubmissionBody['orderSubmission'] = {
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
size: '1',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||
marketId: 'market-id',
|
||||
};
|
||||
|
||||
jest.mock('./use-market-positions', () => ({
|
||||
useMarketPositions: ({
|
||||
marketId,
|
||||
partyId,
|
||||
}: {
|
||||
marketId: string;
|
||||
partyId: string;
|
||||
}) => mockMarketPositions,
|
||||
}));
|
||||
|
||||
describe('useMaximumPositionSize', () => {
|
||||
it('should return correct size when no open positions', () => {
|
||||
mockMarketPositions = null;
|
||||
const price = '50';
|
||||
const expected = 4000;
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useMaximumPositionSize({
|
||||
marketId: '',
|
||||
price,
|
||||
settlementAssetId: '',
|
||||
order: mockOrder,
|
||||
}),
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
expect(result.current).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return correct size when open positions and same side', () => {
|
||||
const price = '50';
|
||||
mockMarketPositions = defaultMockMarketPositions;
|
||||
const expected = 3999;
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useMaximumPositionSize({
|
||||
marketId: '',
|
||||
price,
|
||||
settlementAssetId: '',
|
||||
order: mockOrder,
|
||||
}),
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
expect(result.current).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return correct size when open positions and opposite side', () => {
|
||||
const price = '50';
|
||||
mockOrder.side = Schema.Side.SIDE_SELL;
|
||||
mockMarketPositions = defaultMockMarketPositions;
|
||||
const expected = 4001;
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useMaximumPositionSize({
|
||||
marketId: '',
|
||||
price,
|
||||
settlementAssetId: '',
|
||||
order: mockOrder,
|
||||
}),
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
expect(result.current).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return zero if no account balance', () => {
|
||||
mockAccountBalance = {
|
||||
accountBalance: '0',
|
||||
accountDecimals: 5,
|
||||
};
|
||||
const price = '50';
|
||||
mockMarketPositions = defaultMockMarketPositions;
|
||||
const expected = 0;
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useMaximumPositionSize({
|
||||
marketId: '',
|
||||
price,
|
||||
settlementAssetId: '',
|
||||
order: mockOrder,
|
||||
}),
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
expect(result.current).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useAccountBalance } from '@vegaprotocol/accounts';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { useMarketPositions } from './use-market-positions';
|
||||
|
||||
interface Props {
|
||||
marketId: string;
|
||||
price?: string;
|
||||
settlementAssetId: string;
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
}
|
||||
|
||||
const getSize = (balance: string, price: string) =>
|
||||
new BigNumber(balance).dividedBy(new BigNumber(price));
|
||||
|
||||
export const useMaximumPositionSize = ({
|
||||
marketId,
|
||||
price,
|
||||
settlementAssetId,
|
||||
order,
|
||||
}: Props): number => {
|
||||
const { accountBalance } = useAccountBalance(settlementAssetId) || {};
|
||||
const marketPositions = useMarketPositions({ marketId: marketId });
|
||||
if (!accountBalance || new BigNumber(accountBalance || 0).isZero()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const size = getSize(accountBalance, price || '');
|
||||
|
||||
if (!marketPositions) {
|
||||
return size.toNumber() || 0;
|
||||
}
|
||||
|
||||
const isSameSide =
|
||||
(new BigNumber(marketPositions.openVolume).isPositive() &&
|
||||
order.side === Schema.Side.SIDE_BUY) ||
|
||||
(new BigNumber(marketPositions.openVolume).isNegative() &&
|
||||
order.side === Schema.Side.SIDE_SELL);
|
||||
|
||||
const adjustedForVolume = new BigNumber(size)[isSameSide ? 'minus' : 'plus'](
|
||||
marketPositions.openVolume
|
||||
);
|
||||
|
||||
return adjustedForVolume.isNegative() ? 0 : adjustedForVolume.toNumber();
|
||||
};
|
||||
@@ -1,117 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
import { useOrderCloseOut } from './use-order-closeout';
|
||||
|
||||
jest.mock('@vegaprotocol/wallet', () => ({
|
||||
...jest.requireActual('@vegaprotocol/wallet'),
|
||||
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
|
||||
}));
|
||||
let mockMarketMargin: string | undefined = undefined;
|
||||
jest.mock('@vegaprotocol/positions', () => ({
|
||||
...jest.requireActual('@vegaprotocol/positions'),
|
||||
useMarketMargin: () => mockMarketMargin,
|
||||
}));
|
||||
|
||||
describe('useOrderCloseOut', () => {
|
||||
const order = { size: '2', side: 'SIDE_BUY' };
|
||||
const market = {
|
||||
decimalPlaces: 5,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
settlementAsset: {
|
||||
id: 'assetId',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as Market;
|
||||
|
||||
const marketData = {
|
||||
markPrice: 100000,
|
||||
} as unknown as MarketData;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return proper null value', () => {
|
||||
mockMarketMargin = '-1';
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useOrderCloseOut({
|
||||
order: order as OrderSubmissionBody['orderSubmission'],
|
||||
market,
|
||||
marketData: {
|
||||
markPrice: '0',
|
||||
} as MarketData,
|
||||
}),
|
||||
{
|
||||
wrapper: MockedProvider,
|
||||
}
|
||||
);
|
||||
expect(result.current).toEqual(null);
|
||||
});
|
||||
|
||||
it('should return proper sell value', () => {
|
||||
mockMarketMargin = '0';
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useOrderCloseOut({
|
||||
order: {
|
||||
...order,
|
||||
side: 'SIDE_SELL',
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
market,
|
||||
marketData,
|
||||
}),
|
||||
{
|
||||
wrapper: MockedProvider,
|
||||
}
|
||||
);
|
||||
expect(result.current).toEqual('1');
|
||||
});
|
||||
|
||||
it('should return proper sell value on limit order', () => {
|
||||
mockMarketMargin = '0';
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useOrderCloseOut({
|
||||
order: {
|
||||
...order,
|
||||
price: '1000000',
|
||||
type: 'TYPE_LIMIT',
|
||||
side: 'SIDE_SELL',
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
market,
|
||||
marketData,
|
||||
}),
|
||||
{
|
||||
wrapper: MockedProvider,
|
||||
}
|
||||
);
|
||||
expect(result.current).toEqual('1000000');
|
||||
});
|
||||
|
||||
it('should return proper empty value', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useOrderCloseOut({
|
||||
order: {
|
||||
...order,
|
||||
side: 'SIDE_SELL',
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
market,
|
||||
marketData: {
|
||||
markPrice: '0',
|
||||
} as MarketData,
|
||||
}),
|
||||
{
|
||||
wrapper: MockedProvider,
|
||||
}
|
||||
);
|
||||
expect(result.current).toEqual('0');
|
||||
});
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { addDecimal } from '@vegaprotocol/utils';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
import {
|
||||
useAccountBalance,
|
||||
useMarketAccountBalance,
|
||||
} from '@vegaprotocol/accounts';
|
||||
import { useMarketMargin } from '@vegaprotocol/positions';
|
||||
import { useMarketPositions } from './use-market-positions';
|
||||
|
||||
interface Props {
|
||||
order: OrderSubmissionBody['orderSubmission'];
|
||||
market: Market;
|
||||
marketData: MarketData;
|
||||
}
|
||||
|
||||
export const useOrderCloseOut = ({
|
||||
order,
|
||||
market,
|
||||
marketData,
|
||||
}: Props): string | null => {
|
||||
const { accountBalance, accountDecimals } = useAccountBalance(
|
||||
market.tradableInstrument.instrument.product.settlementAsset.id
|
||||
);
|
||||
const { accountBalance: positionBalance, accountDecimals: positionDecimals } =
|
||||
useMarketAccountBalance(market.id);
|
||||
const maintenanceLevel = useMarketMargin(market.id);
|
||||
|
||||
const marginMaintenanceLevel = new BigNumber(
|
||||
addDecimal(maintenanceLevel || 0, market.decimalPlaces)
|
||||
);
|
||||
const positionAccountBalance = new BigNumber(
|
||||
addDecimal(positionBalance || 0, positionDecimals || 0)
|
||||
);
|
||||
const generalAccountBalance = new BigNumber(
|
||||
addDecimal(accountBalance || 0, accountDecimals || 0)
|
||||
);
|
||||
const { openVolume } =
|
||||
useMarketPositions({
|
||||
marketId: market.id,
|
||||
}) || {};
|
||||
|
||||
const volume = new BigNumber(
|
||||
addDecimal(openVolume || '0', market.positionDecimalPlaces)
|
||||
)[order.side === Schema.Side.SIDE_BUY ? 'plus' : 'minus'](order.size);
|
||||
const price =
|
||||
order.type === Schema.OrderType.TYPE_LIMIT && order.price
|
||||
? new BigNumber(order.price)
|
||||
: new BigNumber(addDecimal(marketData.markPrice, market.decimalPlaces));
|
||||
// regarding formula (marginMaintenanceLevel - positionAccountBalance - generalAccountBalance) / volume + markPrice
|
||||
const marginDifference = marginMaintenanceLevel
|
||||
.minus(positionAccountBalance)
|
||||
.minus(generalAccountBalance);
|
||||
const closeOut = marginDifference.div(volume).plus(price);
|
||||
if (closeOut.isPositive()) {
|
||||
return closeOut.toString();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -51,11 +51,12 @@ export const ApproveNotification = ({
|
||||
intent={intent}
|
||||
testId="approve-default"
|
||||
message={t(
|
||||
`Before you can make a deposit of your chosen asset, ${selectedAsset?.symbol}, you need to approve its use in your Ethereum wallet`
|
||||
'Before you can make a deposit of your chosen asset, %s, you need to approve its use in your Ethereum wallet',
|
||||
selectedAsset?.symbol
|
||||
)}
|
||||
buttonProps={{
|
||||
size: 'sm',
|
||||
text: `Approve ${selectedAsset?.symbol}`,
|
||||
text: t('Approve %s', selectedAsset?.symbol),
|
||||
action: onApprove,
|
||||
dataTestId: 'approve-submit',
|
||||
}}
|
||||
@@ -68,13 +69,12 @@ export const ApproveNotification = ({
|
||||
intent={intent}
|
||||
testId="reapprove-default"
|
||||
message={t(
|
||||
`Approve again to deposit more than ${formatNumber(
|
||||
balances.allowance.toString()
|
||||
)}`
|
||||
'Approve again to deposit more than %s',
|
||||
formatNumber(balances.allowance.toString())
|
||||
)}
|
||||
buttonProps={{
|
||||
size: 'sm',
|
||||
text: `Approve ${selectedAsset?.symbol}`,
|
||||
text: t('Approve %s', selectedAsset?.symbol),
|
||||
action: onApprove,
|
||||
dataTestId: 'reapprove-submit',
|
||||
}}
|
||||
@@ -157,7 +157,8 @@ const ApprovalTxFeedback = ({
|
||||
intent={Intent.Warning}
|
||||
testId="approve-requested"
|
||||
message={t(
|
||||
`Go to your Ethereum wallet and approve the transaction to enable the use of ${selectedAsset?.symbol}`
|
||||
'Go to your Ethereum wallet and approve the transaction to enable the use of %s',
|
||||
selectedAsset?.symbol
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -174,7 +175,8 @@ const ApprovalTxFeedback = ({
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
`Your ${selectedAsset?.symbol} is being confirmed by the Ethereum network. When this is complete, you can continue your deposit`
|
||||
'Your %s approval is being confirmed by the Ethereum network. When this is complete, you can continue your deposit',
|
||||
selectedAsset?.symbol
|
||||
)}{' '}
|
||||
</p>
|
||||
{txLink && <p>{txLink}</p>}
|
||||
@@ -194,13 +196,10 @@ const ApprovalTxFeedback = ({
|
||||
message={
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
`You can now make deposits in ${
|
||||
selectedAsset?.symbol
|
||||
}, up to a maximum of ${formatNumber(
|
||||
allowance?.toString() || 0
|
||||
)}`
|
||||
)}
|
||||
{t('You approved deposits of up to %s %s.', [
|
||||
selectedAsset?.symbol,
|
||||
formatNumber(allowance?.toString() || 0),
|
||||
])}
|
||||
</p>
|
||||
{txLink && <p>{txLink}</p>}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { waitFor, fireEvent, render, screen } from '@testing-library/react';
|
||||
import {
|
||||
waitFor,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
act,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { DepositFormProps } from './deposit-form';
|
||||
import { DepositForm } from './deposit-form';
|
||||
@@ -61,6 +68,7 @@ beforeEach(() => {
|
||||
submitDeposit: jest.fn(),
|
||||
submitFaucet: jest.fn(),
|
||||
onDisconnect: jest.fn(),
|
||||
handleAmountChange: jest.fn(),
|
||||
approveTxId: null,
|
||||
faucetTxId: null,
|
||||
isFaucetable: true,
|
||||
@@ -140,12 +148,14 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Insufficient amount in Ethereum wallet')
|
||||
await screen.findByText(
|
||||
"You can't deposit more than you have in your Ethereum wallet, 5"
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('fails when submitted amount is more than the maximum limit', async () => {
|
||||
render(<DepositForm {...props} />);
|
||||
render(<DepositForm {...props} selectedAsset={asset} />);
|
||||
|
||||
const amountMoreThanLimit = '21';
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
@@ -154,7 +164,9 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Amount is above lifetime deposit limit')
|
||||
await screen.findByText(
|
||||
"You can't deposit more than your remaining deposit allowance, 10 asset-symbol"
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -178,7 +190,9 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Amount is above approved amount')
|
||||
await screen.findByText(
|
||||
"You can't deposit more than your approved deposit amount, 30"
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -282,8 +296,6 @@ describe('Deposit form', () => {
|
||||
expect(screen.getByTestId('BALANCE_AVAILABLE_value')).toHaveTextContent(
|
||||
'50'
|
||||
);
|
||||
expect(screen.getByTestId('MAX_LIMIT_value')).toHaveTextContent('20');
|
||||
expect(screen.getByTestId('DEPOSITED_value')).toHaveTextContent('10');
|
||||
expect(screen.getByTestId('REMAINING_value')).toHaveTextContent('10');
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
@@ -364,4 +376,32 @@ describe('Deposit form', () => {
|
||||
/this app only works on/i
|
||||
);
|
||||
});
|
||||
|
||||
it('Remaining deposit allowance tooltip should be rendered', async () => {
|
||||
render(<DepositForm {...props} selectedAsset={asset} />);
|
||||
await act(async () => {
|
||||
await userEvent.hover(screen.getByText('Remaining deposit allowance'));
|
||||
});
|
||||
await waitFor(async () => {
|
||||
await expect(
|
||||
screen.getByRole('tooltip', {
|
||||
name: /VEGA has a lifetime deposit limit of 20 asset-symbol per address/,
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Ethereum deposit cap tooltip should be rendered', async () => {
|
||||
render(<DepositForm {...props} selectedAsset={asset} />);
|
||||
await act(async () => {
|
||||
await userEvent.hover(screen.getByText('Ethereum deposit cap'));
|
||||
});
|
||||
await waitFor(async () => {
|
||||
await expect(
|
||||
screen.getByRole('tooltip', {
|
||||
name: /The deposit cap is set when you approve an asset for use with this app/,
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
maxSafe,
|
||||
addDecimal,
|
||||
isAssetTypeERC20,
|
||||
formatNumber,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useLocalStorage } from '@vegaprotocol/react-helpers';
|
||||
@@ -25,11 +26,9 @@ import {
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import type { ButtonHTMLAttributes, ChangeEvent, ReactNode } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useWatch, Controller, useForm } from 'react-hook-form';
|
||||
import { DepositLimits } from './deposit-limits';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import {
|
||||
@@ -40,6 +39,7 @@ import {
|
||||
import type { DepositBalances } from './use-deposit-balances';
|
||||
import { FaucetNotification } from './faucet-notification';
|
||||
import { ApproveNotification } from './approve-notification';
|
||||
import { usePersistentDeposit } from './use-persistent-deposit';
|
||||
|
||||
interface FormFields {
|
||||
asset: string;
|
||||
@@ -53,6 +53,7 @@ export interface DepositFormProps {
|
||||
selectedAsset?: Asset;
|
||||
balances: DepositBalances | null;
|
||||
onSelectAsset: (assetId: string) => void;
|
||||
handleAmountChange: (amount: string) => void;
|
||||
onDisconnect: () => void;
|
||||
submitApprove: () => void;
|
||||
approveTxId: number | null;
|
||||
@@ -71,6 +72,7 @@ export const DepositForm = ({
|
||||
selectedAsset,
|
||||
balances,
|
||||
onSelectAsset,
|
||||
handleAmountChange,
|
||||
onDisconnect,
|
||||
submitApprove,
|
||||
submitDeposit,
|
||||
@@ -85,6 +87,8 @@ export const DepositForm = ({
|
||||
const { pubKey, pubKeys: _pubKeys } = useVegaWallet();
|
||||
const [approveNotificationIntent, setApproveNotificationIntent] =
|
||||
useState<Intent>(Intent.Warning);
|
||||
const [persistedDeposit] = usePersistentDeposit(selectedAsset?.id);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -95,7 +99,8 @@ export const DepositForm = ({
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
to: pubKey ? pubKey : undefined,
|
||||
asset: selectedAsset?.id || '',
|
||||
asset: selectedAsset?.id,
|
||||
amount: persistedDeposit?.amount,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -129,11 +134,8 @@ export const DepositForm = ({
|
||||
return _pubKeys ? _pubKeys.map((pk) => pk.publicKey) : [];
|
||||
}, [_pubKeys]);
|
||||
|
||||
const approved = balances
|
||||
? balances.allowance.isGreaterThan(0)
|
||||
? true
|
||||
: false
|
||||
: false;
|
||||
const approved =
|
||||
balances && balances.allowance.isGreaterThan(0) ? true : false;
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -190,6 +192,44 @@ export const DepositForm = ({
|
||||
<InputError intent="danger">{errors.from.message}</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup label={t('To (Vega key)')} labelFor="to">
|
||||
<AddressField
|
||||
pubKeys={pubKeys}
|
||||
onChange={() => setValue('to', '')}
|
||||
select={
|
||||
<Select {...register('to')} id="to" defaultValue="">
|
||||
<option value="" disabled>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.length &&
|
||||
pubKeys.map((pk) => (
|
||||
<option key={pk} value={pk}>
|
||||
{pk}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
}
|
||||
input={
|
||||
<Input
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="to"
|
||||
type="text"
|
||||
{...register('to', {
|
||||
validate: {
|
||||
required,
|
||||
vegaPublicKey,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{errors.to?.message && (
|
||||
<InputError intent="danger" forInput="to">
|
||||
{errors.to.message}
|
||||
</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup label={t('Asset')} labelFor="asset">
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -246,45 +286,7 @@ export const DepositForm = ({
|
||||
selectedAsset={selectedAsset}
|
||||
faucetTxId={faucetTxId}
|
||||
/>
|
||||
<FormGroup label={t('To (Vega key)')} labelFor="to">
|
||||
<AddressField
|
||||
pubKeys={pubKeys}
|
||||
onChange={() => setValue('to', '')}
|
||||
select={
|
||||
<Select {...register('to')} id="to" defaultValue="">
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.length &&
|
||||
pubKeys.map((pk) => (
|
||||
<option key={pk} value={pk}>
|
||||
{pk}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
}
|
||||
input={
|
||||
<Input
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="to"
|
||||
type="text"
|
||||
{...register('to', {
|
||||
validate: {
|
||||
required,
|
||||
vegaPublicKey,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{errors.to?.message && (
|
||||
<InputError intent="danger" forInput="to">
|
||||
{errors.to.message}
|
||||
</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
{selectedAsset && balances && (
|
||||
{approved && selectedAsset && balances && (
|
||||
<div className="mb-6">
|
||||
<DepositLimits {...balances} asset={selectedAsset} />
|
||||
</div>
|
||||
@@ -301,8 +303,15 @@ export const DepositForm = ({
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
approved: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(balances?.allowance || 0)) {
|
||||
return t('Amount is above approved amount');
|
||||
const allowance = new BigNumber(balances?.allowance || 0);
|
||||
if (value.isGreaterThan(allowance)) {
|
||||
return t(
|
||||
"You can't deposit more than your approved deposit amount, %s %s",
|
||||
[
|
||||
formatNumber(allowance.toString()),
|
||||
selectedAsset?.symbol || ' ',
|
||||
]
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@@ -318,14 +327,24 @@ export const DepositForm = ({
|
||||
}
|
||||
|
||||
if (value.isGreaterThan(lifetimeLimit)) {
|
||||
return t('Amount is above lifetime deposit limit');
|
||||
return t(
|
||||
"You can't deposit more than your remaining deposit allowance, %s %s",
|
||||
[
|
||||
formatNumber(lifetimeLimit.toString()),
|
||||
selectedAsset?.symbol || ' ',
|
||||
]
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
balance: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(balances?.balance || 0)) {
|
||||
return t('Insufficient amount in Ethereum wallet');
|
||||
const balance = new BigNumber(balances?.balance || 0);
|
||||
if (value.isGreaterThan(balance)) {
|
||||
return t(
|
||||
"You can't deposit more than you have in your Ethereum wallet, %s %s",
|
||||
[formatNumber(balance), selectedAsset?.symbol || ' ']
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@@ -333,6 +352,9 @@ export const DepositForm = ({
|
||||
return maxSafe(balances?.balance || new BigNumber(0))(v);
|
||||
},
|
||||
},
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||
handleAmountChange(e.target.value || '');
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
@@ -343,10 +365,9 @@ export const DepositForm = ({
|
||||
{selectedAsset && balances && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue(
|
||||
'amount',
|
||||
balances.balance.toFixed(selectedAsset.decimals)
|
||||
);
|
||||
const amount = balances.balance.toFixed(selectedAsset.decimals);
|
||||
setValue('amount', amount);
|
||||
handleAmountChange(amount);
|
||||
clearErrors('amount');
|
||||
}}
|
||||
>
|
||||
@@ -399,7 +420,7 @@ const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
|
||||
type="submit"
|
||||
data-testid="deposit-submit"
|
||||
variant={isActive ? 'primary' : 'default'}
|
||||
fill={true}
|
||||
fill
|
||||
disabled={invalidChain}
|
||||
>
|
||||
{t('Deposit')}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { CompactNumber } from '@vegaprotocol/react-helpers';
|
||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type BigNumber from 'bignumber.js';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
|
||||
// Note: all of the values here are with correct asset's decimals
|
||||
// See `libs/deposits/src/lib/use-deposit-balances.ts`
|
||||
@@ -33,21 +38,35 @@ export const DepositLimits = ({
|
||||
'-'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'MAX_LIMIT',
|
||||
label: t('Lifetime deposit allowance'),
|
||||
rawValue: max,
|
||||
value: <CompactNumber number={max} decimals={asset.decimals} />,
|
||||
},
|
||||
{
|
||||
key: 'DEPOSITED',
|
||||
label: t('Deposited'),
|
||||
rawValue: deposited,
|
||||
value: <CompactNumber number={deposited} decimals={asset.decimals} />,
|
||||
},
|
||||
{
|
||||
key: 'REMAINING',
|
||||
label: t('Remaining'),
|
||||
label: (
|
||||
<Tooltip
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
'VEGA has a lifetime deposit limit of %s %s per address. This can be changed through governance',
|
||||
[formatNumber(max.toString()), asset.symbol]
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
'To date, %s %s has been deposited from this Ethereum address, so you can deposit up to %s %s more.',
|
||||
[
|
||||
formatNumber(deposited.toString()),
|
||||
asset.symbol,
|
||||
formatNumber(max.minus(deposited).toString()),
|
||||
asset.symbol,
|
||||
]
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<button type="button">{t('Remaining deposit allowance')}</button>
|
||||
</Tooltip>
|
||||
),
|
||||
rawValue: max.minus(deposited),
|
||||
value: (
|
||||
<CompactNumber
|
||||
@@ -58,7 +77,20 @@ export const DepositLimits = ({
|
||||
},
|
||||
{
|
||||
key: 'ALLOWANCE',
|
||||
label: t('Approved'),
|
||||
label: (
|
||||
<Tooltip
|
||||
description={
|
||||
<p>
|
||||
{t(
|
||||
'The deposit cap is set when you approve an asset for use with this app. To increase this cap, approve %s again and choose a higher cap. Check the documentation for your Ethereum wallet app for details.',
|
||||
asset.symbol
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<button type="button">{t('Ethereum deposit cap')}</button>
|
||||
</Tooltip>
|
||||
),
|
||||
rawValue: allowance,
|
||||
value: allowance ? (
|
||||
<CompactNumber number={allowance} decimals={asset.decimals} />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { prepend0x } from '@vegaprotocol/smart-contracts';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { useSubmitApproval } from './use-submit-approval';
|
||||
import { useSubmitFaucet } from './use-submit-faucet';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useDepositBalances } from './use-deposit-balances';
|
||||
import { useDepositDialog } from './deposit-dialog';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
useBridgeContract,
|
||||
useEthereumConfig,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { usePersistentDeposit } from './use-persistent-deposit';
|
||||
|
||||
interface DepositManagerProps {
|
||||
assetId?: string;
|
||||
@@ -28,7 +29,9 @@ export const DepositManager = ({
|
||||
}: DepositManagerProps) => {
|
||||
const createEthTransaction = useEthTransactionStore((state) => state.create);
|
||||
const { config } = useEthereumConfig();
|
||||
const [assetId, setAssetId] = useState(initialAssetId);
|
||||
const [persistentDeposit, savePersistentDeposit] =
|
||||
usePersistentDeposit(initialAssetId);
|
||||
const [assetId, setAssetId] = useState(persistentDeposit?.assetId);
|
||||
const asset = assets.find((a) => a.id === assetId);
|
||||
const bridgeContract = useBridgeContract();
|
||||
const closeDepositDialog = useDepositDialog((state) => state.close);
|
||||
@@ -65,17 +68,27 @@ export const DepositManager = ({
|
||||
closeDepositDialog();
|
||||
};
|
||||
|
||||
const onAmountChange = useCallback(
|
||||
(amount: string) => {
|
||||
persistentDeposit &&
|
||||
savePersistentDeposit({ ...persistentDeposit, amount });
|
||||
},
|
||||
[savePersistentDeposit, persistentDeposit]
|
||||
);
|
||||
|
||||
return (
|
||||
<DepositForm
|
||||
selectedAsset={asset}
|
||||
onDisconnect={reset}
|
||||
onSelectAsset={(id) => {
|
||||
setAssetId(id);
|
||||
savePersistentDeposit({ assetId: id });
|
||||
// When we change asset, also clear the tracked faucet/approve transactions so
|
||||
// we dont render stale UI
|
||||
approve.reset();
|
||||
faucet.reset();
|
||||
}}
|
||||
handleAmountChange={onAmountChange}
|
||||
assets={sortBy(assets, 'name')}
|
||||
submitApprove={approve.perform}
|
||||
submitDeposit={submitDeposit}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { usePersistentDeposit } from './use-persistent-deposit';
|
||||
|
||||
describe('usePersistenDeposit', () => {
|
||||
it('should return empty data', () => {
|
||||
const { result } = renderHook(() => usePersistentDeposit());
|
||||
expect(result.current).toEqual([undefined, expect.any(Function)]);
|
||||
});
|
||||
it('should return empty and properly saved data', async () => {
|
||||
const aId = 'test';
|
||||
const retObj = { assetId: 'test', amount: '1.00000' };
|
||||
const { result } = renderHook(() => usePersistentDeposit(aId));
|
||||
expect(result.current).toEqual([{ assetId: 'test' }, expect.any(Function)]);
|
||||
await act(() => {
|
||||
result.current[1](retObj);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toEqual(retObj);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useMemo } from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
|
||||
const STORAGE_KEY = 'vega_deposit_store';
|
||||
interface PersistedDeposit {
|
||||
assetId: string;
|
||||
amount?: string;
|
||||
}
|
||||
type PersistedDepositData = Record<string, PersistedDeposit>;
|
||||
|
||||
const usePersistentDepositStore = create<{
|
||||
deposits: PersistedDepositData;
|
||||
saveValue: (entry: PersistedDeposit) => void;
|
||||
lastVisited?: PersistedDeposit;
|
||||
}>()(
|
||||
persist(
|
||||
immer((set) => ({
|
||||
deposits: {},
|
||||
saveValue: (entry) =>
|
||||
set((state) => {
|
||||
const oldValue = state.deposits[entry.assetId] || null;
|
||||
state.deposits[entry.assetId] = { ...oldValue, ...entry };
|
||||
state.lastVisited = { ...oldValue, ...entry };
|
||||
return state;
|
||||
}),
|
||||
})),
|
||||
{ name: STORAGE_KEY }
|
||||
)
|
||||
);
|
||||
|
||||
export const usePersistentDeposit = (
|
||||
assetId?: string
|
||||
): [PersistedDeposit | undefined, (entry: PersistedDeposit) => void] => {
|
||||
const { deposits, lastVisited, saveValue } = usePersistentDepositStore();
|
||||
const discoveredData = useMemo(() => {
|
||||
return assetId
|
||||
? deposits[assetId]
|
||||
? deposits[assetId]
|
||||
: { assetId }
|
||||
: lastVisited;
|
||||
}, [deposits, lastVisited, assetId]);
|
||||
|
||||
return [discoveredData, saveValue];
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isAssetTypeERC20, removeDecimal } from '@vegaprotocol/utils';
|
||||
import { MaxUint256 } from '@ethersproject/constants';
|
||||
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
|
||||
import {
|
||||
EthTxStatus,
|
||||
useEthereumConfig,
|
||||
@@ -37,10 +38,9 @@ export const useSubmitApproval = (
|
||||
},
|
||||
perform: () => {
|
||||
if (!asset || !config) return;
|
||||
const amount = removeDecimal('1000000', asset.decimals);
|
||||
const id = createEthTransaction(contract, 'approve', [
|
||||
config?.collateral_bridge_contract.address,
|
||||
amount,
|
||||
MaxUint256.toString(),
|
||||
]);
|
||||
setId(id);
|
||||
},
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MockedResponse } from '@apollo/react-testing';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import { RadioGroup } from '@vegaprotocol/ui-toolkit';
|
||||
import type {
|
||||
BlockTimeSubscription,
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
import { BlockTimeDocument } from '../../utils/__generated__/Node';
|
||||
import { StatisticsDocument } from '../../utils/__generated__/Node';
|
||||
import type { RowDataProps } from './row-data';
|
||||
import { POLL_INTERVAL } from './row-data';
|
||||
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
||||
import type { HeaderEntry } from '@vegaprotocol/apollo-client';
|
||||
import { useHeaderStore } from '@vegaprotocol/apollo-client';
|
||||
@@ -25,7 +26,7 @@ const statsQueryMock: MockedResponse<StatisticsQuery> = {
|
||||
result: {
|
||||
data: {
|
||||
statistics: {
|
||||
blockHeight: '1234',
|
||||
blockHeight: '1234', // the actual value used in the component is the value from the header store
|
||||
vegaTime: new Date().toISOString(),
|
||||
chainId: 'test-chain-id',
|
||||
},
|
||||
@@ -251,4 +252,105 @@ describe('RowData', () => {
|
||||
|
||||
expect(mockOnBlockHeight).toHaveBeenCalledWith(blockHeight);
|
||||
});
|
||||
|
||||
it('should poll the query unless an errors is returned', async () => {
|
||||
jest.useFakeTimers();
|
||||
const createStatsQueryMock = (
|
||||
blockHeight: string
|
||||
): MockedResponse<StatisticsQuery> => {
|
||||
return {
|
||||
request: {
|
||||
query: StatisticsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
statistics: {
|
||||
blockHeight,
|
||||
vegaTime: new Date().toISOString(),
|
||||
chainId: 'test-chain-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createFailedStatsQueryMock = (): MockedResponse<StatisticsQuery> => {
|
||||
return {
|
||||
request: {
|
||||
query: StatisticsDocument,
|
||||
},
|
||||
result: {
|
||||
data: undefined,
|
||||
},
|
||||
error: new Error('failed'),
|
||||
};
|
||||
};
|
||||
|
||||
mockHeaders(props.url);
|
||||
const statsQueryMock1 = createStatsQueryMock('1234');
|
||||
const statsQueryMock2 = createStatsQueryMock('1235');
|
||||
const statsQueryMock3 = createFailedStatsQueryMock();
|
||||
const statsQueryMock4 = createStatsQueryMock('1236');
|
||||
render(
|
||||
<MockedProvider
|
||||
mocks={[
|
||||
statsQueryMock1,
|
||||
statsQueryMock2,
|
||||
statsQueryMock3,
|
||||
statsQueryMock4,
|
||||
subMock,
|
||||
]}
|
||||
>
|
||||
<RadioGroup>
|
||||
{/* Radio group required as radio is being render in isolation */}
|
||||
<RowData {...props} />
|
||||
</RadioGroup>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('block-height-cell')).toHaveTextContent(
|
||||
'Checking'
|
||||
);
|
||||
|
||||
// statsQueryMock1 should be rendered
|
||||
await waitFor(() => {
|
||||
const elem = screen.getByTestId('query-block-height');
|
||||
expect(elem).toHaveAttribute('data-query-block-height', '1234');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(POLL_INTERVAL);
|
||||
});
|
||||
|
||||
// statsQueryMock2 should be rendered
|
||||
await waitFor(() => {
|
||||
const elem = screen.getByTestId('query-block-height');
|
||||
expect(elem).toHaveAttribute('data-query-block-height', '1235');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(POLL_INTERVAL);
|
||||
});
|
||||
|
||||
// statsQueryMock3 should FAIL!
|
||||
await waitFor(() => {
|
||||
const elem = screen.getByTestId('query-block-height');
|
||||
expect(elem).toHaveAttribute('data-query-block-height', 'failed');
|
||||
});
|
||||
|
||||
// run the timer again, but statsQueryMock4's result should not be
|
||||
// rendered even though its successful, because the poll
|
||||
// should have been stopped
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(POLL_INTERVAL);
|
||||
});
|
||||
|
||||
// should still render the result of statsQueryMock3
|
||||
await waitFor(() => {
|
||||
const elem = screen.getByTestId('query-block-height');
|
||||
expect(elem).toHaveAttribute('data-query-block-height', 'failed');
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '../../utils/__generated__/Node';
|
||||
import { LayoutCell } from './layout-cell';
|
||||
|
||||
const POLL_INTERVAL = 1000;
|
||||
export const POLL_INTERVAL = 1000;
|
||||
export const BLOCK_THRESHOLD = 3;
|
||||
|
||||
export interface RowDataProps {
|
||||
@@ -60,18 +60,22 @@ export const RowData = ({
|
||||
|
||||
// handle polling
|
||||
useEffect(() => {
|
||||
const handleStartPoll = () => startPolling(POLL_INTERVAL);
|
||||
const handleStartPoll = () => {
|
||||
if (error) return;
|
||||
startPolling(POLL_INTERVAL);
|
||||
};
|
||||
const handleStopPoll = () => stopPolling();
|
||||
|
||||
// start polling on mount, but only if there is no error
|
||||
if (error) {
|
||||
handleStopPoll();
|
||||
} else {
|
||||
handleStartPoll();
|
||||
}
|
||||
|
||||
window.addEventListener('blur', handleStopPoll);
|
||||
window.addEventListener('focus', handleStartPoll);
|
||||
|
||||
handleStartPoll();
|
||||
|
||||
if (error) {
|
||||
stopPolling();
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('blur', handleStopPoll);
|
||||
window.removeEventListener('focus', handleStartPoll);
|
||||
@@ -81,6 +85,7 @@ export const RowData = ({
|
||||
// measure response time
|
||||
useEffect(() => {
|
||||
if (!isValidUrl(url)) return;
|
||||
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
|
||||
// every time we get data measure response speed
|
||||
const requestUrl = new URL(url);
|
||||
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||
@@ -177,7 +182,14 @@ export const RowData = ({
|
||||
hasError={getHasError()}
|
||||
dataTestId="block-height-cell"
|
||||
>
|
||||
{getBlockDisplayValue(headers?.blockHeight, error)}
|
||||
<span
|
||||
data-testid="query-block-height"
|
||||
data-query-block-height={
|
||||
error ? 'failed' : data?.statistics.blockHeight
|
||||
}
|
||||
>
|
||||
{getBlockDisplayValue(headers?.blockHeight, error)}
|
||||
</span>
|
||||
</LayoutCell>
|
||||
<LayoutCell
|
||||
label={t('Subscription')}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './use-environment';
|
||||
export * from './use-links';
|
||||
export * from './use-node-health';
|
||||
export * from './use-vega-releases';
|
||||
export * from './use-vega-release';
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,41 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import {
|
||||
GITHUB_VEGA_DEV_RELEASES,
|
||||
GITHUB_VEGA_RELEASES,
|
||||
} from './use-vega-releases';
|
||||
import {
|
||||
GITHUB_VEGA_RELEASES_DATA,
|
||||
GITHUB_VEGA_DEV_RELEASES_DATA,
|
||||
} from './mocks/github-releases';
|
||||
import { useVegaRelease } from './use-vega-release';
|
||||
|
||||
describe('useVegaRelease', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.get(GITHUB_VEGA_RELEASES, GITHUB_VEGA_RELEASES_DATA);
|
||||
fetchMock.get(GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_DEV_RELEASES_DATA);
|
||||
});
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
it('should return release information by given tag', async () => {
|
||||
const { result } = renderHook(() => useVegaRelease('v0.70.1'));
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeTruthy();
|
||||
expect(result.current?.tagName).toEqual('v0.70.1');
|
||||
expect(result.current?.htmlUrl).toEqual(
|
||||
'https://github.com/vegaprotocol/vega/releases/tag/v0.70.1'
|
||||
);
|
||||
expect(result.current?.name).toEqual('v0.70.1');
|
||||
expect(result.current?.isDraft).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when a release cannot be found', async () => {
|
||||
const { result } = renderHook(() => useVegaRelease('v0.70.1'));
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useVegaReleases } from './use-vega-releases';
|
||||
|
||||
export const useVegaRelease = (tag: string, includeDevReleases = false) => {
|
||||
const { data } = useVegaReleases();
|
||||
return useMemo(() => {
|
||||
return data?.find((r) => r.tagName === tag);
|
||||
}, [data, tag]);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import {
|
||||
GITHUB_VEGA_DEV_RELEASES,
|
||||
GITHUB_VEGA_RELEASES,
|
||||
useVegaReleases,
|
||||
} from './use-vega-releases';
|
||||
import {
|
||||
GITHUB_VEGA_RELEASES_DATA,
|
||||
GITHUB_VEGA_DEV_RELEASES_DATA,
|
||||
} from './mocks/github-releases';
|
||||
|
||||
describe('useVegaReleases', () => {
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
it('should return an empty list when request is unsuccessful', async () => {
|
||||
fetchMock.get(GITHUB_VEGA_RELEASES, 404);
|
||||
fetchMock.get(GITHUB_VEGA_DEV_RELEASES, 404);
|
||||
|
||||
const { result } = renderHook(() => useVegaReleases());
|
||||
expect(result.current.loading).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBeFalsy();
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a list of releases', async () => {
|
||||
fetchMock.get(GITHUB_VEGA_RELEASES, GITHUB_VEGA_RELEASES_DATA);
|
||||
fetchMock.get(GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_DEV_RELEASES_DATA);
|
||||
|
||||
const { result } = renderHook(() => useVegaReleases());
|
||||
expect(result.current.loading).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBeFalsy();
|
||||
expect(result.current).not.toEqual([]);
|
||||
const data = result.current.data;
|
||||
expect(data?.[0].tagName).toEqual('v0.70.3');
|
||||
expect(data?.[0].htmlUrl).toEqual(
|
||||
'https://github.com/vegaprotocol/vega/releases/tag/v0.70.3'
|
||||
);
|
||||
expect(data?.[1].tagName).toEqual('v0.70.2');
|
||||
expect(data?.[2].tagName).toEqual('v0.70.1');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a list of releases including dev releases', async () => {
|
||||
fetchMock.get(GITHUB_VEGA_RELEASES, GITHUB_VEGA_RELEASES_DATA);
|
||||
fetchMock.get(GITHUB_VEGA_DEV_RELEASES, GITHUB_VEGA_DEV_RELEASES_DATA);
|
||||
const { result } = renderHook(() => useVegaReleases(true));
|
||||
await waitFor(() => {
|
||||
expect(result.current.data?.length).toBeGreaterThan(30);
|
||||
expect(fetchMock.calls().length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import z from 'zod';
|
||||
|
||||
export const GITHUB_VEGA_RELEASES =
|
||||
'https://api.github.com/repos/vegaprotocol/vega/releases';
|
||||
export const GITHUB_VEGA_DEV_RELEASES =
|
||||
'https://api.github.com/repos/vegaprotocol/vega-dev-releases/releases';
|
||||
|
||||
const GithubReleaseSchema = z.object({
|
||||
url: z.string(),
|
||||
html_url: z.string(),
|
||||
id: z.number(),
|
||||
tag_name: z.string(),
|
||||
name: z.string().nullable(),
|
||||
draft: z.boolean(),
|
||||
created_at: z.string().datetime(),
|
||||
});
|
||||
|
||||
const GithubReleasesSchema = z.array(GithubReleaseSchema);
|
||||
|
||||
type ReleaseInfo = {
|
||||
id: number;
|
||||
name: string;
|
||||
tagName: string;
|
||||
htmlUrl: string;
|
||||
url: string;
|
||||
isDraft: boolean;
|
||||
createdAt: Date;
|
||||
isDevRelease: boolean;
|
||||
};
|
||||
|
||||
const toReleaseInfo = (
|
||||
releaseData: z.infer<typeof GithubReleaseSchema>,
|
||||
isDevRelease: boolean
|
||||
): ReleaseInfo => ({
|
||||
id: releaseData.id,
|
||||
name: releaseData.name || '',
|
||||
tagName: releaseData.tag_name,
|
||||
htmlUrl: releaseData.html_url,
|
||||
url: releaseData.url,
|
||||
isDraft: releaseData.draft,
|
||||
createdAt: new Date(releaseData.created_at),
|
||||
isDevRelease,
|
||||
});
|
||||
|
||||
enum ReleaseFeed {
|
||||
// @ts-ignore TS18033 - allowed in 5.0
|
||||
Vega = GITHUB_VEGA_RELEASES,
|
||||
// @ts-ignore TS18033
|
||||
VegaDev = GITHUB_VEGA_DEV_RELEASES,
|
||||
}
|
||||
|
||||
const fetchReleases = async (feed: ReleaseFeed) => {
|
||||
const response = await fetch(String(feed), {
|
||||
cache: 'force-cache',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
const data = GithubReleasesSchema.parse(json);
|
||||
return data.map((d) => toReleaseInfo(d, feed === ReleaseFeed.VegaDev));
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
type State = {
|
||||
loading: boolean;
|
||||
data: ReleaseInfo[] | null;
|
||||
error: Error | null | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a list of vega releases from github.
|
||||
* First element is the newest.
|
||||
*/
|
||||
export const useVegaReleases = (includeDevReleases = false) => {
|
||||
const [state, setState] = useState<State>({
|
||||
loading: true,
|
||||
data: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const fetchData = useCallback(() => {
|
||||
let mounted = true;
|
||||
|
||||
Promise.all(
|
||||
includeDevReleases
|
||||
? [fetchReleases(ReleaseFeed.Vega), fetchReleases(ReleaseFeed.VegaDev)]
|
||||
: [fetchReleases(ReleaseFeed.Vega)]
|
||||
)
|
||||
.then(([vega, vegaDev]) => {
|
||||
if (mounted) {
|
||||
setState({
|
||||
loading: false,
|
||||
data: (vegaDev ? vega.concat(vegaDev) : vega).sort(
|
||||
(a, b) => b.createdAt.valueOf() - a.createdAt.valueOf()
|
||||
),
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (mounted) {
|
||||
setState({
|
||||
loading: false,
|
||||
data: null,
|
||||
error: err,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [includeDevReleases, setState]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return state;
|
||||
};
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { Orderbook } from './orderbook';
|
||||
import { addDecimal } from '@vegaprotocol/utils';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { marketDepthProvider } from './market-depth-provider';
|
||||
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
|
||||
@@ -48,12 +46,12 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
throttle(() => {
|
||||
dataRef.current = {
|
||||
...marketDataRef.current,
|
||||
indicativePrice: marketDataRef.current?.indicativePrice
|
||||
? getPriceLevel(
|
||||
marketDataRef.current.indicativePrice,
|
||||
resolutionRef.current
|
||||
)
|
||||
: undefined,
|
||||
indicativePrice:
|
||||
marketDataRef.current?.indicativePrice &&
|
||||
getPriceLevel(
|
||||
marketDataRef.current.indicativePrice,
|
||||
resolutionRef.current
|
||||
),
|
||||
midPrice: getMidPrice(
|
||||
rawDataRef.current?.depth.sell,
|
||||
rawDataRef.current?.depth.buy,
|
||||
@@ -148,13 +146,12 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
variables,
|
||||
});
|
||||
|
||||
marketDataRef.current = marketData;
|
||||
if (!marketDataRef.current && marketData) {
|
||||
marketDataRef.current = marketData;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const throttleRunner = updateOrderbookData.current;
|
||||
if (!marketDataRef.current) {
|
||||
return;
|
||||
}
|
||||
if (!data) {
|
||||
dataRef.current = { rows: null };
|
||||
setOrderbookData(dataRef.current);
|
||||
@@ -162,10 +159,9 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
}
|
||||
dataRef.current = {
|
||||
...marketDataRef.current,
|
||||
indicativePrice: getPriceLevel(
|
||||
marketDataRef.current.indicativePrice,
|
||||
resolution
|
||||
),
|
||||
indicativePrice:
|
||||
marketDataRef.current?.indicativePrice &&
|
||||
getPriceLevel(marketDataRef.current.indicativePrice, resolution),
|
||||
midPrice: getMidPrice(data.depth.sell, data.depth.buy, resolution),
|
||||
rows: compactRows(data.depth.sell, data.depth.buy, resolution),
|
||||
};
|
||||
@@ -175,7 +171,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
return () => {
|
||||
throttleRunner.cancel();
|
||||
};
|
||||
}, [data, marketData, resolution]);
|
||||
}, [data, resolution]);
|
||||
|
||||
useEffect(() => {
|
||||
resolutionRef.current = resolution;
|
||||
@@ -197,10 +193,9 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
positionDecimalPlaces={market?.positionDecimalPlaces ?? 0}
|
||||
resolution={resolution}
|
||||
onResolutionChange={(resolution: number) => setResolution(resolution)}
|
||||
onClick={(price?: string | number) => {
|
||||
onClick={(price: string) => {
|
||||
if (price) {
|
||||
const priceValue = addDecimal(price, market?.decimalPlaces ?? 0);
|
||||
updateOrder(marketId, { price: priceValue });
|
||||
updateOrder(marketId, { price });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
|
||||
import { addDecimal, addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
|
||||
import { PriceCell, VolCell, CumulativeVol } from '@vegaprotocol/datagrid';
|
||||
|
||||
interface OrderbookRowProps {
|
||||
@@ -15,7 +15,7 @@ interface OrderbookRowProps {
|
||||
price: string;
|
||||
relativeAsk?: number;
|
||||
relativeBid?: number;
|
||||
onClick?: (price?: string | number) => void;
|
||||
onClick?: (price: string) => void;
|
||||
}
|
||||
|
||||
export const OrderbookRow = React.memo(
|
||||
@@ -59,7 +59,7 @@ export const OrderbookRow = React.memo(
|
||||
<PriceCell
|
||||
testId={`price-${price}`}
|
||||
value={BigInt(price)}
|
||||
onClick={() => onClick && onClick(price)}
|
||||
onClick={() => onClick && onClick(addDecimal(price, decimalPlaces))}
|
||||
valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)}
|
||||
/>
|
||||
<CumulativeVol
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('Orderbook', () => {
|
||||
const decimalPlaces = 3;
|
||||
it('should scroll to mid price on init', async () => {
|
||||
window.innerHeight = 11 * rowHeight;
|
||||
const result = render(
|
||||
render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
@@ -30,7 +30,7 @@ describe('Orderbook', () => {
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
});
|
||||
|
||||
it('should keep mid price row in the middle', async () => {
|
||||
@@ -45,7 +45,7 @@ describe('Orderbook', () => {
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight);
|
||||
result.rerender(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
@@ -121,7 +121,7 @@ describe('Orderbook', () => {
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 0.01);
|
||||
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 0.01);
|
||||
});
|
||||
|
||||
it('should get back to mid price on click', async () => {
|
||||
@@ -143,7 +143,7 @@ describe('Orderbook', () => {
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(1);
|
||||
const scrollToMidPriceButton = result.getByTestId('scroll-to-midprice');
|
||||
fireEvent.click(scrollToMidPriceButton);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 1);
|
||||
expect(screen.getByTestId('scroll').scrollTop).toBe(91 * rowHeight + 1);
|
||||
});
|
||||
|
||||
it('should get back to mid price on resolution change', async () => {
|
||||
@@ -158,12 +158,12 @@ describe('Orderbook', () => {
|
||||
/>
|
||||
);
|
||||
await waitFor(() => screen.getByTestId(`bid-vol-${params.midPrice}`));
|
||||
const scrollElement = result.getByTestId('scroll');
|
||||
const scrollElement = screen.getByTestId('scroll');
|
||||
expect(scrollElement.scrollTop).toBe(91 * rowHeight);
|
||||
scrollElement.scrollTop = 1;
|
||||
fireEvent.scroll(scrollElement);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(1);
|
||||
const resolutionSelect = result.getByTestId(
|
||||
expect(screen.getByTestId('scroll').scrollTop).toBe(1);
|
||||
const resolutionSelect = screen.getByTestId(
|
||||
'resolution'
|
||||
) as HTMLSelectElement;
|
||||
fireEvent.change(resolutionSelect, { target: { value: '10' } });
|
||||
@@ -181,6 +181,47 @@ describe('Orderbook', () => {
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
expect(result.getByTestId('scroll').scrollTop).toBe(6 * rowHeight);
|
||||
expect(screen.getByTestId('scroll').scrollTop).toBe(6 * rowHeight);
|
||||
});
|
||||
|
||||
it('should format correctly the numbers on resolution change', async () => {
|
||||
const onClickSpy = jest.fn();
|
||||
const result = render(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
onClick={onClickSpy}
|
||||
fillGaps
|
||||
{...generateMockData(params)}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
await screen.findByTestId(`bid-vol-${params.midPrice}`)
|
||||
).toBeInTheDocument();
|
||||
// Before resolution change the price is 122.934
|
||||
await fireEvent.click(await screen.getByTestId('price-122934'));
|
||||
expect(onClickSpy).toBeCalledWith('122.934');
|
||||
const resolutionSelect = screen.getByTestId(
|
||||
'resolution'
|
||||
) as HTMLSelectElement;
|
||||
await fireEvent.change(resolutionSelect, { target: { value: '10' } });
|
||||
await result.rerender(
|
||||
<Orderbook
|
||||
decimalPlaces={decimalPlaces}
|
||||
positionDecimalPlaces={0}
|
||||
onClick={onClickSpy}
|
||||
fillGaps
|
||||
{...generateMockData({
|
||||
...params,
|
||||
resolution: 10,
|
||||
})}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
);
|
||||
await fireEvent.click(await screen.getByTestId('price-12299'));
|
||||
// After resolution change the price is 122.99
|
||||
expect(onResolutionChange.mock.calls[0][0]).toBe(10);
|
||||
expect(onClickSpy).toBeCalledWith('122.99');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ interface OrderbookProps extends OrderbookData {
|
||||
positionDecimalPlaces: number;
|
||||
resolution: number;
|
||||
onResolutionChange: (resolution: number) => void;
|
||||
onClick?: (price?: string | number) => void;
|
||||
onClick?: (price: string) => void;
|
||||
fillGaps?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
<LiquidityInfoPanel market={market}>
|
||||
<Link
|
||||
to={`/liquidity/${market.id}`}
|
||||
onClick={(ev) => onSelect?.(market.id, ev.metaKey)}
|
||||
onClick={(ev) => onSelect?.(market.id, ev.metaKey || ev.ctrlKey)}
|
||||
data-testid="view-liquidity-link"
|
||||
>
|
||||
<UILink>{t('View liquidity provision table')}</UILink>
|
||||
|
||||
@@ -11,12 +11,13 @@ import {
|
||||
AgGridDynamic as AgGrid,
|
||||
PriceFlashCell,
|
||||
MarketNameCell,
|
||||
SetFilter,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { MarketMaybeWithData, MarketFieldsFragment } from '../../';
|
||||
import type { MarketMaybeWithData } from '../../';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
|
||||
const { MarketTradingMode, AuctionTrigger } = Schema;
|
||||
@@ -33,7 +34,6 @@ export const MarketListTable = forwardRef<
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('No markets')}
|
||||
getRowId={getRowId}
|
||||
ref={ref}
|
||||
defaultColDef={{
|
||||
@@ -43,7 +43,7 @@ export const MarketListTable = forwardRef<
|
||||
filter: true,
|
||||
filterParams: { buttons: ['reset'] },
|
||||
}}
|
||||
suppressCellFocus={true}
|
||||
suppressCellFocus
|
||||
components={{ PriceFlashCell, MarketNameCell }}
|
||||
{...props}
|
||||
>
|
||||
@@ -59,11 +59,11 @@ export const MarketListTable = forwardRef<
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Trading mode')}
|
||||
field="data"
|
||||
field="tradingMode"
|
||||
minWidth={170}
|
||||
valueGetter={({
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketMaybeWithData, 'data'>) => {
|
||||
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
|
||||
if (!data?.data) return undefined;
|
||||
const { trigger } = data.data;
|
||||
const { tradingMode } = data;
|
||||
@@ -75,14 +75,22 @@ export const MarketListTable = forwardRef<
|
||||
- ${Schema.AuctionTriggerMapping[trigger]}`
|
||||
: Schema.MarketTradingModeMapping[tradingMode];
|
||||
}}
|
||||
filter={SetFilter}
|
||||
filterParams={{
|
||||
set: Schema.MarketTradingModeMapping,
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Status')}
|
||||
field="state"
|
||||
valueGetter={({
|
||||
valueFormatter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
|
||||
return data?.state ? Schema.MarketStateMapping[data?.state] : '-';
|
||||
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
|
||||
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
|
||||
}}
|
||||
filter={SetFilter}
|
||||
filterParams={{
|
||||
set: Schema.MarketStateMapping,
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { MarketListTable } from './market-list-table';
|
||||
@@ -12,15 +14,24 @@ interface MarketsContainerProps {
|
||||
}
|
||||
|
||||
export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const [dataCount, setDataCount] = useState(0);
|
||||
const { data, error, loading, reload } = useDataProvider({
|
||||
dataProvider,
|
||||
skipUpdates: true,
|
||||
variables: undefined,
|
||||
});
|
||||
useEffect(() => {
|
||||
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
|
||||
}, [data]);
|
||||
const onFilterChanged = useCallback(() => {
|
||||
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
|
||||
}, []);
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
<MarketListTable
|
||||
rowData={error ? [] : data}
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
onCellClicked={(cellEvent: CellClickedEvent) => {
|
||||
@@ -36,10 +47,12 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
|
||||
}
|
||||
onSelect(
|
||||
(data as MarketMaybeWithData).id,
|
||||
(event as unknown as MouseEvent)?.metaKey
|
||||
(event as unknown as MouseEvent)?.metaKey ||
|
||||
(event as unknown as MouseEvent)?.ctrlKey
|
||||
);
|
||||
}}
|
||||
onMarketClick={onSelect}
|
||||
onFilterChanged={onFilterChanged}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
@@ -47,6 +60,7 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
|
||||
error={error}
|
||||
data={data}
|
||||
noDataMessage={t('No markets')}
|
||||
noDataCondition={() => !dataCount}
|
||||
reload={reload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
export * from './__generated__/OrdersSubscription';
|
||||
export * from './use-has-active-order';
|
||||
export * from './use-order-cancel';
|
||||
export * from './use-order-edit';
|
||||
export * from './use-order-submit';
|
||||
export * from './use-order-update';
|
||||
export * from './use-pending-orders-volume';
|
||||
export * from './use-order-store';
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { useOrderCancel } from './use-order-cancel';
|
||||
import type { OrderSubSubscription } from './';
|
||||
import { OrderSubDocument } from './';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const defaultWalletContext = {
|
||||
pubKey: null,
|
||||
pubKeys: [],
|
||||
isReadOnly: false,
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
selectPubKey: jest.fn(),
|
||||
connector: null,
|
||||
};
|
||||
|
||||
function setup(context?: Partial<VegaWalletContextShape>) {
|
||||
const mocks: MockedResponse<OrderSubSubscription> = {
|
||||
request: {
|
||||
query: OrderSubDocument,
|
||||
variables: {
|
||||
partyId: context?.pubKey || '',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
orders: [
|
||||
{
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
rejectionReason: null,
|
||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
||||
size: '10',
|
||||
price: '300000',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
marketId: 'market-id',
|
||||
__typename: 'OrderUpdate',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const filterMocks: MockedResponse<OrderSubSubscription> = {
|
||||
request: {
|
||||
query: OrderSubDocument,
|
||||
variables: {
|
||||
partyId: context?.pubKey || '',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
orders: [
|
||||
{
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
rejectionReason: null,
|
||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
||||
size: '10',
|
||||
price: '300000',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
marketId: 'market-id',
|
||||
__typename: 'OrderUpdate',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={[mocks, filterMocks]}>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ ...defaultWalletContext, ...context }}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
return renderHook(() => useOrderCancel(), { wrapper });
|
||||
}
|
||||
|
||||
describe('useOrderCancel', () => {
|
||||
it('has the correct default state', () => {
|
||||
const { result } = setup();
|
||||
expect(typeof result.current.cancel).toEqual('function');
|
||||
expect(typeof result.current.reset).toEqual('function');
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
||||
expect(result.current.transaction.txHash).toEqual(null);
|
||||
expect(result.current.transaction.error).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not sendTx if no keypair', () => {
|
||||
const mockSendTx = jest.fn();
|
||||
const { result } = setup({
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [],
|
||||
pubKey: null,
|
||||
});
|
||||
act(() => {
|
||||
result.current.cancel({ orderId: 'order-id', marketId: 'market-id' });
|
||||
});
|
||||
expect(mockSendTx).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should cancel a correctly formatted order', async () => {
|
||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
|
||||
const { result } = setup({
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [pubKeyObj],
|
||||
pubKey: pubKeyObj.publicKey,
|
||||
});
|
||||
|
||||
const args = {
|
||||
orderId: 'order-id',
|
||||
marketId: 'market-id',
|
||||
};
|
||||
act(() => {
|
||||
result.current.cancel(args);
|
||||
});
|
||||
|
||||
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
|
||||
orderCancellation: args,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import {
|
||||
useVegaWallet,
|
||||
useVegaTransaction,
|
||||
useTransactionResult,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type {
|
||||
OrderCancellationBody,
|
||||
TransactionResult,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type { OrderSubFieldsFragment } from './';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useOrderUpdate } from './use-order-update';
|
||||
|
||||
export const useOrderCancel = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const [cancelledOrder, setCancelledOrder] =
|
||||
useState<OrderSubFieldsFragment | null>(null);
|
||||
const [transactionResult, setTransactionResult] =
|
||||
useState<TransactionResult>();
|
||||
|
||||
const {
|
||||
send,
|
||||
transaction,
|
||||
reset: resetTransaction,
|
||||
setComplete,
|
||||
Dialog,
|
||||
} = useVegaTransaction();
|
||||
|
||||
const waitForOrderUpdate = useOrderUpdate(transaction);
|
||||
const waitForTransactionResult = useTransactionResult();
|
||||
|
||||
const reset = useCallback(() => {
|
||||
resetTransaction();
|
||||
setCancelledOrder(null);
|
||||
}, [resetTransaction]);
|
||||
|
||||
const cancel = useCallback(
|
||||
async (orderCancellation: OrderCancellationBody['orderCancellation']) => {
|
||||
if (!pubKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCancelledOrder(null);
|
||||
|
||||
try {
|
||||
const res = await send(pubKey, {
|
||||
orderCancellation,
|
||||
});
|
||||
if (orderCancellation.orderId) {
|
||||
const cancelledOrder = await waitForOrderUpdate(
|
||||
orderCancellation.orderId,
|
||||
pubKey
|
||||
);
|
||||
setCancelledOrder(cancelledOrder);
|
||||
setComplete();
|
||||
} else if (res) {
|
||||
const txResult = await waitForTransactionResult(
|
||||
res.transactionHash,
|
||||
pubKey
|
||||
);
|
||||
setTransactionResult(txResult);
|
||||
setComplete();
|
||||
}
|
||||
return res;
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[pubKey, send, setComplete, waitForOrderUpdate, waitForTransactionResult]
|
||||
);
|
||||
|
||||
return {
|
||||
transaction,
|
||||
transactionResult,
|
||||
cancelledOrder,
|
||||
Dialog,
|
||||
cancel,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
@@ -1,179 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useOrderEdit } from './use-order-edit';
|
||||
import type { OrderSubSubscription } from './__generated__/OrdersSubscription';
|
||||
import { OrderSubDocument } from './__generated__/OrdersSubscription';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { Order } from '../components';
|
||||
import { generateOrder } from '../components';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const defaultWalletContext = {
|
||||
pubKey: null,
|
||||
pubKeys: [],
|
||||
isReadOnly: false,
|
||||
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
selectPubKey: jest.fn(),
|
||||
connector: null,
|
||||
};
|
||||
|
||||
function setup(order: Order, context?: Partial<VegaWalletContextShape>) {
|
||||
const mocks: MockedResponse<OrderSubSubscription> = {
|
||||
request: {
|
||||
query: OrderSubDocument,
|
||||
variables: {
|
||||
partyId: context?.pubKey || '',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
orders: [
|
||||
{
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
rejectionReason: null,
|
||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
||||
size: '10',
|
||||
price: '300000',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
marketId: 'market-id',
|
||||
__typename: 'OrderUpdate',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const filterMocks: MockedResponse<OrderSubSubscription> = {
|
||||
request: {
|
||||
query: OrderSubDocument,
|
||||
variables: {
|
||||
partyId: context?.pubKey || '',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
orders: [
|
||||
{
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
id: '9c70716f6c3698ac7bbcddc97176025b985a6bb9a0c4507ec09c9960b3216b62',
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
rejectionReason: null,
|
||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
||||
size: '10',
|
||||
price: '300000',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
marketId: 'market-id',
|
||||
__typename: 'OrderUpdate',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={[mocks, filterMocks]}>
|
||||
<VegaWalletContext.Provider
|
||||
value={{ ...defaultWalletContext, ...context }}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useOrderEdit(order), { wrapper });
|
||||
}
|
||||
|
||||
describe('useOrderEdit', () => {
|
||||
it('should edit a correctly formatted order if there is no size', async () => {
|
||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
|
||||
const order = generateOrder({
|
||||
price: '123456789',
|
||||
market: { decimalPlaces: 2 },
|
||||
});
|
||||
const { result } = setup(order, {
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [pubKeyObj],
|
||||
pubKey: pubKeyObj.publicKey,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.edit({ price: '1234567.89' });
|
||||
});
|
||||
|
||||
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
|
||||
orderAmendment: {
|
||||
orderId: order.id,
|
||||
// eslint-disable-next-line
|
||||
marketId: order.market!.id,
|
||||
timeInForce: order.timeInForce,
|
||||
price: '123456789', // Decimal removed
|
||||
sizeDelta: 0,
|
||||
expiresAt: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should edit a correctly formatted order', async () => {
|
||||
const mockSendTx = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
const pubKeyObj = { publicKey: '0x123', name: 'test key 1' };
|
||||
const order = generateOrder({
|
||||
price: '123456789',
|
||||
market: { decimalPlaces: 2 },
|
||||
});
|
||||
const { result } = setup(order, {
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [pubKeyObj],
|
||||
pubKey: pubKeyObj.publicKey,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.edit({ price: '1234567.89', size: '20' });
|
||||
});
|
||||
|
||||
expect(mockSendTx).toHaveBeenCalledWith(pubKeyObj.publicKey, {
|
||||
orderAmendment: {
|
||||
orderId: order.id,
|
||||
// eslint-disable-next-line
|
||||
marketId: order.market!.id,
|
||||
timeInForce: order.timeInForce,
|
||||
price: '123456789', // Decimal removed
|
||||
sizeDelta: 1990,
|
||||
expiresAt: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('has the correct default state', () => {
|
||||
const order = generateOrder();
|
||||
const { result } = setup(order);
|
||||
expect(typeof result.current.edit).toEqual('function');
|
||||
expect(typeof result.current.reset).toEqual('function');
|
||||
expect(result.current.transaction.status).toEqual(VegaTxStatus.Default);
|
||||
expect(result.current.transaction.txHash).toEqual(null);
|
||||
expect(result.current.transaction.error).toEqual(null);
|
||||
});
|
||||
|
||||
it('should not sendTx if no keypair', async () => {
|
||||
const order = generateOrder();
|
||||
const mockSendTx = jest.fn();
|
||||
const { result } = setup(order, {
|
||||
sendTx: mockSendTx,
|
||||
pubKeys: [],
|
||||
pubKey: null,
|
||||
});
|
||||
await act(async () => {
|
||||
result.current.edit(order);
|
||||
});
|
||||
expect(mockSendTx).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,82 +0,0 @@
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useVegaTransaction, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import type { OrderSubFieldsFragment } from './';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import type { Order } from '../components';
|
||||
import { useOrderUpdate } from './use-order-update';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export interface EditOrderArgs {
|
||||
price: string;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export const useOrderEdit = (order: Order | null) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const [updatedOrder, setUpdatedOrder] =
|
||||
useState<OrderSubFieldsFragment | null>(null);
|
||||
|
||||
const {
|
||||
send,
|
||||
transaction,
|
||||
reset: resetTransaction,
|
||||
setComplete,
|
||||
Dialog,
|
||||
} = useVegaTransaction();
|
||||
|
||||
const waitForOrderUpdate = useOrderUpdate(transaction);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
resetTransaction();
|
||||
setUpdatedOrder(null);
|
||||
}, [resetTransaction]);
|
||||
|
||||
const edit = useCallback(
|
||||
async (args: EditOrderArgs) => {
|
||||
if (!pubKey || !order || !order.market) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUpdatedOrder(null);
|
||||
|
||||
try {
|
||||
await send(pubKey, {
|
||||
orderAmendment: {
|
||||
orderId: order.id,
|
||||
marketId: order.market.id,
|
||||
price: removeDecimal(args.price, order.market.decimalPlaces),
|
||||
timeInForce: order.timeInForce,
|
||||
sizeDelta: args.size
|
||||
? new BigNumber(
|
||||
removeDecimal(args.size, order.market.positionDecimalPlaces)
|
||||
)
|
||||
.minus(order.size)
|
||||
.toNumber()
|
||||
: 0,
|
||||
expiresAt: order.expiresAt
|
||||
? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const updatedOrder = await waitForOrderUpdate(order.id, pubKey);
|
||||
setUpdatedOrder(updatedOrder);
|
||||
setComplete();
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[pubKey, send, order, setComplete, waitForOrderUpdate]
|
||||
);
|
||||
|
||||
return {
|
||||
transaction,
|
||||
updatedOrder,
|
||||
Dialog,
|
||||
edit,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user