Compare commits

...
Author SHA1 Message Date
Madalina Raicu 102e87d7d0 fix: notional size formatting 2023-06-27 09:21:40 +03:00
Matthew Russell 13f2e51798 chore(ui-toolkit): remove forwardRef as refs weren't being passed (#4160) 2023-06-23 15:36:21 -07:00
Bartłomiej Głownia fc047feeee fix(trading): avoid items remounting in MarketSelector List (#4162) 2023-06-23 15:35:50 -07:00
Bartłomiej Głownia 005455c870 chore(orders): use columnDefs in orders list, fix sorting by size and filled (#4170) 2023-06-23 15:33:25 -07:00
daro-maj 2c2bc391e8 test(trading): chart depth e2e tests (#4171) 2023-06-23 15:13:28 +02:00
m.ray 445a085190 fix(trading): market info number formatting (#4163) 2023-06-23 12:44:18 +01:00
Sam Keen bb1b236cdf feat(governance): show multisig penalty in tooltip (#4140) 2023-06-22 09:43:46 +01:00
Joe Tsang da66b7b20d test(governance): test market details specifications (#4153) 2023-06-22 08:32:33 +01:00
Ciaran McGhie ec12811f72 fix(ui-toolkit): visual fixes for healthbar - % and text colours (#4156) 2023-06-21 11:13:19 -07:00
Ciaran McGhie 180de8cf25 fix(ui-toolkit): healthbar fee decimals (#4152) 2023-06-21 14:51:17 +01:00
m.ray df88e77cdf fix(trading): liquidity provision table quantum formatting (#4114) 2023-06-21 11:23:07 +00:00
Mikołaj Młodzikowski 9441aee8cf feat(ci): update process tested on fairground 2023-06-21 13:05:42 +02:00
Bartłomiej Głownia 2353812834 fix(fills): fix mapping fill update (#4144) 2023-06-21 10:09:08 +01:00
Mikołaj Młodzikowski bba2b3c177 feat(ci): deploy fairground through interstitial page (#4151) 2023-06-21 11:00:27 +02:00
Joe Tsang ae57bd92f4 chore(governance): add e2e test for voting with different pub keys (#4147) 2023-06-21 09:42:36 +01:00
daro-maj 115b642140 test(trading): cover post and reduce only ACs (#4149) 2023-06-21 10:05:31 +02:00
Sam Keen ce3da97a8a fix(governance): proposal deadline validation missing (#4150) 2023-06-20 23:55:45 +01:00
Sam Keen cabd99d3ef feat(governance): new market details improvements (#4106) 2023-06-20 23:53:53 +01:00
Bartłomiej Głownia 597e07608f feat(positions): add liquidation to positions view (#4115) 2023-06-20 21:49:52 +00:00
Matthew Russell e451dc54b3 chore(trading): disable flakey network switcher test 2023-06-20 13:51:14 -07:00
Matthew Russell 9703c3b7a6 fix(trading): deposit table loading state (#4143) 2023-06-20 13:50:10 -07:00
Mikołaj Młodzikowski c22b6f3ce9 feat(ci): add missing fairground record to switch 2023-06-20 17:33:39 +02:00
Bartłomiej Głownia c740c11eaa feat(deal-ticket): improve no collateral validation (#4122) 2023-06-20 08:31:52 -07:00
Mikołaj Młodzikowski d1fabb31a3 feat(ci): fix deployments to testnet 2023-06-20 16:32:27 +02:00
Mikołaj Młodzikowski 94f045a60b fix(ci): port changes for release process fixes for console on frontend 2023-06-20 16:17:13 +02:00
Mikołaj Młodzikowski 1af375bab8 feat(ci): setup fleek deployments for testnet for console (#4138) 2023-06-20 14:49:39 +02:00
Sam Keen 5a48ba4a33 fix(governance): ensure cached finalized vote is only used when pubkey is c… (#4040) 2023-06-19 17:07:30 +00:00
Radosław Szpiech d1fd9184ce test(trading): #3508 tests for Trades ACs (#4139) 2023-06-19 17:07:27 +02:00
Joe Tsang 8f29824f9e chore(governance): update staking acs (#4123) 2023-06-16 16:45:19 +00:00
Joe Tsang fc3c73ad24 chore(governance): un-skip staking test and increase test coverage for smoke tests (#4112) 2023-06-16 14:42:38 +01:00
Bartłomiej Głownia 1027827576 feat(trading): show margin level visualisation in asset breakdown modal (#4048) 2023-06-16 12:28:59 +02:00
Sam Keen 0089920d4c fix(governance): earned by me reward percentage (#4093) 2023-06-16 10:04:03 +01:00
Matthew Russell 35896a9cf5 chore(trading): clear apollo warnings (#4120) 2023-06-16 09:50:22 +01:00
122 changed files with 3521 additions and 1985 deletions
+49 -33
View File
@@ -40,7 +40,7 @@ jobs:
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
with:
# registry: registry.hub.docker.com
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -173,7 +173,7 @@ jobs:
uses: docker/build-push-action@v3
continue-on-error: true
id: dockerhub-push
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
@@ -183,7 +183,7 @@ jobs:
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:mainnet
vegaprotocol/${{ matrix.app }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }}
- name: Publish dist as docker image (ghcr - retry)
uses: docker/build-push-action@v3
@@ -210,13 +210,13 @@ jobs:
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:mainnet
vegaprotocol/${{ matrix.app }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }}
# bucket creation in github.com/vegaprotocol/terraform//frontend
- name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master
# s3 releases are not happening for trading on mainnet - it's IPFS
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !endsWith(github.ref, 'main') ) ) }}
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) ) ) }}
with:
args: --acl private --follow-symlinks --delete
env:
@@ -235,22 +235,37 @@ jobs:
- name: Trigger fleek deployment
# release to ipfs happens only on mainnet (represented by main branch) for trading
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
run: |
# display info about app
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
if echo ${{ github.ref }} | grep -q main; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
elif echo ${{ github.ref }} | grep -q release/testnet; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id status}}"}' \
https://api.fleek.co/graphql
fi
- name: Check out ipfs-redirect
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }}
uses: actions/checkout@v3
with:
repository: 'vegaprotocol/ipfs-redirect'
@@ -258,11 +273,12 @@ jobs:
fetch-depth: '0'
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update console.vega.xyz DNS to redirect to the new console
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'main') }}
- name: Update interstitial page to point to the new console
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }}
env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
run: |
# set CID
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
@@ -270,28 +286,28 @@ jobs:
new_hash=$(cat ${{ matrix.app }}-ipfs-hash)
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
ls -al ipfs-redirect
echo $new_hash > ipfs-redirect/cidv0.txt
echo $new_cid > ipfs-redirect/cidv1.txt
(
cd ipfs-redirect
# configure git
git status
cat .git/config
git config --global user.email "vega-ci-bot@vega.xyz"
git config --global user.name "vega-ci-bot"
branch_name="update-hash-${{ github.ref }}"
git checkout -b "$branch_name"
# update CID files
if echo ${{ github.ref }} | grep -q main; then
echo $new_hash > cidv0-mainnet.txt
echo $new_cid > cidv1-mainnet.txt
git add cidv0-mainnet.txt cidv1-mainnet.txt
elif echo ${{ github.ref }} | grep -q release/testnet; then
echo $new_hash > cidv0-fairground.txt
echo $new_cid > cidv1-fairground.txt
git add cidv0-fairground.txt cidv1-fairground.txt
fi
# create commit
commit_msg="Automated hash update from ${{ github.ref }}"
git add cidv0.txt cidv1.txt
git commit -m "$commit_msg"
git push -u origin "$branch_name"
pr_url="$(gh pr create --title "${commit_msg}" --body 'automated pull request to update CIDs')"
echo $pr_url
# once auto merge get's enabled on documentation repo let's do follow up
sleep 5
gh pr merge "${pr_url}" --delete-branch --squash --admin
git push -u origin "main"
)
@@ -77,7 +77,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
hide={window.innerWidth <= BREAKPOINT_MD}
valueGetter={({
data,
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
}: VegaValueGetterParams<MarketFieldsFragment>) => {
return data?.state ? MarketStateMapping[data?.state] : '-';
}}
/>
+1 -1
View File
@@ -49,7 +49,7 @@ module.exports = defineConfig({
vegaTokenContractAddress: '0xF41bD86d462D36b997C0bbb4D97a0a3382f205B7',
vegaTokenAddress: '0x67175Da1D5e966e40D11c4B2519392B2058373de',
txTimeout: { timeout: 70000 },
epochTimeout: { timeout: 6000 },
epochTimeout: { timeout: 10000 },
blockConfirmations: 3,
grepTags: '@regression @smoke @slow',
grepFilterSpecs: true,
@@ -14,9 +14,16 @@ import {
submitUniqueRawProposal,
voteForProposal,
} from '../../../../governance-e2e/src/support/governance.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../../../governance-e2e/src/support/staking.functions';
import {
ensureSpecifiedUnstakedTokensAreAssociated,
stakingPageAssociateTokens,
stakingPageDisassociateAllTokens,
} from '../../../../governance-e2e/src/support/staking.functions';
import { ethereumWalletConnect } from '../../../../governance-e2e/src/support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../../../governance-e2e/src/support/wallet-teardown.functions';
import {
switchVegaWalletPubKey,
vegaWalletSetSpecifiedApprovalAmount,
} from '../../support/wallet-functions';
import type { testFreeformProposal } from '../../support/common-interfaces';
import { formatDateWithLocalTimezone } from '@vegaprotocol/utils';
@@ -44,7 +51,7 @@ describe(
before('connect wallets and set approval limit', function () {
cy.visit('/');
ethereumWalletConnect();
cy.associateTokensToVegaWallet('1');
// cy.associateTokensToVegaWallet('1');
});
beforeEach('visit proposals tab', function () {
@@ -297,5 +304,33 @@ describe(
.and('be.visible');
});
});
it('Able to vote for proposal twice by switching public key', function () {
ensureSpecifiedUnstakedTokensAreAssociated('1');
createRawProposal();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getProposalFromTitle(rawProposal.rationale.title).within(() =>
cy.get(viewProposalButton).click()
);
voteForProposal('for');
cy.contains('You voted: For').should('be.visible');
ethereumWalletConnect();
switchVegaWalletPubKey();
stakingPageAssociateTokens('2');
navigateTo(navigation.proposals);
getProposalFromTitle(rawProposal.rationale.title).within(() =>
cy.get(viewProposalButton).click()
);
cy.getByTestId('you-voted').should('not.exist');
voteForProposal('against');
cy.contains('You voted: Against').should('be.visible');
switchVegaWalletPubKey();
cy.get(proposalVoteProgressForTokens).should('contain.text', '1.00');
// Checking vote status for different public keys is displayed correctly
cy.contains('You voted: For').should('be.visible');
});
switchVegaWalletPubKey();
stakingPageDisassociateAllTokens();
});
}
);
@@ -16,7 +16,7 @@ import {
} from '../../support/proposal.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-functions';
const proposalListItem = '[data-testid="proposals-list-item"]';
const closedProposals = '[data-testid="closed-proposals"]';
@@ -89,8 +89,14 @@ context(
});
cy.get(proposalStatus).should('have.text', 'Open');
voteForProposal('for');
cy.get(proposalStatus, proposalTimeout).should('have.text', 'Passed');
cy.get(proposalStatus, proposalTimeout).should('have.text', 'Enacted');
cy.get(proposalStatus, proposalTimeout)
.should('have.text', 'Passed')
.then(() => {
cy.get(proposalStatus, proposalTimeout).should(
'have.text',
'Enacted'
);
});
cy.get(votesTable).within(() => {
cy.contains('Vote passed.').should('be.visible');
cy.contains('Voting has ended.').should('be.visible');
@@ -32,7 +32,7 @@ import {
import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
} from '../../support/wallet-functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import type { testFreeformProposal } from '../../support/common-interfaces';
@@ -130,7 +130,7 @@ context(
createRawProposal();
});
it.skip('Creating a proposal - proposal rejected - when closing time sooner than system default', function () {
it('Creating a proposal - proposal rejected - when closing time sooner than system default', function () {
goToMakeNewProposal(governanceProposalType.FREEFORM);
enterUniqueFreeFormProposalBody('0.1', generateFreeFormProposalTitle());
cy.get('input:invalid')
@@ -138,7 +138,7 @@ context(
.should('equal', 'Value must be greater than or equal to 1.');
});
it.skip('Creating a proposal - proposal rejected - when closing time later than system default', function () {
it('Creating a proposal - proposal rejected - when closing time later than system default', function () {
goToMakeNewProposal(governanceProposalType.FREEFORM);
enterUniqueFreeFormProposalBody(
'100000',
@@ -8,6 +8,7 @@ import {
} from '../../support/common.functions';
import {
getDownloadedProposalJsonPath,
getProposalFromTitle,
submitUniqueRawProposal,
} from '../../support/governance.functions';
import {
@@ -23,10 +24,11 @@ import {
} from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
switchVegaWalletPubKey,
vegaWalletFaucetAssetsWithoutCheck,
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
} from '../../support/wallet-functions';
const proposalListItem = '[data-testid="proposals-list-item"]';
const openProposals = '[data-testid="open-proposals"]';
@@ -54,7 +56,8 @@ const feedbackError = '[data-testid="Error"]';
const viewProposalBtn = 'view-proposal-btn';
const liquidityVoteStatus = 'liquidity-votes-status';
const tokenVoteStatus = 'token-votes-status';
const proposalTermsSection = 'proposal';
const proposalJsonToggle = 'proposal-json-toggle';
const proposalJsonSection = 'proposal-json';
const vegaWalletPublicKey = Cypress.env('vegaWalletPublicKey');
const fUSDCId =
'816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc';
@@ -185,7 +188,7 @@ context(
cy.get(maxVoteDeadline).click();
cy.get(enactmentDeadlineError).should(
'have.text',
'Proposal will fail if enactment is earlier than the voting deadline'
'The proposal will fail if enactment is earlier than the voting deadline'
);
cy.get(proposalDownloadBtn)
.should('be.visible')
@@ -205,29 +208,51 @@ context(
});
// 3003-PMAN-001
it('Able to submit valid new market proposal', function () {
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.get(newProposalTitle).type('Test new market proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
const newMarketPayload = JSON.stringify(newMarketProposal);
cy.get(newProposalTerms).type(newMarketPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
cy.get(proposalDownloadBtn)
.should('be.visible')
.click()
.then(() => {
cy.wrap(
getDownloadedProposalJsonPath('vega-new-market-proposal-')
).then((filePath) => {
goToMakeNewProposal(governanceProposalType.RAW);
submitUniqueRawProposal({ proposalBody: filePath }); // 3003-PMAN-003
it(
'Able to submit valid new market proposal',
{ tags: '@smoke' },
function () {
const proposalTitle = 'Test new market proposal';
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.get(newProposalTitle).type('Test new market proposal');
cy.get(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
const newMarketPayload = JSON.stringify(newMarketProposal);
cy.get(newProposalTerms).type(newMarketPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
});
cy.get(proposalDownloadBtn)
.should('be.visible')
.click()
.then(() => {
cy.wrap(
getDownloadedProposalJsonPath('vega-new-market-proposal-')
).then((filePath) => {
goToMakeNewProposal(governanceProposalType.RAW);
submitUniqueRawProposal({ proposalBody: filePath }); // 3003-PMAN-003
});
});
navigateTo(navigation.proposals);
getProposalFromTitle(proposalTitle).within(() =>
cy.getByTestId('view-proposal-btn').click()
);
cy.getByTestId('proposal-market-data').within(() => {
cy.getByTestId('proposal-market-data-toggle').click();
cy.contains('Key details').click();
getMarketProposalDetailsFromTable('Name').should(
'have.text',
'Token test market'
);
cy.contains('Settlement asset').click();
// Settlement asset symbol
cy.getByTestId('3_value').should('have.text', 'fBTC');
cy.contains('Oracle').click();
cy.getByTestId('oracle-spec-links').should('have.attr', 'href');
});
}
);
it('Unable to submit new market proposal with missing/invalid fields', function () {
const errorMsg =
@@ -265,8 +290,7 @@ context(
// Will fail if run after 'Able to submit update market proposal and vote for proposal'
// 3002-PROP-022
it('Unable to submit update market proposal without equity-like share in the market', function () {
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click(); // switch to second wallet pub key
switchVegaWalletPubKey();
stakingPageAssociateTokens('1');
goToMakeNewProposal(governanceProposalType.UPDATE_MARKET);
cy.get(newProposalTitle).type('Test update market proposal - rejected');
@@ -296,8 +320,7 @@ context(
closeDialog();
ethereumWalletConnect();
stakingPageDisassociateAllTokens();
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
switchVegaWalletPubKey();
});
// 3002-PROP-020
@@ -451,8 +474,8 @@ context(
.within(() => {
cy.getByTestId(viewProposalBtn).click();
});
cy.getByTestId('proposal-terms-toggle').click();
cy.getByTestId(proposalTermsSection).within(() => {
cy.getByTestId(proposalJsonToggle).click();
cy.getByTestId(proposalJsonSection).within(() => {
cy.contains('USDT Coin').should('be.visible');
cy.contains('USDT').should('be.visible');
});
@@ -498,13 +521,11 @@ context(
.invoke('text')
.should('not.be.empty');
// 3001-VOTE-030 3001-VOTE-031
cy.getByTestId('proposal-terms-toggle').click();
cy.getByTestId('proposal-terms').within(() => {
getProposalInformationFromTable('assetId').should('have.text', assetId);
getProposalInformationFromTable('lifetimeLimit').should(
'have.text',
'10'
);
cy.getByTestId(proposalJsonToggle).click();
cy.getByTestId(proposalJsonSection).within(() => {
cy.contains(assetId).should('be.visible');
cy.contains('lifetimeLimit').should('be.visible');
cy.contains('10').should('be.visible');
});
});
@@ -602,5 +623,13 @@ context(
});
});
}
function getMarketProposalDetailsFromTable(heading: string) {
return cy
.getByTestId('key-value-table-row')
.contains(heading)
.parent()
.siblings();
}
}
);
@@ -21,7 +21,7 @@ import {
} from '../../support/governance.functions';
import { ensureSpecifiedUnstakedTokensAreAssociated } from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-teardown.functions';
import { vegaWalletSetSpecifiedApprovalAmount } from '../../support/wallet-functions';
const proposalListItem = 'proposals-list-item';
const openProposals = '[data-testid="open-proposals"]';
@@ -14,7 +14,7 @@ import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
depositAsset,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
} from '../../support/wallet-functions';
const vegaAssetAddress = '0x67175Da1D5e966e40D11c4B2519392B2058373de';
const vegaWalletUnstakedBalance =
@@ -25,7 +25,7 @@ import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
} from '../../support/wallet-functions';
const stakeValidatorListTotalStake = 'total-stake';
const stakeValidatorListTotalShare = 'total-stake-share';
const stakeValidatorListStakePercentage = 'stake-percentage';
@@ -54,7 +54,7 @@ context(
'Staking Tab - with eth and vega wallets connected',
{ tags: '@slow' },
function () {
// 2001-STKE-002, 2001-STKE-032
// 1002-STKE-002, 1002-STKE-032
before('visit staking tab and connect vega wallet', function () {
cy.visit('/');
ethereumWalletConnect();
@@ -63,7 +63,7 @@ context(
vegaWalletSetSpecifiedApprovalAmount('1000');
});
describe.skip('Eth wallet - contains VEGA tokens', function () {
describe('Eth wallet - contains VEGA tokens', function () {
beforeEach(
'teardown wallet & drill into a specific validator',
function () {
@@ -77,25 +77,41 @@ context(
}
);
// 1002-STKE-035 1002-STKE-036
it('Unable to stake against a validator with less than minimum and more than associated amount', function () {
ensureSpecifiedUnstakedTokensAreAssociated('3');
verifyUnstakedBalance(3.0);
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletAssociatedBalance('3.0');
cy.get('button').contains('Select a validator to nominate').click();
clickOnValidatorFromList(0);
cy.getByTestId(stakeAddStakeRadioButton, epochTimeout).click({
force: true,
});
cy.getByTestId(stakeTokenAmountInputBox).type('0.001');
cy.getByTestId(stakeTokenSubmitButton).should('be.disabled');
cy.getByTestId(stakeTokenAmountInputBox).clear().type('4');
cy.getByTestId(stakeTokenSubmitButton).should('be.disabled');
});
it('Able to stake against a validator - using vega from wallet', function () {
ensureSpecifiedUnstakedTokensAreAssociated('3');
verifyUnstakedBalance(3.0);
verifyEthWalletTotalAssociatedBalance('3.0');
verifyEthWalletAssociatedBalance('3.0');
cy.get('button').contains('Select a validator to nominate').click();
// 2001-STKE-031
// 1002-STKE-031
clickOnValidatorFromList(0);
// 2001-STKE-033, 2001-STKE-034, 2001-STKE-037
// 1002-STKE-033, 1002-STKE-034, 1002-STKE-037
stakingValidatorPageAddStake('2');
verifyUnstakedBalance(1.0);
// 2001-STKE-039
// 1002-STKE-039
verifyStakedBalance(2.0);
verifyNextEpochValue(2.0); // 2001-STKE-016 2001-STKE-038
verifyThisEpochValue(2.0); // 2001-STKE-013
verifyNextEpochValue(2.0); // 1002-STKE-016 1002-STKE-038
verifyThisEpochValue(2.0); // 1002-STKE-013
closeStakingDialog();
navigateTo(navigation.validators);
// 2002-SINC-007
// 2002-SINC-007 1002-STKE-015 1002-STKE-017 1002-STKE-052
validateValidatorListTotalStakeAndShare('0', '3,002.00', '50.02%');
});
@@ -218,7 +234,7 @@ context(
});
});
// 2001-STKE-041
// 1002-STKE-041 1002-STKE-053
it(
'Able to remove part of a stake against a validator',
{ tags: '@smoke' },
@@ -231,11 +247,11 @@ context(
verifyUnstakedBalance(1.0);
closeStakingDialog();
navigateTo(navigation.validators);
// 2001-STKE-040
// 1002-STKE-040
clickOnValidatorFromList(0);
// 2001-STKE-044, 2001-STKE-048
// 1002-STKE-044, 1002-STKE-048
stakingValidatorPageRemoveStake('1');
// 2001-STKE-049
// 1002-STKE-049
verifyNextEpochValue(2.0);
verifyUnstakedBalance(2.0);
verifyStakedBalance(2.0);
@@ -255,7 +271,7 @@ context(
}
);
// 2001-STKE-045
// 1002-STKE-045
it('Able to remove a full stake against a validator', function () {
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
@@ -303,6 +319,7 @@ context(
.and('be.visible');
});
// 1002-STKE-046 1002-STKE-047
it('Unable to remove a stake greater than staked amount next epoch for a validator', function () {
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
@@ -405,7 +422,7 @@ context(
});
it('Associating wallet tokens - when some already staked - auto stakes tokens to staked validator', function () {
// 2001-STKE-004
// 1002-STKE-004
stakingPageAssociateTokens('3');
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -419,7 +436,7 @@ context(
});
it('Associating vesting contract tokens - when some already staked - auto stakes tokens to staked validator', function () {
// 2001-STKE-004
// 1002-STKE-004
stakingPageAssociateTokens('3', { type: 'contract' });
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -433,7 +450,7 @@ context(
});
it('Associating vesting contract tokens - when wallet tokens already staked - auto stakes tokens to staked validator', function () {
// 2001-STKE-004
// 1002-STKE-004
stakingPageAssociateTokens('3', { type: 'wallet' });
verifyUnstakedBalance(3.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -447,7 +464,7 @@ context(
});
it('Associating tokens - with multiple validators already staked - auto stakes to staked validators - abiding by existing stake ratio', function () {
// 2001-STKE-004
// 1002-STKE-004
stakingPageAssociateTokens('6');
verifyUnstakedBalance(6.0);
cy.get('button').contains('Select a validator to nominate').click();
@@ -500,7 +517,7 @@ context(
}
function verifyThisEpochValue(amount: number) {
cy.getByTestId('stake-this-epoch', epochTimeout) // 2001-STKE-013
cy.getByTestId('stake-this-epoch', epochTimeout) // 1002-STKE-013
.contains(amount, epochTimeout)
.should('be.visible');
}
@@ -14,11 +14,12 @@ import {
} from '../../support/staking.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import {
switchVegaWalletPubKey,
vegaWalletAssociate,
vegaWalletDisassociate,
vegaWalletSetSpecifiedApprovalAmount,
vegaWalletTeardown,
} from '../../support/wallet-teardown.functions';
} from '../../support/wallet-functions';
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
@@ -64,31 +65,38 @@ context(
}
);
it('Able to associate tokens - from wallet', function () {
//1004-ASSO-003
//1004-ASSO-005
//1004-ASSO-009
//1004-ASSO-030
//1004-ASSO-012
//1004-ASSO-013
//1004-ASSO-014
//1004-ASSO-015
//1004-ASSO-030
//0005-ETXN-006
//0005-ETXN-003
//0005-ETXN-005
stakingPageAssociateTokens('2', { skipConfirmation: true });
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
// 0005-ETXN-002
verifyEthWalletAssociatedBalance('2.0');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
});
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0);
});
it(
'Able to associate tokens - from wallet',
{ tags: '@smoke' },
function () {
//1004-ASSO-003
//1004-ASSO-005
//1004-ASSO-009
//1004-ASSO-030
//1004-ASSO-012
//1004-ASSO-013
//1004-ASSO-014
//1004-ASSO-015
//1004-ASSO-030
//0005-ETXN-006
//0005-ETXN-003
//0005-ETXN-005
stakingPageAssociateTokens('2', { skipConfirmation: true });
validateWalletCurrency('Associated', '0.00');
validateWalletCurrency('Pending association', '2.00');
validateWalletCurrency('Total associated after pending', '2.00');
// 0005-ETXN-002
verifyEthWalletAssociatedBalance('2.0');
verifyEthWalletTotalAssociatedBalance('2.0');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
2.0
);
});
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0);
}
);
it('Able to disassociate all associated tokens - manually', function () {
// 1004-ASSO-025
@@ -294,8 +302,7 @@ context(
Cypress.env('vegaWalletPublicKey')
);
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
switchVegaWalletPubKey();
cy.get(connectedVegaKey).should(
'have.text',
Cypress.env('vegaWalletPublicKey2')
@@ -5,7 +5,7 @@ import {
waitForSpinner,
} from '../../support/common.functions';
import { ethereumWalletConnect } from '../../support/wallet-eth.functions';
import { depositAsset } from '../../support/wallet-teardown.functions';
import { depositAsset } from '../../support/wallet-functions';
const withdraw = 'withdraw';
const withdrawalForm = 'withdraw-form';
@@ -55,6 +55,10 @@ context(
navigateTo(navigation.withdraw);
cy.connectVegaWallet();
ethereumWalletConnect();
cy.getByTestId('currency-title', txTimeout).should(
'contain.text',
usdtName
);
});
it('Able to open withdrawal form with vega wallet connected', function () {
@@ -92,71 +96,75 @@ context(
});
});
it('Able to withdraw asset: -eth wallet connected -withdraw funds button', function () {
// fill in withdrawal form
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(withdrawalThreshold).should(
'have.text',
'100,000.00000T'
);
cy.getByTestId(delayTime).should('have.text', 'None');
cy.getByTestId(amountInput).click().type('120');
cy.getByTestId(submitWithdrawalButton).click();
});
// assert withdrawal request
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'Funds unlocked')
.within(() => {
cy.getByTestId('external-link').should('exist');
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 120.00 tUSDC'
it(
'Able to withdraw asset: -eth wallet connected -withdraw funds button',
{ tags: '@smoke' },
function () {
// fill in withdrawal form
cy.getByTestId(withdraw).should('be.visible').click();
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
cy.getByTestId('select-asset').click();
cy.get('select')
.select(usdtSelectValue, { force: true })
.should('have.value', usdtSelectValue);
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
cy.getByTestId(withdrawalThreshold).should(
'have.text',
'100,000.00000T'
);
cy.getByTestId(toastCompleteWithdrawal).click();
cy.getByTestId(toastClose).click();
cy.getByTestId(delayTime).should('have.text', 'None');
cy.getByTestId(amountInput).click().type('120');
cy.getByTestId(submitWithdrawalButton).click();
});
// withdrawal complete
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'The withdrawal has been approved.')
.within(() => {
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 120.00 tUSDC'
);
});
cy.getByTestId(toast)
.last(txTimeout)
.should('contain.text', 'Transaction confirmed')
.within(() => {
cy.getByTestId('external-link').should('exist');
});
// withdrawal history for complete withdrawal displayed
cy.get(tableWithdrawnStatus)
.eq(1, txTimeout)
.should('have.text', 'Completed')
.parent()
.within(() => {
cy.get(tableAssetSymbol).should('have.text', usdcSymbol);
cy.get(tableAmount).should('have.text', '120.00');
cy.get(tableReceiverAddress)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/address/');
cy.get(tableWithdrawnTimeStamp).should('not.be.empty');
cy.get(tableTxHash)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/tx/');
});
});
// assert withdrawal request
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'Funds unlocked')
.within(() => {
cy.getByTestId('external-link').should('exist');
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 120.00 tUSDC'
);
cy.getByTestId(toastCompleteWithdrawal).click();
cy.getByTestId(toastClose).click();
});
// withdrawal complete
cy.getByTestId(toast)
.first(txTimeout)
.should('contain.text', 'The withdrawal has been approved.')
.within(() => {
cy.getByTestId(toastPanel).should(
'contain.text',
'Withdraw 120.00 tUSDC'
);
});
cy.getByTestId(toast)
.last(txTimeout)
.should('contain.text', 'Transaction confirmed')
.within(() => {
cy.getByTestId('external-link').should('exist');
});
// withdrawal history for complete withdrawal displayed
cy.get(tableWithdrawnStatus)
.eq(1, txTimeout)
.should('have.text', 'Completed')
.parent()
.within(() => {
cy.get(tableAssetSymbol).should('have.text', usdcSymbol);
cy.get(tableAmount).should('have.text', '120.00');
cy.get(tableReceiverAddress)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/address/');
cy.get(tableWithdrawnTimeStamp).should('not.be.empty');
cy.get(tableTxHash)
.find('a')
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/tx/');
});
}
);
it('Able to withdraw asset: -eth wallet not connected', function () {
const ethWalletAddress = Cypress.env('ethWalletPublicKey');
@@ -11,7 +11,7 @@ import {
goToMakeNewProposal,
governanceProposalType,
} from '../../support/governance.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-functions';
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey2');
const vegaPubkeyTruncated = Cypress.env('vegaWalletPublicKey2Short');
@@ -5,7 +5,10 @@ import {
verifyPageHeader,
verifyTabHighlighted,
} from '../../support/common.functions';
import { clickOnValidatorFromList } from '../../support/staking.functions';
import {
clickOnValidatorFromList,
waitForBeginningOfEpoch,
} from '../../support/staking.functions';
const guideLink = '[data-testid="staking-guide-link"]';
const validatorTitle = '[data-testid="validator-node-title"]';
@@ -27,7 +30,6 @@ const normalisedVotingPowerToolTip =
'[data-testid="normalised-voting-power-tooltip"]';
const performancePenaltyToolTip = '[data-testid="performance-penalty-tooltip"]';
const overstakedPenaltyToolTip = '[data-testid="overstaked-penalty-tooltip"]';
const totalPenaltyToolTip = '[data-testid="total-penalty-tooltip"]';
const epochCountDown = '[data-testid="epoch-countdown"]';
const stakeNumberRegex = /^\d{1,3}(,\d{3})*(\.\d+)?$/;
@@ -37,206 +39,210 @@ context('Validators Page - verify elements on page', function () {
});
describe('with wallets disconnected', { tags: '@smoke' }, function () {
describe('description section', function () {
it('Should have validators tab highlighted', function () {
verifyTabHighlighted(navigation.validators);
});
it('Should have validators ON VEGA header visible', function () {
verifyPageHeader('Validators');
});
it('Should have Staking Guide link visible', function () {
// 2001-STKE-003
cy.get(guideLink)
.should('be.visible')
.and('have.text', 'Read more about staking on Vega')
.and(
'have.attr',
'href',
'https://docs.vega.xyz/mainnet/concepts/vega-chain/#staking-on-vega'
);
});
it('Should have validators tab highlighted', function () {
verifyTabHighlighted(navigation.validators);
});
describe(
'Should be able to see validator list from the staking page',
{ tags: '@regression' },
function () {
// 2001-STKE-050
it('Should be able to see validator names', function () {
cy.get('[col-id="validator"] > div > span')
.should('have.length.at.least', 1)
.each(($name) => {
cy.wrap($name).should('not.be.empty');
});
});
it('Should be able to see validator stake', function () {
cy.getByTestId('total-stake')
.should('have.length.at.least', 1)
.each(($stake) => {
cy.wrap($stake).should('not.be.empty');
});
});
it('Should have validators ON VEGA header visible', function () {
verifyPageHeader('Validators');
});
it('Should be able to see validator stake tooltip', function () {
cy.getByTestId('total-stake').first().realHover();
it('Should have Staking Guide link visible', function () {
// 1002-STKE-003
cy.get(guideLink)
.should('be.visible')
.and('have.text', 'Read more about staking on Vega')
.and(
'have.attr',
'href',
'https://docs.vega.xyz/mainnet/concepts/vega-chain/#staking-on-vega'
);
});
cy.get(stakedByOperatorToolTip)
.invoke('text')
.should('contain', 'Staked by operator: 3,000.00');
cy.get(stakedByDelegatesToolTip)
.invoke('text')
.should('contain', 'Staked by delegates: 0.00');
cy.get(totalStakedToolTip)
.invoke('text')
.should('contain', 'Total stake: 3,000.00');
});
it('Should be able to see validator normalised voting power', function () {
cy.getByTestId('normalised-voting-power')
.should('have.length.at.least', 1)
.each(($vPower) => {
cy.wrap($vPower).should('not.be.empty');
});
});
it('Should be able to see validator normalised voting power tooltip', function () {
cy.getByTestId('normalised-voting-power').first().realHover();
cy.get(unnormalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Unnormalised voting power: 20.00%');
cy.get(normalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Normalised voting power: 50.00%');
});
// 2002-SINC-018
it('Should be able to see validator total penalties', function () {
cy.getByTestId('total-penalty')
.should('have.length.at.least', 1)
.each(($penalties) => {
cy.wrap($penalties).should('contain.text', '0%');
});
});
it('Should be able to see validator penalties tooltip', function () {
cy.getByTestId('total-penalty').realHover();
cy.get(performancePenaltyToolTip)
.invoke('text')
.should('contain', 'Performance penalty: 0.00%');
cy.get(overstakedPenaltyToolTip)
.invoke('text')
.should('contain', 'Overstaked penalty: 60.00%'); // value not asserted due to #2886
cy.get(totalPenaltyToolTip)
.invoke('text')
.should('contain', 'Total penalties: 60.00%');
});
it('Should be able to see validator pending stake', function () {
cy.getByTestId('total-pending-stake')
.should('have.length.at.least', 1)
.each(($pendingStake) => {
cy.wrap($pendingStake).should('contain.text', '0.00');
});
});
}
);
// 2001-STKE-050
describe(
'Should be able to see static information about a validator',
{ tags: '@smoke' },
function () {
before('connect wallets and click on validator', function () {
cy.connectVegaWallet();
clickOnValidatorFromList(0);
});
// 2001-STKE-006
it('Should be able to see validator name', function () {
cy.get(validatorTitle).should('not.be.empty');
});
// 2001-STKE-007
it('Should be able to see validator id', function () {
cy.get(validatorId).should('not.be.empty');
});
// 2001-STKE-008
it('Should be able to see validator public key', function () {
cy.get(validatorPubKey).should('not.be.empty');
});
// 2001-STKE-010
it('Should be able to see Ethereum address', function () {
cy.get(ethAddressLink)
.should('not.be.empty')
.and('have.attr', 'href');
});
// TODO validators missing url for more information about them 2001-STKE-09
it('Should be able to see validator status', function () {
cy.get(validatorStatus).should('have.text', 'Consensus');
});
// 2001-STKE-012
it('Should be able to see total stake', function () {
cy.get(totalStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see pending stake', function () {
cy.get(pendingStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see staked by operator', function () {
cy.get(stakedByOperator)
.invoke('text')
.should('match', stakeNumberRegex);
});
it('Should be able to see staked by delegates', function () {
cy.get(stakedByDelegates)
.invoke('text')
.should('match', stakeNumberRegex);
});
// 2001-STKE-051
it('Should be able to see stake share in percentage', function () {
cy.get(stakeShare)
.invoke('text')
.then(($stakePercentage) => {
// The pattern must start at a word boundary (\b).
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
// The pattern can be one of the following:
// A percentage value of zero (0%), or
// A non-zero percentage value that can be:
// A single digit (\d) between 0 and 9, or
// A two-digit number between 0 and 99 (\d{1,2}), or
// The number 100.
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
// The pattern must end with a percentage sign (%).
cy.wrap($stakePercentage).should(
'match',
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
);
});
});
// 2001-STKE-011 2002-SINC-001 2002-SINC-002
it('Should be able to see epoch information', function () {
const epochTitle = 'h3';
const nextEpochInfo = 'p';
cy.get(epochCountDown).within(() => {
cy.get(epochTitle).should('not.be.empty');
cy.get(nextEpochInfo).should('contain.text', 'Next epoch');
});
});
}
);
// 1002-STKE-032
it('Should have button to connect vega wallet in validator page', function () {
clickOnValidatorFromList(0);
cy.getByTestId('connect-to-vega-wallet-btn').should('be.visible');
cy.visit('/validators');
});
});
// 1002-STKE-020 1002-STKE-021 1002-STKE-022 1002-STKE-023 1002-STKE-024
describe(
'Should be able to see validator list from the staking page',
{ tags: '@regression' },
function () {
// 1002-STKE-050
it('Should be able to see validator names', function () {
cy.get('[col-id="validator"] > div > span')
.should('have.length.at.least', 1)
.each(($name) => {
cy.wrap($name).should('not.be.empty');
});
});
it('Should be able to see validator stake', function () {
cy.getByTestId('total-stake')
.should('have.length.at.least', 1)
.each(($stake) => {
cy.wrap($stake).should('not.be.empty');
});
});
it('Should be able to see validator stake tooltip', function () {
waitForBeginningOfEpoch();
cy.getByTestId('total-stake').first().realHover();
cy.get(stakedByOperatorToolTip)
.invoke('text')
.should('contain', 'Staked by operator: 3,000.00');
cy.get(stakedByDelegatesToolTip)
.invoke('text')
.should('contain', 'Staked by delegates: 0.00');
cy.get(totalStakedToolTip)
.invoke('text')
.should('contain', 'Total stake: 3,000.00');
});
it('Should be able to see validator normalised voting power', function () {
cy.getByTestId('normalised-voting-power')
.should('have.length.at.least', 1)
.each(($vPower) => {
cy.wrap($vPower).should('not.be.empty');
});
});
it('Should be able to see validator normalised voting power tooltip', function () {
waitForBeginningOfEpoch();
cy.getByTestId('normalised-voting-power').first().realHover();
cy.get(unnormalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Unnormalised voting power: 20.00%');
cy.get(normalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Normalised voting power: 50.00%');
});
// 2002-SINC-018
it('Should be able to see validator total penalties', function () {
cy.getByTestId('total-penalty')
.should('have.length.at.least', 1)
.each(($penalties) => {
cy.wrap($penalties).should('contain.text', '0%');
});
});
it('Should be able to see validator penalties tooltip', function () {
waitForBeginningOfEpoch();
cy.getByTestId('total-penalty').realHover();
cy.get(performancePenaltyToolTip)
.invoke('text')
.should('contain', 'Performance penalty: 0.00%');
cy.get(overstakedPenaltyToolTip)
.invoke('text')
.should('contain', 'Overstaked penalty: 60.00%'); // value not asserted due to #2886
});
it('Should be able to see validator pending stake', function () {
cy.getByTestId('total-pending-stake')
.should('have.length.at.least', 1)
.each(($pendingStake) => {
cy.wrap($pendingStake).should('contain.text', '0.00');
});
});
}
);
// 1002-STKE-050
describe(
'Should be able to see static information about a validator',
{ tags: '@smoke' },
function () {
before('connect wallets and click on validator', function () {
cy.connectVegaWallet();
clickOnValidatorFromList(0);
});
// 1002-STKE-006
it('Should be able to see validator name', function () {
cy.get(validatorTitle).should('not.be.empty');
});
// 1002-STKE-007
it('Should be able to see validator id', function () {
cy.get(validatorId).should('not.be.empty');
});
// 1002-STKE-008
it('Should be able to see validator public key', function () {
cy.get(validatorPubKey).should('not.be.empty');
});
// 1002-STKE-010
it('Should be able to see Ethereum address', function () {
cy.get(ethAddressLink).should('not.be.empty').and('have.attr', 'href');
});
// TODO validators missing url for more information about them 1002-STKE-09
it('Should be able to see validator status', function () {
cy.get(validatorStatus).should('have.text', 'Consensus');
});
// 1002-STKE-012
it('Should be able to see total stake', function () {
cy.get(totalStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see pending stake', function () {
cy.get(pendingStake).invoke('text').should('match', stakeNumberRegex);
});
it('Should be able to see staked by operator', function () {
cy.get(stakedByOperator)
.invoke('text')
.should('match', stakeNumberRegex);
});
it('Should be able to see staked by delegates', function () {
cy.get(stakedByDelegates)
.invoke('text')
.should('match', stakeNumberRegex);
});
// 1002-STKE-051
it('Should be able to see stake share in percentage', function () {
cy.get(stakeShare)
.invoke('text')
.then(($stakePercentage) => {
// The pattern must start at a word boundary (\b).
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
// The pattern can be one of the following:
// A percentage value of zero (0%), or
// A non-zero percentage value that can be:
// A single digit (\d) between 0 and 9, or
// A two-digit number between 0 and 99 (\d{1,2}), or
// The number 100.
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
// The pattern must end with a percentage sign (%).
cy.wrap($stakePercentage).should(
'match',
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
);
});
});
// 1002-STKE-011 2002-SINC-001 2002-SINC-002
it('Should be able to see epoch information', function () {
const epochTitle = 'h3';
const nextEpochInfo = 'p';
cy.get(epochCountDown).within(() => {
cy.get(epochTitle).should('not.be.empty');
cy.get(nextEpochInfo).should('contain.text', 'Next epoch');
});
});
}
);
});
@@ -1,7 +1,9 @@
import { truncateByChars } from '@vegaprotocol/utils';
import { waitForSpinner } from '../../support/common.functions';
import { vegaWalletTeardown } from '../../support/wallet-teardown.functions';
import { vegaWalletFaucetAssetsWithoutCheck } from '../../support/wallet-vega.functions';
import {
vegaWalletFaucetAssetsWithoutCheck,
vegaWalletTeardown,
} from '../../support/wallet-functions';
const walletContainer = 'aside [data-testid="vega-wallet"]';
const walletHeader = '[data-testid="wallet-header"] h1';
@@ -280,13 +282,6 @@ context(
// 2002-SINC-016
describe('Vega wallet with assets', function () {
const assets = [
{
id: '816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc',
name: 'USDC (fake)',
symbol: 'fUSDC',
amount: '1000000',
expectedAmount: '10.00',
},
{
id: '8566db7257222b5b7ef2886394ad28b938b28680a54a169bbc795027b89d6665',
name: 'DAI (fake)',
@@ -308,6 +303,13 @@ context(
amount: '800000',
expectedAmount: '8.00',
},
{
id: '816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc',
name: 'USDC (fake)',
symbol: 'fUSDC',
amount: '1000000',
expectedAmount: '10.00',
},
];
before('faucet assets to connected vega wallet', function () {
@@ -337,6 +339,7 @@ context(
.parent()
.siblings()
.invoke('text')
.should('have.length.at.least', 4)
.then(parseFloat)
.should('be.gte', parseFloat(expectedAmount));
+1 -2
View File
@@ -5,8 +5,7 @@ import './common.functions.ts';
import './staking.functions.ts';
import './governance.functions.ts';
import './wallet-eth.functions.ts';
import './wallet-teardown.functions.ts';
import './wallet-vega.functions.ts';
import './wallet-functions.ts';
import './proposal.functions.ts';
import 'cypress-mochawesome-reporter/register';
import registerCypressGrep from '@cypress/grep';
@@ -1,5 +1,5 @@
import { closeDialog } from './common.functions';
import { vegaWalletTeardown } from './wallet-teardown.functions';
import { vegaWalletTeardown } from './wallet-functions';
const tokenAmountInputBox = '[data-testid="token-amount-input"]';
const tokenSubmitButton = '[data-testid="token-input-submit-button"]';
@@ -173,3 +173,27 @@ export async function vegaWalletDisassociate(amount: string) {
amount = amount + '0'.repeat(18);
stakingBridgeContract.remove_stake(amount, vegaWalletPubKey);
}
export function vegaWalletFaucetAssetsWithoutCheck(
asset: string,
amount: string,
vegaWalletPublicKey: string
) {
cy.highlight(`Topping up vega wallet with ${asset}, amount: ${amount}`);
cy.exec(
`curl -X POST -d '{"amount": "${amount}", "asset": "${asset}", "party": "${vegaWalletPublicKey}"}' http://localhost:1790/api/v1/mint`
)
.its('stdout')
.then((response) => {
assert.include(
response,
`"success":true`,
'Ensuring curl command was successfully undertaken'
);
});
}
export function switchVegaWalletPubKey() {
cy.get('[data-testid="manage-vega-wallet"]:visible').click();
cy.get('[data-testid="select-keypair-button"]').eq(0).click();
}
@@ -1,18 +0,0 @@
export function vegaWalletFaucetAssetsWithoutCheck(
asset: string,
amount: string,
vegaWalletPublicKey: string
) {
cy.highlight(`Topping up vega wallet with ${asset}, amount: ${amount}`);
cy.exec(
`curl -X POST -d '{"amount": "${amount}", "asset": "${asset}", "party": "${vegaWalletPublicKey}"}' http://localhost:1790/api/v1/mint`
)
.its('stdout')
.then((response) => {
assert.include(
response,
`"success":true`,
'Ensuring curl command was successfully undertaken'
);
});
}
@@ -596,6 +596,8 @@
"noPercentage": "No percentage",
"proposalJson": "Full proposal JSON",
"proposalDetails": "Proposal details",
"marketSpecification": "Market specification",
"viewMarketJson": "View market JSON",
"proposalDescription": "Description",
"currentlySetTo": "Currently expected to ",
"currently": "currently",
@@ -729,7 +731,11 @@
"ThisWillSetValidationDeadlineTo": "This will set the validation deadline to",
"Hours": "hours",
"ThisWillAdd2MinutesToAllowTimeToConfirmInWallet": "Note: 2 minutes of extra time are added when you choose the minimum value. This gives you time to confirm the proposal in your wallet.",
"ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "Proposal will fail if enactment is earlier than the voting deadline",
"ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline": "The proposal will fail if enactment is earlier than the voting deadline",
"ProposalWillFailIfEnactmentIsBelowTheMinimumDeadline": "The proposal will fail if enactment deadline is below the minimum",
"ProposalWillFailIfVotingIsBelowTheMinimumDeadline": "The proposal will fail if voting deadline is below the minimum",
"ProposalWillFailIfEnactmentIsAboveTheMaximumDeadline": "The proposal will fail if enactment deadline is above the maximum",
"ProposalWillFailIfVotingIsAboveTheMaximumDeadline": "The proposal will fail if voting deadline is above the maximum",
"SelectAMarketToChange": "Select a market to change",
"MarketName": "Market name",
"MarketCode": "Market code",
@@ -780,6 +786,7 @@
"performancePenalty": "Performance penalty",
"overstaked": "Overstaked",
"overstakedPenalty": "Overstaked penalty",
"multisigPenalty": "Multisig penalty",
"homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject. Network upgrades are proposed and approved by validators.",
"homeProposalsButtonText": "Browse, vote, and propose",
"homeValidatorsIntro": "Vega runs on a delegated proof of stake blockchain, where validators earn fees for validating block transactions. Tokenholders can nominate validators by staking tokens to them.",
@@ -0,0 +1,6 @@
import classnames from 'classnames';
export const collapsibleToggleStyles = (toggleState: boolean) =>
classnames('mb-4 transition-transform ease-in-out duration-300', {
'rotate-180': toggleState,
});
@@ -1,9 +1,9 @@
import ReactMarkdown from 'react-markdown';
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
export const ProposalDescription = ({
description,
@@ -12,9 +12,6 @@ export const ProposalDescription = ({
}) => {
const { t } = useTranslation();
const [showDescription, setShowDescription] = useState(false);
const showDescriptionIconClasses = classnames('mb-4', {
'rotate-180': showDescription,
});
return (
<section data-testid="proposal-description">
@@ -24,7 +21,7 @@ export const ProposalDescription = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalDescription')} />
<div className={showDescriptionIconClasses}>
<div className={collapsibleToggleStyles(showDescription)}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -1,8 +1,8 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@@ -13,9 +13,6 @@ export const ProposalJson = ({
}) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return (
<section data-testid="proposal-json">
@@ -25,7 +22,7 @@ export const ProposalJson = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalJson')} />
<div className={showDetailsIconClasses}>
<div className={collapsibleToggleStyles(showDetails)}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -0,0 +1 @@
export * from './proposal-market-data';
@@ -0,0 +1,222 @@
import isEqual from 'lodash/isEqual';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
InstrumentInfoPanel,
KeyDetailsInfoPanel,
LiquidityMonitoringParametersInfoPanel,
LiquidityPriceRangeInfoPanel,
MetadataInfoPanel,
OracleInfoPanel,
PriceMonitoringBoundsInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
} from '@vegaprotocol/markets';
import {
Accordion,
AccordionItem,
Button,
CopyWithTooltip,
Dialog,
Icon,
SyntaxHighlighter,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import { create } from 'zustand';
type MarketDataDialogState = {
isOpen: boolean;
open: () => void;
close: () => void;
};
export const useMarketDataDialogStore = create<MarketDataDialogState>(
(set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
})
);
export const ProposalMarketData = ({
marketData,
}: {
marketData: MarketInfoWithData;
}) => {
const { t } = useTranslation();
const { isOpen, open, close } = useMarketDataDialogStore();
const [showDetails, setShowDetails] = useState(false);
if (!marketData) {
return null;
}
const settlementData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
const terminationData = marketData.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
return signers.map(({ signer }) => {
return (
(signer.__typename === 'ETHAddress' && signer.address) ||
(signer.__typename === 'PubKey' && signer.key)
);
});
}
return [];
};
return (
<section className="relative" data-testid="proposal-market-data">
<button
onClick={() => setShowDetails(!showDetails)}
data-testid="proposal-market-data-toggle"
>
<div className="flex items-center gap-3">
<SubHeading title={t('marketSpecification')} />
<div className={collapsibleToggleStyles(showDetails)}>
<Icon name="chevron-down" size={8} />
</div>
</div>
</button>
{showDetails && (
<>
<div className="float-right">
<Button onClick={open} data-testid="view-market-json">
{t('viewMarketJson')}
</Button>
</div>
<div className="mb-10">
<Accordion>
<AccordionItem
itemId="key-details"
title={t('Key details')}
content={<KeyDetailsInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="instrument"
title={t('Instrument')}
content={<InstrumentInfoPanel market={marketData} />}
/>
{isEqual(
getSigners(settlementData),
getSigners(terminationData)
) ? (
<AccordionItem
itemId="oracles"
title={t('Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
/>
}
/>
) : (
<>
<AccordionItem
itemId="settlement-oracle"
title={t('Settlement Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
/>
}
/>
<AccordionItem
itemId="termination-oracle"
title={t('Termination Oracle')}
content={
<OracleInfoPanel market={marketData} type="termination" />
}
/>
</>
)}
<AccordionItem
itemId="settlement-asset"
title={t('Settlement asset')}
content={<SettlementAssetInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="metadata"
title={t('Metadata')}
content={<MetadataInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-model"
title={t('Risk model')}
content={<RiskModelInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={<RiskParametersInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="risk-factors"
title={t('Risk factors')}
content={<RiskFactorsInfoPanel market={marketData} />}
/>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
).map((_, triggerIndex) => (
<AccordionItem
itemId={`trigger-${triggerIndex}`}
title={t(`Price monitoring bounds ${triggerIndex + 1}`)}
content={
<PriceMonitoringBoundsInfoPanel
market={marketData}
triggerIndex={triggerIndex}
/>
}
/>
))}
<AccordionItem
itemId="liqudity-monitoring-parameters"
title={t('Liquidity monitoring parameters')}
content={
<LiquidityMonitoringParametersInfoPanel market={marketData} />
}
/>
<AccordionItem
itemId="liquidity-price-range"
title={t('Liquidity price range')}
content={<LiquidityPriceRangeInfoPanel market={marketData} />}
/>
</Accordion>
</div>
</>
)}
<Dialog
title={marketData.tradableInstrument.instrument.code}
open={isOpen}
onChange={(isOpen) => (isOpen ? open() : close())}
size="medium"
dataTestId="market-json-dialog"
>
<CopyWithTooltip text={JSON.stringify(marketData)}>
<button className="bg-vega-dark-100 rounded-sm py-2 px-3 mb-4 text-white">
<span>
<Icon name="duplicate" />
</span>
<span className="ml-2">Copy</span>
</button>
</CopyWithTooltip>
<SyntaxHighlighter data={marketData} />
</Dialog>
</section>
);
};
@@ -1,4 +1,3 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -13,6 +12,7 @@ import { SubHeading } from '../../../../components/heading';
import { useVoteInformation } from '../../hooks';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { ProposalType } from '../proposal/proposal';
import { collapsibleToggleStyles } from '../../../../lib/collapsible-toggle-styles';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@@ -57,10 +57,6 @@ export const ProposalVotesTable = ({
? t('byTokenVote')
: t('byLiquidityVote');
const showDetailsIconClasses = classnames('mb-4', {
'rotate-180': showDetails,
});
return (
<>
<button
@@ -69,7 +65,7 @@ export const ProposalVotesTable = ({
>
<div className="flex items-center gap-3">
<SubHeading title={t('voteBreakdown')} />
<div className={showDetailsIconClasses}>
<div className={collapsibleToggleStyles(showDetails)}>
<Icon name="chevron-down" size={8} />
</div>
</div>
@@ -29,9 +29,6 @@ jest.mock('../proposal-change-table', () => ({
jest.mock('../proposal-json', () => ({
ProposalJson: () => <div data-testid="proposal-json"></div>,
}));
jest.mock('../proposal-terms/proposal-terms', () => ({
ProposalTerms: () => <div data-testid="proposal-terms"></div>,
}));
jest.mock('../proposal-votes-table', () => ({
ProposalVotesTable: () => <div data-testid="proposal-votes-table"></div>,
}));
@@ -74,7 +71,6 @@ it('renders each section', async () => {
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
expect(screen.getByTestId('proposal-terms')).toBeInTheDocument();
expect(screen.getByTestId('proposal-votes-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-vote-details')).toBeInTheDocument();
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();
@@ -1,22 +1,22 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { AsyncRenderer, Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalDescription } from '../proposal-description';
import { ProposalChangeTable } from '../proposal-change-table';
import { ProposalJson } from '../proposal-json';
import { ProposalTerms } from '../proposal-terms';
import { ProposalVotesTable } from '../proposal-votes-table';
import { VoteDetails } from '../vote-details';
import { ListAsset } from '../list-asset';
import { Link } from 'react-router-dom';
import Routes from '../../../routes';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ProposalMarketData } from '../proposal-market-data';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
export enum ProposalType {
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
@@ -28,11 +28,16 @@ export enum ProposalType {
}
export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
newMarketData?: MarketInfoWithData | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: any;
}
export const Proposal = ({ proposal, restData }: ProposalProps) => {
export const Proposal = ({
proposal,
restData,
newMarketData,
}: ProposalProps) => {
const { t } = useTranslation();
const { params, loading, error } = useNetworkParams([
NetworkParams.governance_proposal_market_minVoterBalance,
@@ -97,51 +102,58 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
</div>
<ProposalHeader proposal={proposal} isListItem={false} />
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
<div id="details">
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={
proposal.terms.change.source.withdrawThreshold
}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
{proposal.terms.change.__typename !== 'NewMarket' &&
proposal.terms.change.__typename !== 'UpdateMarket' &&
proposal.terms.change.__typename !== 'NewFreeform' && (
{newMarketData && (
<div className="mb-4">
<ProposalTerms data={proposal.terms} />
<ProposalMarketData marketData={newMarketData} />
</div>
)}
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
</div>
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
<div id="voting">
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
params?.spam_protection_voting_min_tokens
}
/>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</div>
</div>
</section>
</AsyncRenderer>
@@ -224,7 +224,47 @@ describe('Proposal form vote, validation and enactment deadline', () => {
expect(
screen.getByTestId('enactment-before-voting-deadline')
).toHaveTextContent(
'Proposal will fail if enactment is earlier than the voting deadline'
'The proposal will fail if enactment is earlier than the voting deadline'
);
});
it('displays error text if the vote deadline is set earlier than the minimum allowed', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
fireEvent.change(voteDeadlineInput, { target: { value: 0.01 } });
expect(screen.getByTestId('voting-less-than-min')).toHaveTextContent(
'The proposal will fail if voting deadline is below the minimum'
);
});
it('displays error text if the vote deadline is set later than the maximum allowed', () => {
renderComponent();
const voteDeadlineInput = screen.getByTestId('proposal-vote-deadline');
fireEvent.change(voteDeadlineInput, { target: { value: 100000 } });
expect(screen.getByTestId('voting-greater-than-max')).toHaveTextContent(
'The proposal will fail if voting deadline is above the maximum'
);
});
it('displays error text if the enactment deadline is set earlier than the minimum allowed', () => {
renderComponent();
const enactmentDeadlineInput = screen.getByTestId(
'proposal-enactment-deadline'
);
fireEvent.change(enactmentDeadlineInput, { target: { value: 0.01 } });
expect(screen.getByTestId('enactment-less-than-min')).toHaveTextContent(
'The proposal will fail if enactment deadline is below the minimum'
);
});
it('displays error text if the enactment deadline is set later than the maximum allowed', () => {
renderComponent();
const enactmentDeadlineInput = screen.getByTestId(
'proposal-enactment-deadline'
);
fireEvent.change(enactmentDeadlineInput, { target: { value: 100000 } });
expect(screen.getByTestId('enactment-greater-than-max')).toHaveTextContent(
'The proposal will fail if enactment deadline is above the maximum'
);
});
});
@@ -220,21 +220,41 @@ const EnactmentForm = ({
<span data-testid="enactment-date" className="pl-2">
{getDateTimeFormat().format(deadlineDates.enactment)}
</span>
{deadlines.enactment === minEnactmentHours && (
<span
data-testid="enactment-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.enactment && deadlines.enactment < deadlines.vote && (
<span
data-testid="enactment-before-voting-deadline"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline')}
</span>
{deadlines.enactment && (
<>
{deadlines.enactment === minEnactmentHours && (
<span
data-testid="enactment-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.enactment < deadlines.vote && (
<span
data-testid="enactment-before-voting-deadline"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsEarlierThanVotingDeadline')}
</span>
)}
{deadlines.enactment < minEnactmentHours && (
<span
data-testid="enactment-less-than-min"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsBelowTheMinimumDeadline')}
</span>
)}
{deadlines.enactment > maxEnactmentHours && (
<span
data-testid="enactment-greater-than-max"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfEnactmentIsAboveTheMaximumDeadline')}
</span>
)}
</>
)}
</p>
)}
@@ -500,13 +520,33 @@ export function ProposalFormVoteAndEnactmentDeadline({
<span data-testid="voting-date" className="pl-2">
{getDateTimeFormat().format(deadlineDates.vote)}
</span>
{deadlines.vote === minVoteHours && (
<span
data-testid="voting-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
{deadlines.vote && (
<>
{deadlines.vote === minVoteHours && (
<span
data-testid="voting-2-mins-extra"
className="block mt-4 font-light"
>
{t('ThisWillAdd2MinutesToAllowTimeToConfirmInWallet')}
</span>
)}
{deadlines.vote < minVoteHours && (
<span
data-testid="voting-less-than-min"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfVotingIsBelowTheMinimumDeadline')}
</span>
)}
{deadlines.vote > maxVoteHours && (
<span
data-testid="voting-greater-than-max"
className="block mt-4 text-vega-pink"
>
{t('ProposalWillFailIfVotingIsAboveTheMaximumDeadline')}
</span>
)}
</>
)}
</p>
)}
@@ -1,11 +1,10 @@
import { captureMessage } from '@sentry/minimal';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { VoteValue } from '@vegaprotocol/types';
import { useEffect, useState } from 'react';
import { useUserVoteQuery } from './__generated__/Vote';
import type { FinalizedVote } from '@vegaprotocol/proposals';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { FinalizedVote } from '@vegaprotocol/proposals';
export enum VoteState {
NotCast = 'NotCast',
@@ -45,21 +44,23 @@ export function useUserVote(
);
useEffect(() => {
if (finalizedVote?.vote.value) {
if (finalizedVote?.vote.value && finalizedVote.pubKey === pubKey) {
setUserVote(finalizedVote);
} else if (data?.party?.votesConnection?.edges) {
// This sets the vote (if any) when the user first loads the page
setUserVote(
removePaginationWrapper(data?.party?.votesConnection?.edges).find(
({ proposalId: pId }) => proposalId === pId
)
);
} else if (data?.party?.votesConnection?.edges && pubKey) {
const vote = removePaginationWrapper(
data?.party?.votesConnection?.edges
).find(({ proposalId: pId }) => proposalId === pId);
if (vote) {
setUserVote({ ...vote, pubKey });
}
}
}, [
finalizedVote?.vote.value,
data?.party?.votesConnection?.edges,
finalizedVote,
proposalId,
pubKey,
]);
// If user vote changes update the vote state
@@ -94,6 +95,8 @@ export function useUserVote(
return {
voteState,
userVote,
voteDatetime: userVote ? new Date(userVote.vote.datetime) : null,
voteDatetime: userVote?.vote?.datetime
? new Date(userVote.vote.datetime)
: null,
};
}
@@ -7,6 +7,8 @@ import { ProposalNotFound } from '../components/proposal-not-found';
import { useProposalQuery } from './__generated__/Proposal';
import { useFetch } from '@vegaprotocol/react-helpers';
import { ENV } from '../../../config';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketInfoWithDataProvider } from '@vegaprotocol/markets';
export const ProposalContainer = () => {
const params = useParams<{ proposalId: string }>();
@@ -20,15 +22,36 @@ export const ProposalContainer = () => {
skip: !params.proposalId,
});
const {
data: newMarketData,
loading: newMarketLoading,
error: newMarketError,
} = useDataProvider({
dataProvider: marketInfoWithDataProvider,
skipUpdates: true,
variables: {
marketId: data?.proposal?.id || '',
skip: !data?.proposal?.id,
},
});
useEffect(() => {
const interval = setInterval(refetch, 1000);
const interval = setInterval(refetch, 2000);
return () => clearInterval(interval);
}, [refetch]);
return (
<AsyncRenderer loading={loading} error={error} data={data}>
<AsyncRenderer
loading={loading || newMarketLoading}
error={error || newMarketError}
data={newMarketData ? { newMarketData, data } : data}
>
{data?.proposal ? (
<Proposal proposal={data.proposal} restData={restData} />
<Proposal
proposal={data.proposal}
restData={restData}
newMarketData={newMarketData}
/>
) : (
<ProposalNotFound />
)}
@@ -48,15 +48,22 @@ export const EpochIndividualRewards = ({
return removePaginationWrapper(data.party.rewardsConnection.edges);
}, [data]);
const epochRewardSummaries = useMemo(() => {
if (!data?.epochRewardSummaries) return [];
return removePaginationWrapper(data.epochRewardSummaries.edges);
}, [data]);
const epochIndividualRewardSummaries = useMemo(() => {
if (!data?.party) return [];
return generateEpochIndividualRewardsList({
rewards,
epochId,
epochRewardSummaries,
page,
size: EPOCHS_PAGE_SIZE,
});
}, [data?.party, epochId, page, rewards]);
}, [data?.party, epochId, epochRewardSummaries, page, rewards]);
const refetchData = useCallback(
async (toPage?: number) => {
@@ -65,7 +65,11 @@ describe('generateEpochIndividualRewardsList', () => {
it('should return an empty array if no rewards are provided', () => {
expect(
generateEpochIndividualRewardsList({ rewards: [], epochId: 1 })
generateEpochIndividualRewardsList({
rewards: [],
epochId: 1,
epochRewardSummaries: [],
})
).toEqual([
{
epoch: 1,
@@ -78,6 +82,7 @@ describe('generateEpochIndividualRewardsList', () => {
const result = generateEpochIndividualRewardsList({
rewards: [rewardWrongType],
epochId: 1,
epochRewardSummaries: [],
});
expect(result).toEqual([
@@ -92,6 +97,15 @@ describe('generateEpochIndividualRewardsList', () => {
const result = generateEpochIndividualRewardsList({
rewards: [reward1],
epochId: 1,
epochRewardSummaries: [
{
__typename: 'EpochRewardSummary',
epoch: 1,
assetId: 'usd',
amount: '100000',
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
},
],
});
expect(result[0]).toEqual({
@@ -134,7 +148,11 @@ describe('generateEpochIndividualRewardsList', () => {
it('should return an array sorted by epoch descending', () => {
const rewards = [reward1, reward2, reward3, reward4];
const result1 = generateEpochIndividualRewardsList({ rewards, epochId: 2 });
const result1 = generateEpochIndividualRewardsList({
rewards,
epochId: 2,
epochRewardSummaries: [],
});
expect(result1[0].epoch).toEqual(2);
expect(result1[1].epoch).toEqual(1);
@@ -143,6 +161,7 @@ describe('generateEpochIndividualRewardsList', () => {
const result2 = generateEpochIndividualRewardsList({
rewards: reorderedRewards,
epochId: 2,
epochRewardSummaries: [],
});
expect(result2[0].epoch).toEqual(2);
@@ -151,7 +170,11 @@ describe('generateEpochIndividualRewardsList', () => {
it('correctly calculates the total value of rewards for an asset', () => {
const rewards = [reward1, reward4];
const result = generateEpochIndividualRewardsList({ rewards, epochId: 1 });
const result = generateEpochIndividualRewardsList({
rewards,
epochId: 1,
epochRewardSummaries: [],
});
expect(result[0].rewards[0].totalAmount).toEqual('200');
});
@@ -159,7 +182,11 @@ describe('generateEpochIndividualRewardsList', () => {
it('returns data in the expected shape', () => {
// Just sanity checking the whole structure here
const rewards = [reward1, reward2, reward3, reward4];
const result = generateEpochIndividualRewardsList({ rewards, epochId: 2 });
const result = generateEpochIndividualRewardsList({
rewards,
epochId: 2,
epochRewardSummaries: [],
});
expect(result).toEqual([
{
@@ -273,6 +300,7 @@ describe('generateEpochIndividualRewardsList', () => {
const resultPageOne = generateEpochIndividualRewardsList({
rewards,
epochId: 3,
epochRewardSummaries: [],
page: 1,
size: 2,
});
@@ -386,6 +414,7 @@ describe('generateEpochIndividualRewardsList', () => {
const resultPageTwo = generateEpochIndividualRewardsList({
rewards,
epochId: 3,
epochRewardSummaries: [],
page: 2,
size: 2,
});
@@ -429,4 +458,69 @@ describe('generateEpochIndividualRewardsList', () => {
},
]);
});
it('correctly calculates the percentage of two or more rewards by referencing the total rewards amount', () => {
const result = generateEpochIndividualRewardsList({
rewards: [
// reward1 is 100 usd, which is 10% of the total rewards amount
reward1,
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '200',
percentageOfTotal: '0.2',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD', name: 'USD', decimals: 6 },
party: { id: 'blah' },
epoch: { id: '1' },
},
],
epochId: 1,
epochRewardSummaries: [
{
__typename: 'EpochRewardSummary',
epoch: 1,
assetId: 'usd',
amount: '1000',
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
},
],
});
expect(result[0]).toEqual({
epoch: 1,
rewards: [
{
asset: 'USD',
decimals: 6,
totalAmount: '300',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '300',
percentageOfTotal: '30',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
});
});
});
@@ -1,6 +1,9 @@
import { BigNumber } from '../../../lib/bignumber';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { RewardFieldsFragment } from '../home/__generated__/Rewards';
import type {
EpochRewardSummaryFieldsFragment,
RewardFieldsFragment,
} from '../home/__generated__/Rewards';
import type { AccountType } from '@vegaprotocol/types';
import { calculateEpochOffset } from '../../../lib/epoch-pagination';
@@ -32,11 +35,13 @@ const emptyRowAccountTypes = accountTypes.map((type) => [
export const generateEpochIndividualRewardsList = ({
rewards,
epochId,
epochRewardSummaries,
page = 1,
size = 10,
}: {
rewards: RewardFieldsFragment[];
epochId: number;
epochRewardSummaries: EpochRewardSummaryFieldsFragment[];
page?: number;
size?: number;
}) => {
@@ -54,6 +59,7 @@ export const generateEpochIndividualRewardsList = ({
const epochIndividualRewards = rewards.reduce((acc, reward) => {
const epochId = reward.epoch.id;
const assetName = reward.asset.name;
const assetId = reward.asset.id;
const assetDecimals = reward.asset.decimals;
const rewardType = reward.rewardType;
const amount = reward.amount;
@@ -70,6 +76,14 @@ export const generateEpochIndividualRewardsList = ({
const epoch = acc.get(epochId);
// matchingTotalReward is the total awarded for all users for the reward type in the epoch of the asset
const matchingTotalRewardAmount = epochRewardSummaries.find(
(summary) =>
summary.epoch === Number(epochId) &&
summary.assetId === assetId &&
summary.rewardType === rewardType
)?.amount;
let asset = epoch?.rewards.find((r) => r.asset === assetName);
if (!asset) {
@@ -86,22 +100,24 @@ export const generateEpochIndividualRewardsList = ({
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
} else {
const previousAmount = asset.rewardTypes[rewardType]?.amount;
const previousPercentageOfTotal =
asset.rewardTypes[rewardType]?.percentageOfTotal;
const newAmount = previousAmount
? new BigNumber(previousAmount).plus(amount).toString()
: amount;
asset.rewardTypes[rewardType] = {
amount: previousAmount
? new BigNumber(previousAmount).plus(amount).toString()
: amount,
percentageOfTotal: previousPercentageOfTotal
? new BigNumber(previousPercentageOfTotal)
.plus(percentageOfTotal)
amount: newAmount,
percentageOfTotal: matchingTotalRewardAmount
? new BigNumber(newAmount)
.dividedBy(matchingTotalRewardAmount)
.multipliedBy(100)
.toString()
: percentageOfTotal,
: // this should never be reached, if there's an individual reward there should
// always be a reward total from the api too, but set it as a fallback just in case
percentageOfTotal,
};
}
// totalAmount is the sum of all rewardTypes amounts
// totalAmount is the sum of all individual rewardTypes amounts
asset.totalAmount = Object.values(asset.rewardTypes).reduce(
(sum, rewardType) => {
return new BigNumber(sum).plus(rewardType.amount).toString();
@@ -22,6 +22,13 @@ fragment DelegationFields on Delegation {
epoch
}
fragment EpochRewardSummaryFields on EpochRewardSummary {
epoch
assetId
amount
rewardType
}
query Rewards(
$partyId: ID!
$fromEpoch: Int
@@ -50,13 +57,16 @@ query Rewards(
}
}
}
}
fragment EpochRewardSummaryFields on EpochRewardSummary {
epoch
assetId
amount
rewardType
epochRewardSummaries(
filter: { fromEpoch: $fromEpoch, toEpoch: $toEpoch }
pagination: $rewardsPagination
) {
edges {
node {
...EpochRewardSummaryFields
}
}
}
}
query EpochAssetsRewards(
@@ -7,6 +7,8 @@ export type RewardFieldsFragment = { __typename?: 'Reward', rewardType: Types.Ac
export type DelegationFieldsFragment = { __typename?: 'Delegation', amount: string, epoch: number };
export type EpochRewardSummaryFieldsFragment = { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType };
export type RewardsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
fromEpoch?: Types.InputMaybe<Types.Scalars['Int']>;
@@ -16,9 +18,7 @@ export type RewardsQueryVariables = Types.Exact<{
}>;
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null };
export type EpochRewardSummaryFieldsFragment = { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType };
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null, epochRewardSummaries?: { __typename?: 'EpochRewardSummaryConnection', edges?: Array<{ __typename?: 'EpochRewardSummaryEdge', node: { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType } } | null> | null } | null };
export type EpochAssetsRewardsQueryVariables = Types.Exact<{
epochRewardSummariesFilter?: Types.InputMaybe<Types.RewardSummaryFilter>;
@@ -102,9 +102,20 @@ export const RewardsDocument = gql`
}
}
}
epochRewardSummaries(
filter: {fromEpoch: $fromEpoch, toEpoch: $toEpoch}
pagination: $rewardsPagination
) {
edges {
node {
...EpochRewardSummaryFields
}
}
}
}
${RewardFieldsFragmentDoc}
${DelegationFieldsFragmentDoc}`;
${DelegationFieldsFragmentDoc}
${EpochRewardSummaryFieldsFragmentDoc}`;
/**
* __useRewardsQuery__
@@ -36,6 +36,7 @@ import {
toBigNum,
} from '@vegaprotocol/utils';
import { VALIDATOR_LOGO_MAP } from './logo-map';
import { getMultisigStatusInfo } from '../../../../lib/get-multisig-status-info';
interface CanonisedConsensusNodeProps {
id: string;
@@ -137,6 +138,10 @@ export const ConsensusValidatorsTable = ({
[totalStake]
);
const multisigStatus = previousEpochData
? getMultisigStatusInfo(previousEpochData)
: undefined;
const allNodesInPreviousEpoch = removePaginationWrapper(
previousEpochData?.epoch.validatorsConnection?.edges
);
@@ -223,6 +228,8 @@ export const ConsensusValidatorsTable = ({
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
? formatNumberPercentage(new BigNumber(userStakeShare), 2)
: undefined,
[ValidatorFields.MULTISIG_ERROR]:
multisigStatus?.showMultisigStatusError,
};
}
);
@@ -332,6 +339,7 @@ export const ConsensusValidatorsTable = ({
data,
decimals,
hideTopThird,
multisigStatus?.showMultisigStatusError,
previousEpochData,
thirdOfTotalStake,
validatorsView,
@@ -39,6 +39,7 @@ export enum ValidatorFields {
STAKED_BY_USER = 'stakedByUser',
PENDING_USER_STAKE = 'pendingUserStake',
USER_STAKE_SHARE = 'userStakeShare',
MULTISIG_ERROR = 'multisigError',
}
export const addUserDataToValidator = (
@@ -326,6 +327,7 @@ interface TotalPenaltiesRendererProps {
overstakedAmount: string;
overstakingPenalty: string;
totalPenalties: string;
multisigError?: boolean;
};
}
@@ -344,10 +346,11 @@ export const TotalPenaltiesRenderer = ({
<div data-testid="overstaked-penalty-tooltip">
{t('overstakedPenalty')}: {data.overstakingPenalty}
</div>
<div data-testid="total-penalty-tooltip">
{t('totalPenalties')}:{' '}
<span className="font-bold">{data.totalPenalties}</span>
</div>
{data.multisigError && (
<div data-testid="multisig-error-tooltip">
{t('multisigPenalty')}: 100%
</div>
)}
</>
}
>
@@ -0,0 +1,15 @@
describe('charts', { tags: '@smoke' }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId('Depth').click();
});
it('can see market depth chart', () => {
// 6006-DEPC-001
cy.getByTestId('tab-depth').should('be.visible');
cy.get('.depth-chart-module_canvas__260De').should('be.visible');
});
});
@@ -1,10 +1,8 @@
const dialogContent = 'dialog-content';
const nodeHealth = 'node-health';
const statusIncidentsLink = 'footer [data-testid=external-link]';
describe('home', { tags: '@regression' }, () => {
before(() => {
cy.clearAllLocalStorage();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
@@ -76,23 +74,14 @@ describe('home', { tags: '@regression' }, () => {
cy.visit('/');
});
// 0006-NETW-002
// 0006-NETW-003
// 0006-NETW-011
it('switch to fairground network and check status & incidents link', () => {
// 0006-NETW-002
// 0006-NETW-003
cy.getByTestId('navigation')
.find('[data-testid="network-switcher"]')
.should('have.text', 'Custom')
.click();
cy.getByTestId('network-item').contains('Fairground testnet').click();
cy.get('[aria-haspopup="menu"]').should('contain.text', 'Fairground');
cy.url().should('include', 'fairground.wtf');
cy.contains('Continue').click();
cy.get(statusIncidentsLink)
.children('span')
.should('have.text', 'Mainnet status & incidents');
cy.get(statusIncidentsLink)
.should('have.attr', 'href')
.and('contain', 'https://blog.vega.xyz/tagged/vega-incident-reports');
cy.getByTestId('network-item').contains('Fairground testnet');
});
});
});
@@ -182,7 +182,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
.contains('Liquidity monitoring parameters')
.click();
validateMarketDataRow(0, 'Triggering Ratio', '0');
validateMarketDataRow(0, 'Triggering Ratio', '0.7');
validateMarketDataRow(1, 'Time Window', '3,600');
validateMarketDataRow(2, 'Scaling Factor', '10');
});
@@ -81,6 +81,7 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
cy.get(rowSelector).first().find(colFee).should('have.text', '0.09%');
// 5002-LIQP-013
cy.get(rowSelector)
.first()
.find(colAverageEntryValuation)
@@ -165,7 +166,7 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
});
it('can see liquidity supplied', () => {
//// 5002-LIQP-008
// 5002-LIQP-008
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('liquidity-supplied').within(() => {
cy.getByTestId(itemHeader).should('have.text', 'Liquidity supplied');
@@ -237,6 +238,7 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
.find(colFee)
.should('have.text', '0.09%');
// 5002-LIQP-013
cy.get(rowSelectorLiquidityActive)
.first()
.find(colAverageEntryValuation)
@@ -268,7 +270,7 @@ describe('liquidity table view', { tags: '@smoke' }, () => {
});
it('renders liquidity inactive table correctly', () => {
//// 5002-LIQP-012
// 5002-LIQP-012
cy.getByTestId('Inactive').click();
cy.get(rowSelectorLiquidityInactive)
.first()
@@ -109,7 +109,7 @@ describe('accounts', { tags: '@smoke' }, () => {
it('should open usage breakdown dialog when clicked on used', () => {
// 7001-COLL-009
cy.get('[col-id="used"]').contains('1.01').click();
const headers = ['Market', 'Account type', 'Balance'];
const headers = ['Market', 'Account type', 'Balance', 'Margin health'];
cy.getByTestId('usage-breakdown').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
@@ -128,6 +128,7 @@ describe('accounts', { tags: '@smoke' }, () => {
cy.wrap(btn).click();
});
}
cy.contains('Loading...').should('not.exist');
});
// 7001-COLL-010
it('sorting by asset', () => {
@@ -146,12 +147,17 @@ describe('accounts', { tags: '@smoke' }, () => {
it('sorting by total', () => {
cy.getByTestId('Collateral').click();
const marketsSortedDefault = [
'1,000.00',
'1,000.00002',
'1,000.01',
'1,000.00',
'1,000.00001',
];
const marketsSortedAsc = [
'1,000.00',
'1,000.00001',
'1,000.00002',
'1,000.01',
];
const marketsSortedAsc = ['1,000.00', '1,000.00', '1,000.00', '1,000.01'];
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
checkSorting(
@@ -188,19 +194,24 @@ describe('accounts', { tags: '@smoke' }, () => {
);
});
it('sorting by total', () => {
it('sorting by available', () => {
cy.getByTestId('Collateral').click();
const marketsSortedDefault = [
'1,000.00',
'1,000.01',
'1,000.00002',
'1,000.00',
'1,000.00',
'1,000.00001',
];
const marketsSortedAsc = [
'1,000.00',
'1,000.00',
'1,000.00001',
'1,000.00002',
];
const marketsSortedAsc = ['1,000.00', '1,000.00', '1,000.00', '1,000.01'];
const marketsSortedDesc = Array.from(marketsSortedAsc).reverse();
checkSorting(
'total',
'available',
marketsSortedDefault,
marketsSortedAsc,
marketsSortedDesc
@@ -81,7 +81,7 @@ describe(
cy.visit('/#/markets/market-0');
});
it('must display that market is not accepting orders', function () {
cy.getByTestId('dealticket-error-message-summary').should(
cy.getByTestId('deal-ticket-error-message-summary').should(
'have.text',
`This market is ${marketState
.split('_')
@@ -45,7 +45,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('dealticket-error-message-expiry').should(
cy.getByTestId('deal-ticket-error-message-expiry').should(
'have.text',
'The expiry date that you have entered appears to be in the past'
);
@@ -57,7 +57,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
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(
cy.getByTestId('deal-ticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
);
@@ -79,7 +79,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(orderSizeField).clear().type('1.234');
// 7002-SORD-060
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('dealticket-error-message-size-market').should(
cy.getByTestId('deal-ticket-error-message-size-market').should(
'have.text',
'Size must be whole numbers for this market'
);
@@ -88,7 +88,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must warn if order size is set to 0', function () {
cy.getByTestId(orderSizeField).clear().type('0');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('dealticket-error-message-size-market').should(
cy.getByTestId('deal-ticket-error-message-size-market').should(
'have.text',
'Size cannot be lower than 1'
);
@@ -96,15 +96,29 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must have total margin available', () => {
// 7001-COLL-011
cy.getByTestId('tab-ticket')
.find('.text-xs')
.eq(5)
.within(() => {
cy.get('[data-state="closed"]').should(
'have.text',
'Total margin available' + '100.01 tDAI'
);
});
cy.getByTestId('deal-ticket-fee-total-margin-available').within(() => {
cy.get('[data-state="closed"]').should(
'have.text',
'Total margin available100.01 tDAI'
);
});
});
it('must have current margin allocation', () => {
cy.getByTestId('deal-ticket-fee-current-margin-allocation').within(() => {
cy.get('[data-state="closed"]:first').should(
'have.text',
'Current margin allocation'
);
});
});
it('should open usage breakdown dialog when clicked on current margin allocation', () => {
cy.getByTestId('deal-ticket-fee-current-margin-allocation').within(() => {
cy.get('button').click();
});
cy.getByTestId('usage-breakdown').should('exist');
cy.getByTestId('dialog-close').click();
});
});
});
@@ -1,6 +1,10 @@
import * as Schema from '@vegaprotocol/types';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { accountsQuery, amendGeneralAccountBalance } from '@vegaprotocol/mock';
import {
accountsQuery,
amendGeneralAccountBalance,
amendMarginAccountBalance,
} from '@vegaprotocol/mock';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
@@ -12,8 +16,9 @@ describe(
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
const accounts = accountsQuery();
amendGeneralAccountBalance(accounts, 'market-0', '0');
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '0');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
@@ -23,10 +28,15 @@ describe(
});
it('should show an error if your balance is zero', () => {
const accounts = accountsQuery();
amendMarginAccountBalance(accounts, 'market-0', '0');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
// 7002-SORD-060
cy.getByTestId('place-order').should('be.enabled');
// 7002-SORD-003
cy.getByTestId('dealticket-error-message-zero-balance').should(
cy.getByTestId('deal-ticket-error-message-zero-balance').should(
'have.text',
'You need ' +
'tDAI' +
@@ -40,8 +50,9 @@ describe(
beforeEach(() => {
cy.setVegaWallet();
cy.mockTradingPage();
const accounts = accountsQuery();
amendGeneralAccountBalance(accounts, 'market-0', '1');
let accounts = accountsQuery();
accounts = amendMarginAccountBalance(accounts, 'market-0', '1000');
accounts = amendGeneralAccountBalance(accounts, 'market-0', '1');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
@@ -54,11 +65,11 @@ describe(
// 7002-SORD-003
// warning should show immediately
cy.getByTestId('dealticket-warning-margin').should(
cy.getByTestId('deal-ticket-warning-margin').should(
'contain.text',
'You may not have enough margin available to open this position'
);
cy.getByTestId('dealticket-warning-margin').should(
cy.getByTestId('deal-ticket-warning-margin').should(
'contain.text',
'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.'
);
@@ -37,7 +37,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
// 7002-SORD-060
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('dealticket-error-message-type').should(
cy.getByTestId('deal-ticket-error-message-type').should(
'have.text',
'This market is in auction until it reaches sufficient liquidity. Only limit orders are permitted when market is in auction'
);
@@ -48,7 +48,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('dealticket-warning-auction').should(
cy.getByTestId('deal-ticket-warning-auction').should(
'have.text',
'Any orders placed now will not trade until the auction ends'
);
@@ -60,7 +60,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
TIFlist.filter((item) => item.code === 'FOK')[0].value
);
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('dealticket-error-message-tif').should(
cy.getByTestId('deal-ticket-error-message-tif').should(
'have.text',
'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
);
@@ -5,6 +5,10 @@ import {
toggleMarket,
} from '../support/deal-ticket';
const tooltipContent = 'tooltip-content';
const reduceOnly = 'reduce-only';
const postOnly = 'post-only';
describe('time in force validation', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
@@ -122,13 +126,18 @@ describe('time in force validation', { tags: '@smoke' }, () => {
// 7002-SORD-026
it(`post and reduce order market for ${tif.code}`, function () {
// 7003-SORD-054
// 7003-SORD-055
// 7003-SORD-056
// 7003-SORD-057
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');
cy.getByTestId(postOnly).should('be.disabled');
cy.getByTestId(reduceOnly).should('be.enabled');
});
});
});
@@ -144,14 +153,33 @@ describe('time in force validation', { tags: '@smoke' }, () => {
validTIFLimit.forEach((tif) => {
it(`post and reduce order for limit ${tif.code}`, function () {
// 7003-SORD-054
// 7003-SORD-055
// 7003-SORD-056
// 7003-SORD-057
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');
cy.getByTestId(postOnly).should('be.enabled');
cy.getByTestId(reduceOnly).should('be.disabled');
});
});
it(`can see explanation of what post only and reduce only is/does`, function () {
// 7003-SORD-058
cy.get('[for="post-only"]').should('have.text', 'Post only').realHover();
cy.getByTestId(tooltipContent).should(
'contain.text',
`"Post only" will ensure the order is not filled immediately but is placed on the order book as a passive order. When the order is processed it is either stopped (if it would not be filled immediately), or placed in the order book as a passive order until the price taker matches with it.`
);
cy.get('[for="reduce-only"]')
.should('have.text', 'Reduce only')
.realHover();
cy.getByTestId(tooltipContent).should(
'contain.text',
`"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".`
);
});
});
});
@@ -1,32 +1,79 @@
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
});
const colHeader = '.ag-header-cell-text';
const colIdPrice = '[col-id=price]';
const colIdSize = '[col-id=size]';
const colIdCreatedAt = '[col-id=createdAt]';
const tradesTab = 'Trades';
const tradesTable = 'tab-trades';
describe('trades', { tags: '@smoke' }, () => {
const colIdPrice = 'price';
const colIdSize = 'size';
const colIdCreatedAt = 'createdAt';
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
});
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.getByTestId(tradesTab).click();
});
it('renders trades', () => {
cy.getByTestId('Trades').click();
cy.getByTestId('tab-trades').should('be.visible');
it('show trades', () => {
// 6005-THIS-001
// 6005-THIS-002
cy.getByTestId(tradesTab).should('be.visible');
cy.getByTestId(tradesTable).should('be.visible');
cy.getByTestId(tradesTable).should('not.be.empty');
});
cy.get(`[col-id=${colIdPrice}]`).each(($tradePrice) => {
it('show trades prices', () => {
// 6005-THIS-003
cy.get(`${colIdPrice} ${colHeader}`).first().should('have.text', 'Price');
cy.get(colIdPrice).each(($tradePrice) => {
cy.wrap($tradePrice).invoke('text').should('not.be.empty');
});
cy.get(`[col-id=${colIdSize}]`).each(($tradeSize) => {
});
it('show trades sizes', () => {
// 6005-THIS-004
cy.get(`${colIdSize} ${colHeader}`).first().should('have.text', 'Size');
cy.get(colIdSize).each(($tradeSize) => {
cy.wrap($tradeSize).invoke('text').should('not.be.empty');
});
});
it('show trades date and time', () => {
// 6005-THIS-005
cy.get(`${colIdCreatedAt} ${colHeader}`).should('have.text', 'Created at');
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
cy.get(`[col-id=${colIdCreatedAt}]`).each(($tradeDateTime, index) => {
cy.get(colIdCreatedAt).each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
cy.wrap($tradeDateTime).invoke('text').should('match', dateTimeRegex);
}
});
});
it('trades are sorted descending by datetime', () => {
// 6005-THIS-006
const dateTimes: Date[] = [];
cy.get(colIdCreatedAt)
.each(($tradeDateTime, index) => {
if (index != 0) {
//ignore header
dateTimes.push(new Date($tradeDateTime.text()));
}
})
.then(() => {
expect(dateTimes).to.deep.equal(
dateTimes.sort((a, b) => b.getTime() - a.getTime())
);
});
});
it('copy price to deal ticket form', () => {
// 6005-THIS-007
cy.get(colIdPrice).last().click();
cy.getByTestId('order-price').should('have.value', '171.16898');
});
});
@@ -83,6 +83,8 @@ export const LiquidityContainer = ({
const assetDecimalPlaces =
market?.tradableInstrument.instrument.product.settlementAsset.decimals || 0;
const quantum =
market?.tradableInstrument.instrument.product.settlementAsset.quantum || 0;
const symbol =
market?.tradableInstrument.instrument.product.settlementAsset.symbol;
@@ -98,6 +100,7 @@ export const LiquidityContainer = ({
rowData={data}
symbol={symbol}
assetDecimalPlaces={assetDecimalPlaces}
quantum={quantum}
stakeToCcyVolume={stakeToCcyVolume}
overlayNoRowsTemplate={error ? error.message : t('No data')}
/>
@@ -8,7 +8,7 @@ import {
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { CSSProperties } from 'react';
import { useState } from 'react';
import { useCallback, useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
@@ -184,7 +184,6 @@ const MarketList = ({
if (error) {
return <div>{error.message}</div>;
}
return (
<AutoSizer>
{({ width, height }) => (
@@ -204,6 +203,29 @@ const MarketList = ({
);
};
interface ListItemData {
data: MarketMaybeWithDataAndCandles[];
onSelect?: (marketId: string) => void;
currentMarketId?: string;
}
const ListItem = ({
index,
style,
data,
}: {
index: number;
style: CSSProperties;
data: ListItemData;
}) => (
<MarketSelectorItem
market={data.data[index]}
currentMarketId={data.currentMarketId}
style={style}
onSelect={data.onSelect}
/>
);
const List = ({
data,
loading,
@@ -212,28 +234,20 @@ const List = ({
onSelect,
noItems,
currentMarketId,
}: {
data: MarketMaybeWithDataAndCandles[];
}: ListItemData & {
loading: boolean;
width: number;
height: number;
noItems: string;
onSelect?: (marketId: string) => void;
currentMarketId?: string;
}) => {
const row = ({ index, style }: { index: number; style: CSSProperties }) => {
const market = data[index];
return (
<MarketSelectorItem
market={market}
currentMarketId={currentMarketId}
style={style}
onSelect={onSelect}
/>
);
};
const itemKey = useCallback(
(index: number, data: ListItemData) => data.data[index].id,
[]
);
const itemData = useMemo(
() => ({ data, onSelect, currentMarketId }),
[data, onSelect, currentMarketId]
);
if (!data || loading) {
return (
<div style={{ width, height }}>
@@ -259,11 +273,13 @@ const List = ({
<FixedSizeList
className="virtualized-list"
itemCount={data.length}
itemData={itemData}
itemSize={130}
itemKey={itemKey}
width={width}
height={height}
>
{row}
{ListItem}
</FixedSizeList>
);
};
@@ -49,7 +49,7 @@ const MarketBottomPanel = memo(
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'bottom' });
const { screenSize } = useScreenDimensions();
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
return 'xxxl' === screenSize ? (
<ResizableGrid
@@ -143,6 +143,7 @@ const MarketBottomPanel = memo(
<VegaWalletContainer>
<TradingViews.collateral.component
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
hideButtons
storeKey="marketCollateral"
/>
@@ -223,6 +224,7 @@ const MarketBottomPanel = memo(
<VegaWalletContainer>
<TradingViews.collateral.component
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
hideButtons
storeKey="marketCollateral"
/>
@@ -248,6 +250,7 @@ const MainGrid = memo(
const [sizesMiddle, handleOnMiddleLayoutChange] = usePaneLayout({
id: 'middle-1',
});
const onMarketClick = useMarketClickHandler(true);
return (
<ResizableGrid vertical onChange={handleOnLayoutChange}>
@@ -266,6 +269,7 @@ const MainGrid = memo(
<Tab id="ticket" name={t('Ticket')}>
<TradingViews.ticket.component
marketId={marketId}
onMarketClick={onMarketClick}
onClickCollateral={() => navigate('/portfolio')}
/>
</Tab>
@@ -39,7 +39,7 @@ export const TradePanels = ({
}: TradePanelsProps) => {
const [drawerOpen, setDrawerOpen] = useState(false);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
const [view, setView] = useState<TradingView>('candles');
const renderView = () => {
@@ -22,7 +22,7 @@ export const DepositsContainer = () => {
<div className="h-full">
<div className="h-full relative">
<DepositsTable
rowData={data || []}
rowData={data}
ref={gridRef}
{...bottomPlaceholderProps}
overlayNoRowsTemplate={error ? error.message : t('No deposits')}
@@ -34,7 +34,7 @@ export const Portfolio = () => {
}, [updateTitle]);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler();
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
const wrapperClasses = 'h-full max-h-full flex flex-col';
return (
@@ -92,7 +92,10 @@ export const Portfolio = () => {
<Tabs storageKey="console-portfolio-bottom">
<Tab id="collateral" name={t('Collateral')}>
<VegaWalletContainer>
<AccountsContainer storeKey="portfolioCollateral" />
<AccountsContainer
storeKey="portfolioCollateral"
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="deposits" name={t('Deposits')}>
@@ -13,10 +13,12 @@ export const AccountsContainer = ({
pinnedAsset,
hideButtons,
storeKey,
onMarketClick,
}: {
pinnedAsset?: PinnedAsset;
hideButtons?: boolean;
storeKey?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}) => {
const { pubKey, isReadOnly } = useVegaWallet();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
@@ -46,6 +48,7 @@ export const AccountsContainer = ({
onClickAsset={onClickAsset}
onClickWithdraw={openWithdrawalDialog}
onClickDeposit={openDepositDialog}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
pinnedAsset={pinnedAsset}
storeKey={storeKey}
@@ -49,6 +49,9 @@ export const AppLoader = ({ children }: { children: ReactNode }) => {
const cacheConfig: InMemoryCacheConfig = {
typePolicies: {
Statistics: {
merge: true,
},
Account: {
keyFields: false,
fields: {
@@ -80,12 +83,6 @@ const cacheConfig: InMemoryCacheConfig = {
ERC20: {
keyFields: ['contractAddress'],
},
PositionUpdate: {
keyFields: false,
},
AccountUpdate: {
keyFields: false,
},
Party: {
keyFields: false,
},
@@ -95,8 +92,16 @@ const cacheConfig: InMemoryCacheConfig = {
Fees: {
keyFields: false,
},
// Don't cache order update as this subscription result gets merged into the main order cache
// We don't need to write these to the cache at all
// The folling types are cached by the data provider and not by apollo
PositionUpdate: {
keyFields: false,
},
TradeUpdate: {
keyFields: false,
},
AccountUpdate: {
keyFields: false,
},
OrderUpdate: {
keyFields: false,
},
@@ -125,14 +125,19 @@ export const MarketLiquiditySupplied = ({
</KeyValueTableRow>
</KeyValueTable>
<br />
<Link href={`/#/liquidity/${marketId}`} data-testid="view-liquidity-link">
{t('View liquidity provision table')}
</Link>
{DocsLinks && (
<ExternalLink href={DocsLinks.LIQUIDITY} className="mt-2">
{t('Learn about providing liquidity')}
</ExternalLink>
)}
<div className="flex flex-col gap-2">
<Link
href={`/#/liquidity/${marketId}`}
data-testid="view-liquidity-link"
>
{t('View liquidity provision table')}
</Link>
{DocsLinks && (
<ExternalLink href={DocsLinks.LIQUIDITY} className="mt-2">
{t('Learn about providing liquidity')}
</ExternalLink>
)}
</div>
{showMessage && (
<p className="mt-4">
{t(
@@ -20,20 +20,8 @@ export const useMarketClickHandler = (replace = false) => {
);
};
export const useMarketLiquidityClickHandler = (replace = false) => {
const navigate = useNavigate();
const { marketId } = useParams();
const { pathname } = useLocation();
const isLiquidityPage = pathname.match(/^\/liquidity\/(.+)/);
return useCallback(
(selectedId: string, metaKey?: boolean) => {
const link = Links[Routes.LIQUIDITY](selectedId);
if (metaKey) {
window.open(`/#${link}`, '_blank');
} else if (selectedId !== marketId || !isLiquidityPage) {
navigate(link, { replace });
}
},
[navigate, marketId, replace, isLiquidityPage]
);
export const useMarketLiquidityClickHandler = () => {
return useCallback((selectedId: string, metaKey?: boolean) => {
window.open(`/#/liquidity/${selectedId}`, metaKey ? '_blank' : '_self');
}, []);
};
+38
View File
@@ -0,0 +1,38 @@
fragment MarginFields on MarginLevels {
maintenanceLevel
searchLevel
initialLevel
collateralReleaseLevel
asset {
id
}
market {
id
}
}
query Margins($partyId: ID!) {
party(id: $partyId) {
id
marginsConnection {
edges {
node {
...MarginFields
}
}
}
}
}
subscription MarginsSubscription($partyId: ID!) {
margins(partyId: $partyId) {
marketId
asset
partyId
maintenanceLevel
searchLevel
initialLevel
collateralReleaseLevel
timestamp
}
}
+114
View File
@@ -0,0 +1,114 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
export type MarginsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type MarginsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, marginsConnection?: { __typename?: 'MarginConnection', edges?: Array<{ __typename?: 'MarginEdge', node: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
export type MarginsSubscriptionSubscriptionVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type MarginsSubscriptionSubscription = { __typename?: 'Subscription', margins: { __typename?: 'MarginLevelsUpdate', marketId: string, asset: string, partyId: string, maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, timestamp: any } };
export const MarginFieldsFragmentDoc = gql`
fragment MarginFields on MarginLevels {
maintenanceLevel
searchLevel
initialLevel
collateralReleaseLevel
asset {
id
}
market {
id
}
}
`;
export const MarginsDocument = gql`
query Margins($partyId: ID!) {
party(id: $partyId) {
id
marginsConnection {
edges {
node {
...MarginFields
}
}
}
}
}
${MarginFieldsFragmentDoc}`;
/**
* __useMarginsQuery__
*
* To run a query within a React component, call `useMarginsQuery` and pass it any options that fit your needs.
* When your component renders, `useMarginsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useMarginsQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function useMarginsQuery(baseOptions: Apollo.QueryHookOptions<MarginsQuery, MarginsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<MarginsQuery, MarginsQueryVariables>(MarginsDocument, options);
}
export function useMarginsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MarginsQuery, MarginsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<MarginsQuery, MarginsQueryVariables>(MarginsDocument, options);
}
export type MarginsQueryHookResult = ReturnType<typeof useMarginsQuery>;
export type MarginsLazyQueryHookResult = ReturnType<typeof useMarginsLazyQuery>;
export type MarginsQueryResult = Apollo.QueryResult<MarginsQuery, MarginsQueryVariables>;
export const MarginsSubscriptionDocument = gql`
subscription MarginsSubscription($partyId: ID!) {
margins(partyId: $partyId) {
marketId
asset
partyId
maintenanceLevel
searchLevel
initialLevel
collateralReleaseLevel
timestamp
}
}
`;
/**
* __useMarginsSubscriptionSubscription__
*
* To run a query within a React component, call `useMarginsSubscriptionSubscription` and pass it any options that fit your needs.
* When your component renders, `useMarginsSubscriptionSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useMarginsSubscriptionSubscription({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function useMarginsSubscriptionSubscription(baseOptions: Apollo.SubscriptionHookOptions<MarginsSubscriptionSubscription, MarginsSubscriptionSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<MarginsSubscriptionSubscription, MarginsSubscriptionSubscriptionVariables>(MarginsSubscriptionDocument, options);
}
export type MarginsSubscriptionSubscriptionHookResult = ReturnType<typeof useMarginsSubscriptionSubscription>;
export type MarginsSubscriptionSubscriptionResult = Apollo.SubscriptionResult<MarginsSubscriptionSubscription>;
@@ -94,6 +94,7 @@ export const accountsOnlyDataProvider = makeDataProvider<
update,
getData,
getDelta,
fetchPolicy: 'no-cache',
});
export interface AccountFields extends Account {
+70 -15
View File
@@ -1,4 +1,4 @@
import { useRef, memo, useState } from 'react';
import { useRef, memo, useState, useCallback } from 'react';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider';
@@ -15,14 +15,25 @@ import BreakdownTable from './breakdown-table';
const AccountBreakdown = ({
assetId,
partyId,
onMarketClick,
}: {
assetId: string;
partyId: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}) => {
const gridRef = useRef<AgGridReact>(null);
const { data } = useDataProvider({
dataProvider: aggregatedAccountDataProvider,
variables: { partyId, assetId },
update: ({ data }) => {
if (gridRef.current?.api && data?.breakdown) {
gridRef.current?.api.setRowData(data?.breakdown);
return true;
}
return false;
},
});
return (
<div
className="h-[35vh] w-full m-auto flex flex-col"
@@ -39,16 +50,56 @@ const AccountBreakdown = ({
])}
</p>
)}
<BreakdownTable data={data?.breakdown || null} domLayout="autoHeight" />
<BreakdownTable
ref={gridRef}
data={data?.breakdown || null}
domLayout="autoHeight"
onMarketClick={onMarketClick}
/>
</div>
);
};
export const AccountBreakdownDialog = memo(
({
assetId,
partyId,
onClose,
onMarketClick,
}: {
assetId?: string;
partyId: string;
onClose: () => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}) => {
return (
<Dialog
size="medium"
open={Boolean(assetId)}
onChange={(isOpen) => {
if (!isOpen) {
onClose();
}
}}
>
{assetId && (
<AccountBreakdown
assetId={assetId}
partyId={partyId}
onMarketClick={onMarketClick}
/>
)}
</Dialog>
);
}
);
interface AccountManagerProps {
partyId: string;
onClickAsset: (assetId: string) => void;
onClickWithdraw?: (assetId?: string) => void;
onClickDeposit?: (assetId?: string) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
isReadOnly: boolean;
pinnedAsset?: PinnedAsset;
storeKey?: string;
@@ -62,6 +113,7 @@ export const AccountManager = ({
isReadOnly,
pinnedAsset,
storeKey,
onMarketClick,
}: AccountManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [breakdownAssetId, setBreakdownAssetId] = useState<string>();
@@ -71,6 +123,16 @@ export const AccountManager = ({
variables: { partyId },
});
const onMarketClickInternal = useCallback(
(...args: Parameters<NonNullable<typeof onMarketClick>>) => {
setBreakdownAssetId(undefined);
if (onMarketClick) {
onMarketClick(...args);
}
},
[onMarketClick]
);
return (
<div className="relative h-full">
<AccountTable
@@ -85,19 +147,12 @@ export const AccountManager = ({
storeKey={storeKey}
overlayNoRowsTemplate={error ? error.message : t('No accounts')}
/>
<Dialog
size="medium"
open={Boolean(breakdownAssetId)}
onChange={(isOpen) => {
if (!isOpen) {
setBreakdownAssetId(undefined);
}
}}
>
{breakdownAssetId && (
<AccountBreakdown assetId={breakdownAssetId} partyId={partyId} />
)}
</Dialog>
<AccountBreakdownDialog
assetId={breakdownAssetId}
partyId={partyId}
onClose={useCallback(() => setBreakdownAssetId(undefined), [])}
onMarketClick={onMarketClick ? onMarketClickInternal : undefined}
/>
</div>
);
};
+48 -14
View File
@@ -151,19 +151,53 @@ export const amendGeneralAccountBalance = (
marketId: string,
balance: string
) => {
if (accounts.party?.accountsConnection?.edges) {
const marginAccount = accounts.party.accountsConnection.edges.find(
(edge) => edge?.node.market?.id === marketId
);
if (marginAccount) {
const generalAccount = accounts.party.accountsConnection.edges.find(
(edge) =>
edge?.node.asset.id === marginAccount.node.asset.id &&
!edge?.node.market
);
if (generalAccount) {
generalAccount.node.balance = balance;
}
}
if (!accounts.party?.accountsConnection?.edges) {
return accounts;
}
const marginAccount = accounts.party?.accountsConnection?.edges?.find(
(edge) => edge?.node.market?.id === marketId
);
if (marginAccount) {
const edges = accounts.party.accountsConnection.edges.map((edge) =>
edge?.node.asset.id === marginAccount.node.asset.id && !edge?.node.market
? { ...edge, node: { ...edge.node, balance } }
: edge
);
return {
...accounts,
party: {
...accounts.party,
accountsConnection: {
...accounts.party.accountsConnection,
edges,
},
},
};
}
return accounts;
};
export const amendMarginAccountBalance = (
accounts: AccountsQuery,
marketId: string,
balance: string
) => {
if (!accounts.party?.accountsConnection?.edges) {
return accounts;
}
const edges = accounts.party?.accountsConnection?.edges?.map((edge) =>
edge?.node.market?.id === marketId
? { ...edge, node: { ...edge?.node, balance } }
: edge
);
return {
...accounts,
party: {
...accounts.party,
accountsConnection: {
...accounts.party.accountsConnection,
edges,
},
},
};
};
+29 -3
View File
@@ -4,6 +4,14 @@ import * as Types from '@vegaprotocol/types';
import type { AccountFields } from './accounts-data-provider';
import { getAccountData } from './accounts-data-provider';
const marginHealthChartTestId = 'margin-health-chart';
jest.mock('./margin-health-chart', () => ({
MarginHealthChart: () => {
return <div data-testid={marginHealthChartTestId}></div>;
},
}));
const singleRow = {
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
@@ -37,10 +45,10 @@ describe('BreakdownTable', () => {
render(<BreakdownTable data={singleRowData} />);
});
const headers = await screen.findAllByRole('columnheader');
expect(headers).toHaveLength(3);
expect(headers).toHaveLength(4);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual(['Market', 'Account type', 'Balance']);
).toEqual(['Market', 'Account type', 'Balance', 'Margin health']);
});
it('should apply correct formatting', async () => {
@@ -55,9 +63,27 @@ describe('BreakdownTable', () => {
'1,256.00',
'1,256.00',
];
cells.forEach((cell, i) => {
cells.slice(0, -1).forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
expect(screen.getByTestId(marginHealthChartTestId)).toBeInTheDocument();
});
it('displays margin health chart only for margin account', async () => {
await act(async () => {
render(
<BreakdownTable
data={[
{
...singleRow,
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
market: null,
},
]}
/>
);
});
expect(screen.queryByTestId(marginHealthChartTestId)).toBeNull();
});
it('should get correct account data', () => {
+24 -2
View File
@@ -9,16 +9,20 @@ import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
import type { AccountFields } from './accounts-data-provider';
import { AccountTypeMapping } from '@vegaprotocol/types';
import type {
VegaICellRendererParams,
VegaValueFormatterParams,
VegaICellRendererParams,
} from '@vegaprotocol/datagrid';
import { ProgressBarCell } from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid, PriceCell } from '@vegaprotocol/datagrid';
import type { ColDef } from 'ag-grid-community';
import { accountValuesComparator } from './accounts-table';
import { MarginHealthChart } from './margin-health-chart';
import { MarketNameCell } from '@vegaprotocol/datagrid';
import { AccountType } from '@vegaprotocol/types';
interface BreakdownTableProps extends AgGridReactProps {
data: AccountFields[] | null;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}
const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
@@ -90,6 +94,24 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
},
comparator: accountValuesComparator,
},
{
headerName: t('Margin health'),
field: 'market.id',
flex: 2,
maxWidth: 500,
sortable: false,
cellRenderer: ({
data,
}: VegaICellRendererParams<AccountFields, 'market.id'>) =>
data?.market?.id &&
data.type === AccountType['ACCOUNT_TYPE_MARGIN'] &&
data?.asset.id ? (
<MarginHealthChart
marketId={data.market.id}
assetId={data.asset.id}
/>
) : null,
},
];
return defs;
}, []);
@@ -104,7 +126,7 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
}
ref={ref}
rowHeight={34}
components={{ PriceCell }}
components={{ PriceCell, MarketNameCell, ProgressBarCell }}
tooltipShowDelay={500}
defaultColDef={{
flex: 1,
+3
View File
@@ -8,3 +8,6 @@ export * from './use-account-balance';
export * from './get-settlement-account';
export * from './use-market-account-balance';
export * from './transfer-dialog';
export * from './__generated__/Margins';
export { MarginHealthChart } from './margin-health-chart';
export * from './margin-data-provider';
@@ -7,13 +7,13 @@ import {
import {
MarginsSubscriptionDocument,
MarginsDocument,
} from './__generated__/Positions';
} from './__generated__/Margins';
import type {
MarginsQuery,
MarginFieldsFragment,
MarginsSubscriptionSubscription,
MarginsQueryVariables,
} from './__generated__/Positions';
} from './__generated__/Margins';
const update = (
data: MarginFieldsFragment[] | null,
@@ -0,0 +1,242 @@
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { Tooltip, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketMarginDataProvider } from './margin-data-provider';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { useAccountBalance } from './use-account-balance';
import { useMarketAccountBalance } from './use-market-account-balance';
const MarginHealthChartTooltipRow = ({
label,
value,
decimals,
href,
}: {
label: string;
value: string;
decimals: number;
href?: string;
}) => (
<>
<div
className="float-left clear-left"
key="label"
data-testid="margin-health-tooltip-label"
>
{href ? (
<ExternalLink href={href} target="_blank">
{label}
</ExternalLink>
) : (
label
)}
</div>
<div
className="float-right"
key="value"
data-testid="margin-health-tooltip-value"
>
{addDecimalsFormatNumber(value, decimals)}
</div>
</>
);
export const MarginHealthChartTooltip = ({
maintenanceLevel,
searchLevel,
initialLevel,
collateralReleaseLevel,
decimals,
marginAccountBalance,
}: {
maintenanceLevel: string;
searchLevel: string;
initialLevel: string;
collateralReleaseLevel: string;
decimals: number;
marginAccountBalance?: string;
}) => {
const tooltipContent = [
<MarginHealthChartTooltipRow
key={'maintenance'}
label={t('maintenance level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-maintenance"
value={maintenanceLevel}
decimals={decimals}
/>,
<MarginHealthChartTooltipRow
key={'search'}
label={t('search level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-searching-for-collateral"
value={searchLevel}
decimals={decimals}
/>,
<MarginHealthChartTooltipRow
key={'initial'}
label={t('initial level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-initial"
value={initialLevel}
decimals={decimals}
/>,
<MarginHealthChartTooltipRow
key={'release'}
label={t('release level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-releasing-collateral"
value={collateralReleaseLevel}
decimals={decimals}
/>,
];
if (marginAccountBalance) {
const balance = (
<MarginHealthChartTooltipRow
key={'balance'}
label={t('balance')}
value={marginAccountBalance}
decimals={decimals}
/>
);
if (BigInt(marginAccountBalance) < BigInt(searchLevel)) {
tooltipContent.splice(1, 0, balance);
} else if (BigInt(marginAccountBalance) < BigInt(initialLevel)) {
tooltipContent.splice(2, 0, balance);
} else if (BigInt(marginAccountBalance) < BigInt(collateralReleaseLevel)) {
tooltipContent.splice(3, 0, balance);
} else {
tooltipContent.push(balance);
}
}
return (
<div className="overflow-hidden" data-testid="margin-health-tooltip">
{tooltipContent}
</div>
);
};
export const MarginHealthChart = ({
marketId,
assetId,
}: {
marketId: string;
assetId: string;
}) => {
const { data: assetsMap } = useAssetsMapProvider();
const { pubKey: partyId } = useVegaWallet();
const { data } = useDataProvider({
dataProvider: marketMarginDataProvider,
variables: { marketId, partyId: partyId ?? '' },
skip: !partyId,
});
const { accountBalance: rawGeneralAccountBalance } =
useAccountBalance(assetId);
const { accountBalance: rawMarginAccountBalance } =
useMarketAccountBalance(marketId);
const asset = assetsMap && assetsMap[assetId];
if (!data || !asset) {
return null;
}
const { decimals } = asset;
const collateralReleaseLevel = Number(data.collateralReleaseLevel);
const initialLevel = Number(data.initialLevel);
const maintenanceLevel = Number(data.maintenanceLevel);
const searchLevel = Number(data.searchLevel);
const marginAccountBalance = Number(rawMarginAccountBalance);
const generalAccountBalance = Number(rawGeneralAccountBalance);
const max = Math.max(
marginAccountBalance + generalAccountBalance,
collateralReleaseLevel
);
const red = maintenanceLevel / max;
const orange = (searchLevel - maintenanceLevel) / max;
const yellow = ((searchLevel + initialLevel) / 2 - searchLevel) / max;
const green = (collateralReleaseLevel - initialLevel) / max + yellow;
const balanceMarker = marginAccountBalance / max;
const tooltip = (
<MarginHealthChartTooltip
maintenanceLevel={data.maintenanceLevel}
searchLevel={data.searchLevel}
initialLevel={data.initialLevel}
collateralReleaseLevel={data.collateralReleaseLevel}
marginAccountBalance={rawMarginAccountBalance}
decimals={decimals}
/>
);
return (
<div data-testid="margin-health-chart">
{addDecimalsFormatNumber(
(BigInt(marginAccountBalance) - BigInt(maintenanceLevel)).toString(),
decimals
)}{' '}
{t('above')}{' '}
<ExternalLink href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-maintenance">
{t('maintenance level')}
</ExternalLink>
<Tooltip description={tooltip}>
<div
data-testid="margin-health-chart-track"
className="relative bg-vega-green-650"
style={{
height: '6px',
marginBottom: '1px',
display: 'flex',
}}
>
<div
data-testid="margin-health-chart-red"
className="bg-vega-pink-550"
style={{
height: '100%',
width: `${red * 100}%`,
}}
></div>
<div
data-testid="margin-health-chart-orange"
className="bg-vega-orange"
style={{
height: '100%',
width: `${orange * 100}%`,
}}
></div>
<div
data-testid="margin-health-chart-yellow"
className="bg-vega-yellow"
style={{
height: '100%',
width: `${yellow * 100}%`,
}}
></div>
<div
data-testid="margin-health-chart-green"
className="bg-vega-green-600"
style={{
height: '100%',
width: `${green * 100}%`,
}}
></div>
{balanceMarker > 0 && balanceMarker < 100 && (
<div
data-testid="margin-health-chart-balance"
className="absolute bg-vega-blue"
style={{
height: '8px',
width: '8px',
top: '-1px',
transform: 'translate(-4px, 0px)',
borderRadius: '50%',
border: '1px solid white',
backgroundColor: 'blue',
left: `${balanceMarker * 100}%`,
}}
></div>
)}
</div>
</Tooltip>
</div>
);
};
@@ -0,0 +1,154 @@
import {
MarginHealthChart,
MarginHealthChartTooltip,
} from './margin-health-chart';
import { act, render, screen } from '@testing-library/react';
import type { MarginFieldsFragment } from './__generated__/Margins';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
const asset: AssetFieldsFragment = {
id: 'assetId',
decimals: 2,
} as AssetFieldsFragment;
const margins: MarginFieldsFragment = {
asset: {
id: 'assetId',
},
collateralReleaseLevel: '1000',
initialLevel: '800',
searchLevel: '600',
maintenanceLevel: '400',
market: {
id: 'marketId',
},
};
const getMargins = jest.fn(() => margins);
const getBalance = jest.fn(() => '0');
jest.mock('./margin-data-provider', () => ({}));
jest.mock('@vegaprotocol/assets', () => ({
useAssetsMapProvider: () => {
return {
data: {
assetId: asset,
},
};
},
}));
jest.mock('@vegaprotocol/wallet', () => ({
useVegaWallet: () => {
return {
pubKey: 'partyId',
};
},
}));
jest.mock('@vegaprotocol/data-provider', () => ({
useDataProvider: () => {
return {
data: getMargins(),
};
},
}));
jest.mock('./use-account-balance', () => ({
useAccountBalance: () => {
return {
accountBalance: getBalance(),
};
},
}));
jest.mock('./use-market-account-balance', () => ({
useMarketAccountBalance: () => {
return {
accountBalance: '700',
};
},
}));
describe('MarginHealthChart', () => {
it('should render correct values', async () => {
render(<MarginHealthChart marketId="marketId" assetId="assetId" />);
const chart = screen.getByTestId('margin-health-chart');
expect(chart).toHaveTextContent('3.00 above maintenance level');
const red = screen.getByTestId('margin-health-chart-red');
const orange = screen.getByTestId('margin-health-chart-orange');
const yellow = screen.getByTestId('margin-health-chart-yellow');
const green = screen.getByTestId('margin-health-chart-green');
const balance = screen.getByTestId('margin-health-chart-balance');
expect(parseInt(red.style.width)).toBe(40);
expect(parseInt(orange.style.width)).toBe(20);
expect(parseInt(yellow.style.width)).toBe(10);
expect(parseInt(green.style.width)).toBe(30);
expect(parseInt(balance.style.left)).toBe(70);
});
it('should use correct scale', async () => {
getBalance.mockReturnValueOnce('1300');
await act(async () => {
render(<MarginHealthChart marketId="marketId" assetId="assetId" />);
});
await screen.findByTestId('margin-health-chart');
const red = screen.getByTestId('margin-health-chart-red');
expect(parseInt(red.style.width)).toBe(20);
});
});
describe('MarginHealthChartTooltip', () => {
it('renders correct values and labels', async () => {
await act(async () => {
render(
<MarginHealthChartTooltip
{...margins}
decimals={asset.decimals}
marginAccountBalance="500"
/>
);
});
const labels = await screen.findAllByTestId('margin-health-tooltip-label');
const expectedLabels = [
'maintenance level',
'balance',
'search level',
'initial level',
'release level',
];
labels.forEach((value, i) => {
expect(value).toHaveTextContent(expectedLabels[i]);
});
const values = await screen.findAllByTestId('margin-health-tooltip-value');
const expectedValues = ['4.00', '5.00', '6.00', '8.00', '10.00'];
values.forEach((value, i) => {
expect(value).toHaveTextContent(expectedValues[i]);
});
});
it('renders balance in correct place', async () => {
const { rerender } = render(
<MarginHealthChartTooltip
{...margins}
decimals={asset.decimals}
marginAccountBalance="700"
/>
);
let values = await screen.findAllByTestId('margin-health-tooltip-value');
expect(values[2]).toHaveTextContent('7.00');
rerender(
<MarginHealthChartTooltip
{...margins}
decimals={asset.decimals}
marginAccountBalance="900"
/>
);
values = await screen.findAllByTestId('margin-health-tooltip-value');
expect(values.length).toBe(5);
expect(values[3]).toHaveTextContent('9.00');
});
});
+8
View File
@@ -290,6 +290,14 @@ export const TransferFee = ({
decimals?: number;
}) => {
if (!feeFactor || !amount || !transferAmount || !fee) return null;
if (
isNaN(Number(feeFactor)) ||
isNaN(Number(amount)) ||
isNaN(Number(transferAmount)) ||
isNaN(Number(fee))
) {
return null;
}
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
@@ -23,7 +23,6 @@ export const useAccountBalance = (assetId?: string) => {
},
[assetId]
);
useDataProvider({
dataProvider: accountsDataProvider,
variables,
@@ -22,7 +22,6 @@ export const useMarketAccountBalance = (marketId: string) => {
},
[marketId]
);
useDataProvider({
dataProvider: accountsDataProvider,
variables: { partyId: pubKey || '' },
@@ -0,0 +1,11 @@
import type { ProposalSubmissionBody } from '@vegaprotocol/wallet';
import { sendVegaTx } from './wallet-client';
import { waitForProposal } from './propose-market';
import { determineId } from '../utils';
const vegaPubKey = Cypress.env('VEGA_PUBLIC_KEY');
export async function submitProposal(proposalTx: ProposalSubmissionBody) {
cy.highlight('Submitting proposal');
const result = await sendVegaTx(vegaPubKey, proposalTx);
await waitForProposal(determineId(result.result.transaction.signature.value));
}
@@ -1,5 +1,6 @@
import { createWalletClient, sendVegaTx } from '../capsule/wallet-client';
import { createWalletClient } from '../capsule/wallet-client';
import type { ProposalSubmissionBody } from '@vegaprotocol/wallet';
import { submitProposal } from '../capsule/submit-proposal';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
@@ -15,11 +16,8 @@ export const addVegaWalletSubmitProposal = () => {
Cypress.Commands.add('VegaWalletSubmitProposal', (proposalTx) => {
const vegaWalletUrl = Cypress.env('VEGA_WALLET_URL');
const token = Cypress.env('VEGA_WALLET_API_TOKEN');
const vegaPubKey = Cypress.env('VEGA_PUBLIC_KEY');
createWalletClient(vegaWalletUrl, token);
cy.highlight('Submitting proposal');
sendVegaTx(vegaPubKey, proposalTx);
cy.wrap(submitProposal(proposalTx));
});
};
+22 -14
View File
@@ -44,27 +44,35 @@ export const checkSorting = (
orderTabDesc: string[]
) => {
checkSortChange(orderTabDefault, column);
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="${column}"]`).click();
});
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).last().click();
});
checkSortChange(orderTabAsc, column);
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="${column}"]`).click();
});
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).click();
});
checkSortChange(orderTabDesc, column);
cy.get('.ag-header-container').within(() => {
cy.get(`[col-id="${column}"]`).click();
});
cy.get('.ag-header-container')
.last()
.within(() => {
cy.get(`[col-id="${column}"]`).click();
});
};
const checkSortChange = (tabsArr: string[], column: string) => {
cy.get('.ag-center-cols-container').within(() => {
tabsArr.forEach((entry, i) => {
cy.get(`[row-index="${i}"]`).within(() => {
cy.get(`[col-id="${column}"]`).should('have.text', tabsArr[i]);
cy.get('.ag-center-cols-container')
.last()
.within(() => {
tabsArr.forEach((entry, i) => {
cy.get(`[row-index="${i}"]`).within(() => {
cy.get(`[col-id="${column}"]`).should('have.text', tabsArr[i]);
});
});
});
});
};
type Edges = { node: unknown }[];
@@ -1,4 +1,4 @@
import type { MouseEvent } from 'react';
import type { MouseEvent, ReactNode } from 'react';
import { useCallback } from 'react';
import get from 'lodash/get';
@@ -7,6 +7,7 @@ interface MarketNameCellProps {
data?: { id?: string; marketId?: string; market?: { id: string } };
idPath?: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
defaultValue?: ReactNode;
}
export const MarketNameCell = ({
@@ -26,10 +27,13 @@ export const MarketNameCell = ({
},
[id, onMarketClick]
);
if (!data) return null;
return (
if (!value || !data) return null;
return onMarketClick ? (
<button onClick={handleOnClick} tabIndex={0}>
{value}
</button>
) : (
// eslint-disable-next-line react/jsx-no-useless-fragment
<>{value}</>
);
};
+6 -4
View File
@@ -24,11 +24,13 @@ export type VegaValueFormatterParams<TRow, TField extends Field> = RowHelper<
TField
>;
export type VegaValueGetterParams<TRow, TField extends Field> = RowHelper<
export type VegaValueGetterParams<TRow> = Omit<
ValueGetterParams,
TRow,
TField
>;
'data' | 'node'
> & {
data?: TRow;
node: (Omit<RowNode, 'data'> & { data?: TRow }) | null;
};
export type VegaICellRendererParams<TRow, TField extends Field = string> = Omit<
RowHelper<ICellRendererParams, TRow, TField>,
@@ -18,7 +18,7 @@ export const MarginWarning = ({ margin, balance, asset }: Props) => {
return (
<Notification
intent={Intent.Warning}
testId="dealticket-warning-margin"
testId="deal-ticket-warning-margin"
message={`You may not have enough margin available to open this position. ${addDecimalsFormatNumber(
margin,
asset.decimals
@@ -18,7 +18,7 @@ export const ZeroBalanceError = ({
return (
<Notification
intent={Intent.Warning}
testId="dealticket-error-message-zero-balance"
testId="deal-ticket-error-message-zero-balance"
message={
<>
{t(
@@ -7,11 +7,13 @@ import { DealTicket } from './deal-ticket';
export interface DealTicketContainerProps {
marketId: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onClickCollateral?: () => void;
}
export const DealTicketContainer = ({
marketId,
onMarketClick,
onClickCollateral,
}: DealTicketContainerProps) => {
const {
@@ -47,6 +49,7 @@ export const DealTicketContainer = ({
marketData={marketData}
submit={(orderSubmission) => create({ orderSubmission })}
onClickCollateral={onClickCollateral}
onMarketClick={onMarketClick}
/>
) : (
<Splash>
@@ -1,14 +1,40 @@
import { useCallback, useState } from 'react';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import classnames from 'classnames';
import type { ReactNode } from 'react';
import { getFeeDetailsValues } from '../../hooks/use-fee-deal-ticket-details';
import type { FeeDetails } from '../../hooks/use-fee-deal-ticket-details';
import { t } from '@vegaprotocol/i18n';
import { FeesBreakdown } from '@vegaprotocol/markets';
import { useVegaWallet } from '@vegaprotocol/wallet';
export interface DealTicketFeeDetailProps {
import type { Market } from '@vegaprotocol/markets';
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder';
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import { formatRange, formatValue } from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
NOTIONAL_SIZE_TOOLTIP_TEXT,
MARGIN_DIFF_TOOLTIP_TEXT,
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
TOTAL_MARGIN_AVAILABLE,
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
MARGIN_ACCOUNT_TOOLTIP_TEXT,
} from '../../constants';
const emptyValue = '-';
export interface DealTicketFeeDetailPros {
label: string;
value?: string | number | null;
labelDescription?: string | ReactNode;
symbol?: string;
value?: string | null | undefined;
symbol: string;
indent?: boolean | undefined;
labelDescription?: ReactNode;
formattedValue?: string;
onClick?: () => void;
}
export const DealTicketFeeDetail = ({
@@ -16,51 +42,317 @@ export const DealTicketFeeDetail = ({
value,
labelDescription,
symbol,
}: DealTicketFeeDetailProps) => (
<div className="text-xs mt-2 flex justify-between items-center gap-4 flex-wrap">
<div>
indent,
onClick,
formattedValue,
}: DealTicketFeeDetailPros) => {
const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`;
const valueElement = onClick ? (
<button
onClick={onClick}
className="text-neutral-500 dark:text-neutral-300"
>
{displayValue}
</button>
) : (
<div className="text-neutral-500 dark:text-neutral-300">{displayValue}</div>
);
return (
<div
data-testid={
'deal-ticket-fee-' + label.toLocaleLowerCase().replace(/\s/g, '-')
}
key={typeof label === 'string' ? label : 'value-dropdown'}
className={classnames(
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
{ 'ml-2': indent }
)}
>
<Tooltip description={labelDescription}>
<div>{label}</div>
</Tooltip>
<Tooltip description={`${value ?? '-'} ${symbol || ''}`}>
{valueElement}
</Tooltip>
</div>
<div className="text-neutral-500 dark:text-neutral-300">{`${value ?? '-'} ${
symbol || ''
}`}</div>
</div>
);
);
};
export interface DealTicketFeeDetailsProps {
generalAccountBalance?: string;
marginAccountBalance?: string;
market: Market;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
assetSymbol: string;
notionalSize: string | null;
feeEstimate: EstimateFeesQuery['estimateFees'] | undefined;
positionEstimate: EstimatePositionQuery['estimatePosition'];
}
export const DealTicketFeeDetails = ({
marginAccountBalance,
generalAccountBalance,
assetSymbol,
feeEstimate,
market,
onMarketClick,
notionalSize,
positionEstimate,
}: DealTicketFeeDetailsProps) => {
const [breakdownDialog, setBreakdownDialog] = useState(false);
const { pubKey: partyId } = useVegaWallet();
const { data: currentMargins } = useDataProvider({
dataProvider: marketMarginDataProvider,
variables: { marketId: market.id, partyId: partyId || '' },
skip: !partyId,
});
const liquidationEstimate = positionEstimate?.liquidation;
const marginEstimate = positionEstimate?.margin;
const totalBalance =
BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0');
const { settlementAsset: asset } =
market.tradableInstrument.instrument.product;
const { decimals: assetDecimals, quantum } = asset;
const marketDecimals = market.decimalPlaces;
let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined;
if (marginEstimate) {
if (currentMargins) {
marginRequiredBestCase = (
BigInt(marginEstimate.bestCase.initialLevel) -
BigInt(currentMargins.initialLevel)
).toString();
if (marginRequiredBestCase.startsWith('-')) {
marginRequiredBestCase = '0';
}
marginRequiredWorstCase = (
BigInt(marginEstimate.worstCase.initialLevel) -
BigInt(currentMargins.initialLevel)
).toString();
if (marginRequiredWorstCase.startsWith('-')) {
marginRequiredWorstCase = '0';
}
} else {
marginRequiredBestCase = marginEstimate.bestCase.initialLevel;
marginRequiredWorstCase = marginEstimate.worstCase.initialLevel;
}
}
const totalMarginAvailable = (
currentMargins
? totalBalance - BigInt(currentMargins.maintenanceLevel)
: totalBalance
).toString();
let deductionFromCollateral = null;
let projectedMargin = null;
if (marginAccountBalance) {
const deductionFromCollateralBestCase =
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) -
BigInt(marginAccountBalance);
const deductionFromCollateralWorstCase =
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) -
BigInt(marginAccountBalance);
deductionFromCollateral = (
<DealTicketFeeDetail
indent
label={t('Deduction from collateral')}
value={formatRange(
deductionFromCollateralBestCase > 0
? deductionFromCollateralBestCase.toString()
: '0',
deductionFromCollateralWorstCase > 0
? deductionFromCollateralWorstCase.toString()
: '0',
assetDecimals
)}
formattedValue={formatRange(
deductionFromCollateralBestCase > 0
? deductionFromCollateralBestCase.toString()
: '0',
deductionFromCollateralWorstCase > 0
? deductionFromCollateralWorstCase.toString()
: '0',
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT(assetSymbol)}
/>
);
projectedMargin = (
<DealTicketFeeDetail
label={t('Projected margin')}
value={formatRange(
marginEstimate?.bestCase.initialLevel,
marginEstimate?.worstCase.initialLevel,
assetDecimals
)}
formattedValue={formatRange(
marginEstimate?.bestCase.initialLevel,
marginEstimate?.worstCase.initialLevel,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={EST_TOTAL_MARGIN_TOOLTIP_TEXT}
/>
);
}
let liquidationPriceEstimate = emptyValue;
if (liquidationEstimate) {
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
liquidationEstimate.bestCase.including_buy_orders.replace(/\..*/, '')
);
const liquidationEstimateBestCaseIncludingSellOrders = BigInt(
liquidationEstimate.bestCase.including_sell_orders.replace(/\..*/, '')
);
const liquidationEstimateBestCase =
liquidationEstimateBestCaseIncludingBuyOrders >
liquidationEstimateBestCaseIncludingSellOrders
? liquidationEstimateBestCaseIncludingBuyOrders
: liquidationEstimateBestCaseIncludingSellOrders;
const liquidationEstimateWorstCaseIncludingBuyOrders = BigInt(
liquidationEstimate.worstCase.including_buy_orders.replace(/\..*/, '')
);
const liquidationEstimateWorstCaseIncludingSellOrders = BigInt(
liquidationEstimate.worstCase.including_sell_orders.replace(/\..*/, '')
);
const liquidationEstimateWorstCase =
liquidationEstimateWorstCaseIncludingBuyOrders >
liquidationEstimateWorstCaseIncludingSellOrders
? liquidationEstimateWorstCaseIncludingBuyOrders
: liquidationEstimateWorstCaseIncludingSellOrders;
// The estimate order query API gives us the liquidation price in formatted by asset decimals.
// We need to calculate it with asset decimals, but display it with market decimals precision until the API changes.
liquidationPriceEstimate = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
(liquidationEstimateBestCase > liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
assetDecimals,
undefined,
market.decimalPlaces
);
}
const onAccountBreakdownDialogClose = useCallback(
() => setBreakdownDialog(false),
[]
);
const quoteName = market.tradableInstrument.instrument.product.quoteName;
export const DealTicketFeeDetails = (props: FeeDetails) => {
const details = getFeeDetailsValues(props);
return (
<div>
{details.map(
({
label,
value,
labelDescription,
symbol,
indent,
formattedValue,
}) => (
<div
key={typeof label === 'string' ? label : 'value-dropdown'}
className={classnames(
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
{ 'ml-2': indent }
)}
>
<div>
<Tooltip description={labelDescription}>
<div>{label}</div>
</Tooltip>
</div>
<Tooltip description={`${value ?? '-'} ${symbol || ''}`}>
<div className="text-neutral-500 dark:text-neutral-300">{`${
formattedValue ?? '-'
} ${symbol || ''}`}</div>
</Tooltip>
</div>
)
<DealTicketFeeDetail
label={t('Notional')}
value={formatValue(notionalSize, marketDecimals)}
formattedValue={formatValue(notionalSize, marketDecimals)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
/>
<DealTicketFeeDetail
label={t('Fees')}
value={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
}
formattedValue={
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
}
labelDescription={
<>
<span>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
)}
</span>
<FeesBreakdown
fees={feeEstimate?.fees}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
/>
</>
}
symbol={assetSymbol}
/>
<DealTicketFeeDetail
label={t('Margin required')}
value={formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
)}
formattedValue={formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals,
quantum
)}
labelDescription={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}
symbol={assetSymbol}
/>
<DealTicketFeeDetail
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
formattedValue={formatValue(
totalMarginAvailable,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={TOTAL_MARGIN_AVAILABLE(
formatValue(generalAccountBalance, assetDecimals, quantum),
formatValue(marginAccountBalance, assetDecimals, quantum),
formatValue(currentMargins?.maintenanceLevel, assetDecimals, quantum),
assetSymbol
)}
/>
{deductionFromCollateral}
<DealTicketFeeDetail
label={t('Current margin allocation')}
indent
onClick={
generalAccountBalance ? () => setBreakdownDialog(true) : undefined
}
value={formatValue(marginAccountBalance, assetDecimals)}
symbol={assetSymbol}
labelDescription={MARGIN_ACCOUNT_TOOLTIP_TEXT}
formattedValue={formatValue(
marginAccountBalance,
assetDecimals,
quantum
)}
/>
{projectedMargin}
<DealTicketFeeDetail
label={t('Liquidation price estimate')}
value={liquidationPriceEstimate}
formattedValue={liquidationPriceEstimate}
symbol={quoteName}
labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}
/>
{partyId && (
<AccountBreakdownDialog
assetId={breakdownDialog ? asset.id : undefined}
partyId={partyId}
onMarketClick={onMarketClick}
onClose={onAccountBreakdownDialogClose}
/>
)}
</div>
);
@@ -25,7 +25,7 @@ export const DealTicketLimitAmount = ({
const renderError = () => {
if (sizeError) {
return (
<InputError testId="dealticket-error-message-size-limit">
<InputError testId="deal-ticket-error-message-size-limit">
{sizeError}
</InputError>
);
@@ -33,7 +33,7 @@ export const DealTicketLimitAmount = ({
if (priceError) {
return (
<InputError testId="dealticket-error-message-price-limit">
<InputError testId="deal-ticket-error-message-price-limit">
{priceError}
</InputError>
);
@@ -90,7 +90,7 @@ export const DealTicketMarketAmount = ({
{sizeError && (
<InputError
intent="danger"
testId="dealticket-error-message-size-market"
testId="deal-ticket-error-message-size-market"
>
{sizeError}
</InputError>
@@ -31,7 +31,7 @@ import {
} from '@vegaprotocol/positions';
import { toBigNum, removeDecimal } from '@vegaprotocol/utils';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { useEstimateFees } from '../../hooks/use-fee-deal-ticket-details';
import { useEstimateFees } from '../../hooks/use-estimate-fees';
import { getDerivedPrice } from '../../utils/get-price';
import type { OrderInfo } from '@vegaprotocol/types';
@@ -55,17 +55,17 @@ import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
import { useOrderForm } from '../../hooks/use-order-form';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketMarginDataProvider } from '@vegaprotocol/positions';
export interface DealTicketProps {
market: Market;
marketData: MarketData;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
submit: (order: OrderSubmission) => void;
onClickCollateral?: () => void;
}
export const DealTicket = ({
market,
onMarketClick,
marketData,
submit,
onClickCollateral,
@@ -124,7 +124,7 @@ export const DealTicket = ({
normalizedOrder.size,
market.positionDecimalPlaces
).multipliedBy(toBigNum(price, market.decimalPlaces)),
asset.decimals
market.decimalPlaces
);
}
return null;
@@ -133,7 +133,6 @@ export const DealTicket = ({
normalizedOrder?.size,
market.decimalPlaces,
market.positionDecimalPlaces,
asset.decimals,
]);
const feeEstimate = useEstimateFees(
@@ -176,12 +175,6 @@ export const DealTicket = ({
const assetSymbol =
market.tradableInstrument.instrument.product.settlementAsset.symbol;
const { data: currentMargins } = useDataProvider({
dataProvider: marketMarginDataProvider,
variables: { marketId: market.id, partyId: pubKey || '' },
skip: !pubKey,
});
useEffect(() => {
if (!pubKey) {
setError('summary', {
@@ -200,7 +193,8 @@ export const DealTicket = ({
return;
}
const hasNoBalance = !BigInt(generalAccountBalance);
const hasNoBalance =
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
if (hasNoBalance) {
setError('summary', {
message: SummaryValidationType.NoCollateral,
@@ -224,6 +218,7 @@ export const DealTicket = ({
marketState,
marketTradingMode,
generalAccountBalance,
marginAccountBalance,
pubKey,
setError,
clearErrors,
@@ -487,6 +482,7 @@ export const DealTicket = ({
}
/>
<DealTicketFeeDetails
onMarketClick={onMarketClick}
feeEstimate={feeEstimate}
notionalSize={notionalSize}
assetSymbol={assetSymbol}
@@ -494,8 +490,6 @@ export const DealTicket = ({
generalAccountBalance={generalAccountBalance}
positionEstimate={positionEstimate?.estimatePosition}
market={market}
currentInitialMargin={currentMargins?.initialLevel}
currentMaintenanceMargin={currentMargins?.maintenanceLevel}
/>
</form>
</TinyScroll>
@@ -536,7 +530,7 @@ const SummaryMessage = memo(
if (isReadOnly) {
return (
<div className="mb-2">
<InputError testId="dealticket-error-message-summary">
<InputError testId="deal-ticket-error-message-summary">
{
'You need to connect your own wallet to start trading on this market'
}
@@ -585,7 +579,7 @@ const SummaryMessage = memo(
if (errorMessage) {
return (
<div className="mb-2">
<InputError testId="dealticket-error-message-summary">
<InputError testId="deal-ticket-error-message-summary">
{errorMessage}
</InputError>
</div>
@@ -613,7 +607,7 @@ const SummaryMessage = memo(
<div className="mb-2">
<Notification
intent={Intent.Warning}
testId={'dealticket-warning-auction'}
testId={'deal-ticket-warning-auction'}
message={t(
'Any orders placed now will not trade until the auction ends'
)}
@@ -31,7 +31,7 @@ export const ExpirySelector = ({
min={minDate}
/>
{errorMessage && (
<InputError testId="dealticket-error-message-expiry">
<InputError testId="deal-ticket-error-message-expiry">
{errorMessage}
</InputError>
)}
@@ -108,7 +108,7 @@ export const TimeInForceSelector = ({
))}
</Select>
{errorMessage && (
<InputError testId="dealticket-error-message-tif">
<InputError testId="deal-ticket-error-message-tif">
{renderError(errorMessage)}
</InputError>
)}
@@ -83,7 +83,7 @@ export const TypeSelector = ({
onChange={(e) => onSelect(e.target.value as Schema.OrderType)}
/>
{errorMessage && (
<InputError testId="dealticket-error-message-type">
<InputError testId="deal-ticket-error-message-type">
{renderError(errorMessage as MarketModeValidationType)}
</InputError>
)}
+1 -1
View File
@@ -1,2 +1,2 @@
export * from './__generated__/EstimateOrder';
export * from './use-fee-deal-ticket-details';
export * from './use-estimate-fees';
@@ -0,0 +1,25 @@
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useEstimateFeesQuery } from './__generated__/EstimateOrder';
export const useEstimateFees = (
order?: OrderSubmissionBody['orderSubmission']
) => {
const { pubKey } = useVegaWallet();
const { data } = useEstimateFeesQuery({
variables: order && {
marketId: order.marketId,
partyId: pubKey || '',
price: order.price,
size: order.size,
side: order.side,
timeInForce: order.timeInForce,
type: order.type,
},
fetchPolicy: 'no-cache',
skip: !pubKey || !order?.size || !order?.price,
});
return data?.estimateFees;
};
@@ -1,327 +0,0 @@
import { FeesBreakdown } from '@vegaprotocol/markets';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
isNumeric,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { Market } from '@vegaprotocol/markets';
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import {
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
NOTIONAL_SIZE_TOOLTIP_TEXT,
MARGIN_ACCOUNT_TOOLTIP_TEXT,
MARGIN_DIFF_TOOLTIP_TEXT,
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
TOTAL_MARGIN_AVAILABLE,
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
} from '../constants';
import { useEstimateFeesQuery } from './__generated__/EstimateOrder';
import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
export const useEstimateFees = (
order?: OrderSubmissionBody['orderSubmission']
) => {
const { pubKey } = useVegaWallet();
const { data } = useEstimateFeesQuery({
variables: order && {
marketId: order.marketId,
partyId: pubKey || '',
price: order.price,
size: order.size,
side: order.side,
timeInForce: order.timeInForce,
type: order.type,
},
skip: !pubKey || !order?.size || !order?.price,
fetchPolicy: 'no-cache',
});
return data?.estimateFees;
};
export interface FeeDetails {
generalAccountBalance?: string;
marginAccountBalance?: string;
market: Market;
assetSymbol: string;
notionalSize: string | null;
feeEstimate: EstimateFeesQuery['estimateFees'] | undefined;
currentInitialMargin?: string;
currentMaintenanceMargin?: string;
positionEstimate: EstimatePositionQuery['estimatePosition'];
}
const emptyValue = '-';
export const formatValue = (
value: string | number | null | undefined,
formatDecimals: number,
quantum?: string
): string => {
if (!isNumeric(value)) return emptyValue;
if (!quantum) return addDecimalsFormatNumber(value, formatDecimals);
return addDecimalsFormatNumberQuantum(value, formatDecimals, quantum);
};
export const formatRange = (
min: string | number | null | undefined,
max: string | number | null | undefined,
formatDecimals: number,
quantum?: string
) => {
const minFormatted = formatValue(min, formatDecimals, quantum);
const maxFormatted = formatValue(max, formatDecimals, quantum);
if (minFormatted !== maxFormatted) {
return `${minFormatted} - ${maxFormatted}`;
}
if (minFormatted !== emptyValue) {
return minFormatted;
}
return maxFormatted;
};
export const getFeeDetailsValues = ({
marginAccountBalance,
generalAccountBalance,
assetSymbol,
feeEstimate,
market,
notionalSize,
currentInitialMargin,
currentMaintenanceMargin,
positionEstimate,
}: FeeDetails) => {
const liquidationEstimate = positionEstimate?.liquidation;
const marginEstimate = positionEstimate?.margin;
const totalBalance =
BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0');
const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals;
const quantum =
market.tradableInstrument.instrument.product.settlementAsset.quantum;
const details: {
label: string;
value?: string | null;
formattedValue?: string | null;
symbol: string;
indent?: boolean;
labelDescription?: React.ReactNode;
}[] = [
{
label: t('Notional'),
value: formatValue(notionalSize, assetDecimals),
formattedValue: formatValue(notionalSize, assetDecimals, quantum),
symbol: assetSymbol,
labelDescription: NOTIONAL_SIZE_TOOLTIP_TEXT(assetSymbol),
},
{
label: t('Fees'),
value:
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`,
formattedValue:
feeEstimate?.totalFeeAmount &&
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`,
labelDescription: (
<>
<span>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
)}
</span>
<FeesBreakdown
fees={feeEstimate?.fees}
feeFactors={market.fees.factors}
symbol={assetSymbol}
decimals={assetDecimals}
/>
</>
),
symbol: assetSymbol,
},
];
let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined;
if (marginEstimate) {
if (currentInitialMargin) {
marginRequiredBestCase = (
BigInt(marginEstimate.bestCase.initialLevel) -
BigInt(currentInitialMargin)
).toString();
if (marginRequiredBestCase.startsWith('-')) {
marginRequiredBestCase = '0';
}
marginRequiredWorstCase = (
BigInt(marginEstimate.worstCase.initialLevel) -
BigInt(currentInitialMargin)
).toString();
if (marginRequiredWorstCase.startsWith('-')) {
marginRequiredWorstCase = '0';
}
} else {
marginRequiredBestCase = marginEstimate.bestCase.initialLevel;
marginRequiredWorstCase = marginEstimate.worstCase.initialLevel;
}
}
details.push({
label: t('Margin required'),
formattedValue: formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals,
quantum
),
value: formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
),
symbol: assetSymbol,
labelDescription: MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol),
});
const totalMarginAvailable = (
currentMaintenanceMargin
? totalBalance - BigInt(currentMaintenanceMargin)
: totalBalance
).toString();
details.push({
indent: true,
label: t('Total margin available'),
formattedValue: formatValue(totalMarginAvailable, assetDecimals, quantum),
value: formatValue(totalMarginAvailable, assetDecimals),
symbol: assetSymbol,
labelDescription: TOTAL_MARGIN_AVAILABLE(
formatValue(generalAccountBalance, assetDecimals, quantum),
formatValue(marginAccountBalance, assetDecimals, quantum),
formatValue(currentMaintenanceMargin, assetDecimals, quantum),
assetSymbol
),
});
if (marginAccountBalance) {
const deductionFromCollateralBestCase =
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) -
BigInt(marginAccountBalance);
const deductionFromCollateralWorstCase =
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) -
BigInt(marginAccountBalance);
details.push({
indent: true,
label: t('Deduction from collateral'),
value: formatRange(
deductionFromCollateralBestCase > 0
? deductionFromCollateralBestCase.toString()
: '0',
deductionFromCollateralWorstCase > 0
? deductionFromCollateralWorstCase.toString()
: '0',
assetDecimals
),
formattedValue: formatRange(
deductionFromCollateralBestCase > 0
? deductionFromCollateralBestCase.toString()
: '0',
deductionFromCollateralWorstCase > 0
? deductionFromCollateralWorstCase.toString()
: '0',
assetDecimals,
quantum
),
symbol: assetSymbol,
labelDescription: DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT(assetSymbol),
});
details.push({
label: t('Projected margin'),
value: formatRange(
marginEstimate?.bestCase.initialLevel,
marginEstimate?.worstCase.initialLevel,
assetDecimals
),
formattedValue: formatRange(
marginEstimate?.bestCase.initialLevel,
marginEstimate?.worstCase.initialLevel,
assetDecimals,
quantum
),
symbol: assetSymbol,
labelDescription: EST_TOTAL_MARGIN_TOOLTIP_TEXT,
});
}
details.push({
label: t('Current margin allocation'),
value: formatValue(marginAccountBalance, assetDecimals),
symbol: assetSymbol,
labelDescription: MARGIN_ACCOUNT_TOOLTIP_TEXT,
formattedValue: formatValue(marginAccountBalance, assetDecimals, quantum),
});
let liquidationPriceEstimate = emptyValue;
let liquidationPriceEstimateFormatted;
if (liquidationEstimate) {
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
liquidationEstimate.bestCase.including_buy_orders.replace(/\..*/, '')
);
const liquidationEstimateBestCaseIncludingSellOrders = BigInt(
liquidationEstimate.bestCase.including_sell_orders.replace(/\..*/, '')
);
const liquidationEstimateBestCase =
liquidationEstimateBestCaseIncludingBuyOrders >
liquidationEstimateBestCaseIncludingSellOrders
? liquidationEstimateBestCaseIncludingBuyOrders
: liquidationEstimateBestCaseIncludingSellOrders;
const liquidationEstimateWorstCaseIncludingBuyOrders = BigInt(
liquidationEstimate.worstCase.including_buy_orders.replace(/\..*/, '')
);
const liquidationEstimateWorstCaseIncludingSellOrders = BigInt(
liquidationEstimate.worstCase.including_sell_orders.replace(/\..*/, '')
);
const liquidationEstimateWorstCase =
liquidationEstimateWorstCaseIncludingBuyOrders >
liquidationEstimateWorstCaseIncludingSellOrders
? liquidationEstimateWorstCaseIncludingBuyOrders
: liquidationEstimateWorstCaseIncludingSellOrders;
liquidationPriceEstimate = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
(liquidationEstimateBestCase > liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
assetDecimals
);
liquidationPriceEstimateFormatted = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
(liquidationEstimateBestCase > liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
assetDecimals,
quantum
);
}
details.push({
label: t('Liquidation price estimate'),
value: liquidationPriceEstimate,
formattedValue: liquidationPriceEstimateFormatted,
symbol: assetSymbol,
labelDescription: LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
});
return details;
};
-2
View File
@@ -6,7 +6,6 @@ import {
truncateByChars,
isNumeric,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type {
@@ -25,7 +24,6 @@ export const DepositsTable = forwardRef<
return (
<AgGrid
ref={ref}
overlayNoRowsTemplate={t('No deposits')}
defaultColDef={{ resizable: true }}
style={{ width: '100%', height: '100%' }}
suppressCellFocus={true}
+1 -1
View File
@@ -48,7 +48,7 @@ const mapFillUpdateToFill = (
id: marketId,
},
buyer: { id: buyerId, __typename: 'Party' },
seller: { id: buyerId, __typename: 'Party' },
seller: { id: sellerId, __typename: 'Party' },
};
};
+154 -112
View File
@@ -1,20 +1,22 @@
import { forwardRef } from 'react';
import { forwardRef, useMemo } from 'react';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
formatNumberPercentage,
getDateTimeFormat,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type {
VegaValueFormatterParams,
TypedDataAgGrid,
} from '@vegaprotocol/datagrid';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import type { ValueFormatterParams } from 'ag-grid-community';
import type {
ColDef,
ITooltipParams,
ValueFormatterParams,
} from 'ag-grid-community';
import BigNumber from 'bignumber.js';
import type { LiquidityProvisionStatus } from '@vegaprotocol/types';
import { LiquidityProvisionStatusMapping } from '@vegaprotocol/types';
import type { LiquidityProvisionData } from './liquidity-data-provider';
@@ -35,21 +37,152 @@ export interface LiquidityTableProps
symbol?: string;
assetDecimalPlaces?: number;
stakeToCcyVolume: string | null;
quantum?: string | number;
}
export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
({ symbol = '', assetDecimalPlaces, stakeToCcyVolume, ...props }, ref) => {
const assetDecimalsFormatter = ({ value }: ValueFormatterParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0, 5)}`;
};
const stakeToCcyVolumeFormatter = ({ value }: ValueFormatterParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0, 5)}`;
};
(
{ symbol = '', assetDecimalPlaces, stakeToCcyVolume, quantum, ...props },
ref
) => {
const colDefs = useMemo(() => {
const assetDecimalsFormatter = ({ value }: ITooltipParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0)}`;
};
const assetDecimalsQuantumFormatter = ({
value,
}: ValueFormatterParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumberQuantum(
value,
assetDecimalPlaces ?? 0,
quantum ?? 0
)}`;
};
const stakeToCcyVolumeFormatter = ({ value }: ITooltipParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0)}`;
};
const stakeToCcyVolumeQuantumFormatter = ({
value,
}: ValueFormatterParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumberQuantum(
newValue,
assetDecimalPlaces ?? 0,
quantum ?? 0
)}`;
};
const defs: ColDef[] = [
{
headerName: t('Party'),
field: 'party.id',
headerTooltip: t(
'The public key of the party making this commitment.'
),
},
{
headerName: t(`Commitment (${symbol})`),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
'The amount committed to the market by this liquidity provider.'
),
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
{
headerName: t(`Share`),
field: 'equityLikeShare',
type: 'rightAligned',
headerTooltip: t(
'The equity-like share of liquidity of the market used to determine allocation of LP fees. Calculated based on share of total liquidity, with a premium added for length of commitment.'
),
valueFormatter: percentageFormatter,
},
{
headerName: t('Proposed fee'),
headerTooltip: t(
'The fee percentage (per trade) proposed by each liquidity provider.'
),
field: 'fee',
type: 'rightAligned',
valueFormatter: percentageFormatter,
},
{
headerName: t('Market valuation at entry'),
field: 'averageEntryValuation',
type: 'rightAligned',
headerTooltip: t(
'The valuation of the market at the time the liquidity commitment was made. Commitments made at a lower valuation earlier in the lifetime of the market would be expected to have a higher equity-like share if the market has grown. If a commitment is amended, value will reflect the average of the market valuations across the lifetime of the commitment.'
),
minWidth: 160,
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
{
headerName: t('Obligation'),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
`The liquidity provider's obligation to the market, calculated as the liquidity commitment amount multiplied by the value of the stake_to_ccy_volume network parameter to convert into units of liquidity volume. The obligation can be met by a combination of LP orders and limit orders on the order book.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Supplied'),
field: 'balance',
type: 'rightAligned',
headerTooltip: t(
`The amount of liquidity volume supplied by the LP order in order to meet the obligation. If the obligation is already met in full by other limit orders from the same Vega key the LP order is not required and this value will be zero. Also note if the target stake for the market is less than the obligation the full value of the obligation may not be required.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Status'),
headerTooltip: t('The current status of this liquidity provision.'),
field: 'status',
valueFormatter: ({ value }) => {
if (!value) return value;
return LiquidityProvisionStatusMapping[
value as LiquidityProvisionStatus
];
},
},
{
headerName: t('Created'),
headerTooltip: t(
'The date and time this liquidity provision was created.'
),
field: 'createdAt',
type: 'rightAligned',
valueFormatter: dateValueFormatter,
},
{
headerName: t('Updated'),
headerTooltip: t(
'The date and time this liquidity provision was last updated.'
),
field: 'updatedAt',
type: 'rightAligned',
valueFormatter: dateValueFormatter,
},
];
return defs;
}, [assetDecimalPlaces, quantum, stakeToCcyVolume, symbol]);
return (
<AgGrid
@@ -66,99 +199,8 @@ export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
}}
storeKey="liquidityProvisionTable"
{...props}
>
<AgGridColumn
headerName={t('Party')}
field="party.id"
headerTooltip={t(
'The public key of the party making this commitment.'
)}
/>
<AgGridColumn
headerName={t(`Commitment (${symbol})`)}
field="commitmentAmount"
type="rightAligned"
headerTooltip={t(
'The amount committed to the market by this liquidity provider.'
)}
valueFormatter={assetDecimalsFormatter}
/>
<AgGridColumn
headerName={t('Share')}
field="equityLikeShare"
type="rightAligned"
headerTooltip={t(
'The equity-like share of liquidity of the market used to determine allocation of LP fees. Calculated based on share of total liquidity, with a premium added for length of commitment.'
)}
valueFormatter={percentageFormatter}
/>
<AgGridColumn
headerName={t('Proposed fee')}
headerTooltip={t(
'The fee percentage (per trade) proposed by each liquidity provider.'
)}
field="fee"
type="rightAligned"
valueFormatter={percentageFormatter}
/>
<AgGridColumn
headerName={t('Market valuation at entry')}
field="averageEntryValuation"
type="rightAligned"
headerTooltip={t(
'The valuation of the market at the time the liquidity commitment was made. Commitments made at a lower valuation earlier in the lifetime of the market would be expected to have a higher equity-like share if the market has grown. If a commitment is amended, value will reflect the average of the market valuations across the lifetime of the commitment.'
)}
minWidth={160}
valueFormatter={assetDecimalsFormatter}
/>
<AgGridColumn
headerName={t('Obligation')}
field="commitmentAmount"
type="rightAligned"
headerTooltip={t(
`The liquidity provider's obligation to the market, calculated as the liquidity commitment amount multiplied by the value of the stake_to_ccy_siskas network parameter to convert into units of liquidity volume. The obligation can be met by a combination of LP orders and limit orders on the order book.`
)}
valueFormatter={stakeToCcyVolumeFormatter}
/>
<AgGridColumn
headerName={t('Supplied')}
headerTooltip={t(
`The amount of liquidity volume supplied by the LP order in order to meet the obligation. If the obligation is already met in full by other limit orders from the same Vega key the LP order is not required and this value will be zero. Also note if the target stake for the market is less than the obligation the full value of the obligation may not be required.`
)}
field="balance"
type="rightAligned"
valueFormatter={stakeToCcyVolumeFormatter}
/>
<AgGridColumn
headerName={t('Status')}
headerTooltip={t('The current status of this liquidity provision.')}
field="status"
valueFormatter={({
value,
}: VegaValueFormatterParams<LiquidityProvisionData, 'status'>) => {
if (!value) return value;
return LiquidityProvisionStatusMapping[value];
}}
/>
<AgGridColumn
headerName={t('Created')}
headerTooltip={t(
'The date and time this liquidity provision was created.'
)}
field="createdAt"
type="rightAligned"
valueFormatter={dateValueFormatter}
/>
<AgGridColumn
headerName={t('Updated')}
headerTooltip={t(
'The last time this liquidity provision was updated.'
)}
field="updatedAt"
type="rightAligned"
valueFormatter={dateValueFormatter}
/>
</AgGrid>
columnDefs={colDefs}
></AgGrid>
);
}
);
@@ -1,6 +1,5 @@
import {
addDecimalsFormatNumber,
formatNumber,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
@@ -47,7 +46,10 @@ const Row = ({
if (asPercentage) {
return formatNumberPercentage(new BigNumber(value).times(100));
}
return `${formatNumber(Number(value))} ${assetSymbol}`;
if (assetSymbol) {
return `${Number(value).toLocaleString()} ${assetSymbol}`;
}
return Number(value).toLocaleString();
};
const formattedValue = getFormattedValue(value);
@@ -94,7 +94,7 @@ export const marketInfoQuery = (
},
lpPriceRange: '0.02',
liquidityMonitoringParameters: {
triggeringRatio: '0',
triggeringRatio: '0.7',
targetStakeParameters: {
timeWindow: 3600,
scalingFactor: 10,
@@ -76,9 +76,7 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.bestBidPrice'>) => {
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
return data?.data?.bestBidPrice === undefined
? undefined
: toBigNum(data?.data?.bestBidPrice, data.decimalPlaces).toNumber();
@@ -102,12 +100,7 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) => {
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
return data?.data?.bestOfferPrice === undefined
? undefined
: toBigNum(
@@ -134,9 +127,7 @@ export const useColumnDefs = ({ onMarketClick }: Props) => {
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData, 'data.markPrice'>) => {
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
return data?.data?.markPrice === undefined
? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
@@ -2,14 +2,13 @@ import {
addDecimalsFormatNumber,
getDateTimeFormat,
isNumeric,
toBigNum,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
import BigNumber from 'bignumber.js';
import type { ForwardedRef } from 'react';
import { memo, forwardRef } from 'react';
import { memo, forwardRef, useMemo } from 'react';
import {
AgGridLazy as AgGrid,
SetFilter,
@@ -24,11 +23,13 @@ import type {
TypedDataAgGrid,
VegaICellRendererParams,
VegaValueFormatterParams,
VegaValueGetterParams,
} from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react';
import type { Order } from '../order-data-provider';
import { OrderActionsDropdown } from '../order-actions-dropdown';
import { Filter } from '../order-list-manager';
import type { ColDef } from 'ag-grid-community';
export type OrderListTableProps = TypedDataAgGrid<Order> & {
marketId?: string;
@@ -54,49 +55,40 @@ export const OrderListTable = memo<
: filter === undefined || filter === Filter.Open
? true
: false;
return (
<AgGrid
ref={ref}
defaultColDef={{
resizable: true,
sortable: true,
filterParams: { buttons: ['reset'] },
}}
style={{
width: '100%',
height: '100%',
}}
getRowId={({ data }) => data.id}
components={{ MarketNameCell, OrderTypeCell }}
{...props}
>
<AgGridColumn
headerName={t('Market')}
field="market.tradableInstrument.instrument.code"
cellRenderer="MarketNameCell"
cellRendererParams={{ idPath: 'market.id', onMarketClick }}
minWidth={150}
/>
<AgGridColumn
headerName={t('Size')}
field="size"
cellClass="font-mono text-right"
type="rightAligned"
cellClassRules={{
const columnDefs: ColDef[] = useMemo(
() => [
{
headerName: t('Market'),
field: 'market.tradableInstrument.instrument.code',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'market.id', onMarketClick },
minWidth: 150,
},
{
headerName: t('Size'),
field: 'size',
cellClass: 'font-mono text-right',
type: 'rightAligned',
cellClassRules: {
[positiveClassNames]: ({ data }: { data: Order }) =>
data?.side === Schema.Side.SIDE_BUY,
[negativeClassNames]: ({ data }: { data: Order }) =>
data?.side === Schema.Side.SIDE_SELL,
}}
valueFormatter={({
value,
},
valueGetter: ({ data }: VegaValueGetterParams<Order>) => {
return data?.size && data.market
? toBigNum(data.size, data.market.positionDecimalPlaces ?? 0)
.multipliedBy(data.side === Schema.Side.SIDE_SELL ? -1 : 1)
.toNumber()
: undefined;
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Order, 'size'>) => {
if (!data) {
return undefined;
return '';
}
if (!data?.market || !isNumeric(value)) {
if (!data?.market || !isNumeric(data.size)) {
return '-';
}
const prefix = data
@@ -107,33 +99,33 @@ export const OrderListTable = memo<
return (
prefix +
addDecimalsFormatNumber(
value,
data.size,
data.market.positionDecimalPlaces
)
);
}}
minWidth={80}
/>
<AgGridColumn
field="type"
filter={SetFilter}
filterParams={{
},
minWidth: 80,
},
{
field: 'type',
filter: SetFilter,
filterParams: {
set: Schema.OrderTypeMapping,
}}
cellRenderer="OrderTypeCell"
cellRendererParams={{
},
cellRenderer: 'OrderTypeCell',
cellRendererParams: {
onClick: onOrderTypeClick,
}}
minWidth={80}
/>
<AgGridColumn
field="status"
filter={SetFilter}
filterParams={{
},
minWidth: 80,
},
{
field: 'status',
filter: SetFilter,
filterParams: {
set: Schema.OrderStatusMapping,
readonly: filter !== undefined,
}}
valueFormatter={({
},
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<Order, 'status'>) => {
@@ -145,8 +137,8 @@ export const OrderListTable = memo<
}`;
}
return value ? Schema.OrderStatusMapping[value] : '';
}}
cellRenderer={({
},
cellRenderer: ({
valueFormatted,
data,
}: {
@@ -156,45 +148,51 @@ export const OrderListTable = memo<
<span data-testid={`order-status-${data?.id}`}>
{valueFormatted}
</span>
)}
minWidth={100}
/>
<AgGridColumn
headerName={t('Filled')}
field="remaining"
cellClass="font-mono text-right"
type="rightAligned"
valueFormatter={({
),
minWidth: 100,
},
{
headerName: t('Filled'),
field: 'remaining',
cellClass: 'font-mono text-right',
type: 'rightAligned',
valueGetter: ({ data }: VegaValueGetterParams<Order>) => {
return data?.size && data.market
? toBigNum(
(BigInt(data.size) - BigInt(data.remaining)).toString(),
data.market.positionDecimalPlaces ?? 0
).toNumber()
: undefined;
},
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<Order, 'remaining'>) => {
}: VegaValueFormatterParams<Order, 'remaining'>): string => {
if (!data) {
return undefined;
return '';
}
if (!data?.market || !isNumeric(value) || !isNumeric(data.size)) {
return '-';
}
const dps = data.market.positionDecimalPlaces;
const size = new BigNumber(data.size);
const remaining = new BigNumber(value);
const fills = size.minus(remaining);
const { positionDecimalPlaces } = data.market;
const filled = BigInt(data.size) - BigInt(data.remaining);
return `${addDecimalsFormatNumber(
fills.toString(),
dps
)}/${addDecimalsFormatNumber(size.toString(), dps)}`;
}}
minWidth={100}
/>
<AgGridColumn
field="price"
type="rightAligned"
cellClass="font-mono text-right"
valueFormatter={({
filled.toString(),
positionDecimalPlaces
)}/${addDecimalsFormatNumber(data.size, positionDecimalPlaces)}`;
},
minWidth: 100,
},
{
field: 'price',
type: 'rightAligned',
cellClass: 'font-mono text-right',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<Order, 'price'>) => {
if (!data) {
return undefined;
return '';
}
if (
!data?.market ||
@@ -204,16 +202,16 @@ export const OrderListTable = memo<
return '-';
}
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
}}
minWidth={100}
/>
<AgGridColumn
field="timeInForce"
filter={SetFilter}
filterParams={{
},
minWidth: 100,
},
{
field: 'timeInForce',
filter: SetFilter,
filterParams: {
set: Schema.OrderTimeInForceMapping,
}}
valueFormatter={({
},
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<Order, 'timeInForce'>) => {
@@ -235,13 +233,13 @@ export const OrderListTable = memo<
}${data?.reduceOnly ? t('. Reduce only') : ''}`;
return label;
}}
minWidth={150}
/>
<AgGridColumn
field="createdAt"
filter={DateRangeFilter}
cellRenderer={({
},
minWidth: 150,
},
{
field: 'createdAt',
filter: DateRangeFilter,
cellRenderer: ({
value,
}: VegaICellRendererParams<Order, 'createdAt'>) => {
return (
@@ -249,12 +247,12 @@ export const OrderListTable = memo<
{value ? getDateTimeFormat().format(new Date(value)) : value}
</span>
);
}}
minWidth={150}
/>
<AgGridColumn
field="updatedAt"
cellRenderer={({
},
minWidth: 150,
},
{
field: 'updatedAt',
cellRenderer: ({
data,
value,
}: VegaICellRendererParams<Order, 'updatedAt'>) => {
@@ -266,15 +264,15 @@ export const OrderListTable = memo<
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
</span>
);
}}
minWidth={150}
/>
<AgGridColumn
colId="amend"
{...COL_DEFS.actions}
minWidth={showAllActions ? 120 : COL_DEFS.actions.minWidth}
maxWidth={showAllActions ? 120 : COL_DEFS.actions.minWidth}
cellRenderer={({ data }: { data?: Order }) => {
},
minWidth: 150,
},
{
colId: 'amend',
...COL_DEFS.actions,
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
cellRenderer: ({ data }: { data?: Order }) => {
if (!data) return null;
return (
@@ -298,9 +296,37 @@ export const OrderListTable = memo<
<OrderActionsDropdown id={data?.id} />
</div>
);
}}
/>
</AgGrid>
},
},
],
[
filter,
onCancel,
onEdit,
onMarketClick,
onOrderTypeClick,
props.isReadOnly,
showAllActions,
]
);
return (
<AgGrid
ref={ref}
defaultColDef={{
resizable: true,
sortable: true,
filterParams: { buttons: ['reset'] },
}}
columnDefs={columnDefs}
style={{
width: '100%',
height: '100%',
}}
getRowId={({ data }) => data.id}
components={{ MarketNameCell, OrderTypeCell }}
{...props}
/>
);
}
)
-1
View File
@@ -1,7 +1,6 @@
export * from './lib/__generated__/Positions';
export * from './lib/positions-container';
export * from './lib/positions-data-providers';
export * from './lib/margin-data-provider';
export * from './lib/positions-table';
export * from './lib/use-market-margin';
export * from './lib/use-open-volume';

Some files were not shown because too many files have changed in this diff Show More