Compare commits

...
Author SHA1 Message Date
Matthew Russell edcba24399 fix: use lp id for row id and update func 2023-06-26 12:22:33 -07:00
ArtandBartłomiej Głownia 484d7888cf feat(web3): ready to withdraw toast (#4142)
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
2023-06-26 16:28:14 +02:00
Joe Tsang 187f1929fe chore(governance): increase epoch timeout (#4185) 2023-06-26 14:09:44 +00:00
Matthew Russell 0a4333645b chore(trading,governance,explorer): re-enable nx-cloud (#4182) 2023-06-26 14:47:22 +02:00
Bartłomiej Głownia 310506e5af feat(positions): live update market data in positions table (#4154) 2023-06-26 12:50:57 +02:00
Joe Tsang 0b33ed4299 test(governance): 3802 multisig error test (#4169) 2023-06-26 10:21:13 +01: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
122 changed files with 3032 additions and 1455 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: 10000 },
epochTimeout: { timeout: 12000 },
blockConfirmations: 3,
grepTags: '@regression @smoke @slow',
grepFilterSpecs: true,
@@ -0,0 +1,63 @@
export const previousEpochData = {
epoch: {
id: '7611',
validatorsConnection: {
edges: [
{
node: {
id: 'cd96782bc0ad5679869cf69fe7838a92212da7f53b4a214bed68067117494122',
stakedTotal: '3154229668720612941799',
rewardScore: {
rawValidatorScore: '0.2',
performanceScore: '1',
multisigScore: '0',
validatorScore: '0.2',
normalisedScore: '0.2007216887087119',
validatorStatus: 'VALIDATOR_NODE_STATUS_TENDERMINT',
__typename: 'RewardScore',
},
rankingScore: {
status: 'VALIDATOR_NODE_STATUS_TENDERMINT',
previousStatus: 'VALIDATOR_NODE_STATUS_TENDERMINT',
rankingScore: '0.211713765544955625',
stakeScore: '0.2016321576618625',
performanceScore: '1',
votingPower: '2007',
__typename: 'RankingScore',
},
__typename: 'Node',
},
__typename: 'NodeEdge',
},
{
node: {
id: '887d936f797a47032eceb572a13b69b581d3b1fa595a7d021a3e3cf2a5d2acfd',
stakedTotal: '3151161904761904764551',
rewardScore: {
rawValidatorScore: '0.2',
performanceScore: '1',
multisigScore: '1',
validatorScore: '0.2',
normalisedScore: '0.2007216887087119',
validatorStatus: 'VALIDATOR_NODE_STATUS_TENDERMINT',
__typename: 'RewardScore',
},
rankingScore: {
status: 'VALIDATOR_NODE_STATUS_TENDERMINT',
previousStatus: 'VALIDATOR_NODE_STATUS_TENDERMINT',
rankingScore: '0.211507855409133255',
stakeScore: '0.2014360527706031',
performanceScore: '1',
votingPower: '2007',
__typename: 'RankingScore',
},
__typename: 'Node',
},
__typename: 'NodeEdge',
},
],
__typename: 'NodesConnection',
},
__typename: 'Epoch',
},
};
@@ -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')
@@ -209,6 +212,7 @@ context(
'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');
@@ -230,6 +234,23 @@ context(
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');
});
}
);
@@ -269,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');
@@ -300,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
@@ -455,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');
});
@@ -502,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');
});
});
@@ -606,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';
@@ -58,8 +58,6 @@ context(
before('visit staking tab and connect vega wallet', function () {
cy.visit('/');
ethereumWalletConnect();
// this is a workaround for #2422 which can be removed once issue is resolved
cy.associateTokensToVegaWallet('4');
vegaWalletSetSpecifiedApprovalAmount('1000');
});
@@ -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"]';
@@ -301,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';
@@ -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');
@@ -1,5 +1,6 @@
/// <reference types="cypress" />
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import {
navigation,
verifyPageHeader,
@@ -9,6 +10,7 @@ import {
clickOnValidatorFromList,
waitForBeginningOfEpoch,
} from '../../support/staking.functions';
import { previousEpochData } from '../../fixtures/mocks/previous-epoch';
const guideLink = '[data-testid="staking-guide-link"]';
const validatorTitle = '[data-testid="validator-node-title"]';
@@ -30,7 +32,7 @@ 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 multisigPenaltyToolTip = '[data-testid="multisig-error-tooltip"]';
const epochCountDown = '[data-testid="epoch-countdown"]';
const stakeNumberRegex = /^\d{1,3}(,\d{3})*(\.\d+)?$/;
@@ -143,9 +145,6 @@ context('Validators Page - verify elements on page', function () {
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 () {
@@ -155,6 +154,22 @@ context('Validators Page - verify elements on page', function () {
cy.wrap($pendingStake).should('contain.text', '0.00');
});
});
it('Should be able to see multisig error', function () {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'PreviousEpoch', previousEpochData);
});
waitForBeginningOfEpoch();
cy.getByTestId('total-penalty').first().realHover();
cy.get(multisigPenaltyToolTip)
.invoke('text')
.should('contain', 'Multisig penalty: 100%');
cy.getByTestId('total-penalty').eq(1).realHover();
cy.get(multisigPenaltyToolTip)
.invoke('text')
.should('contain', 'Multisig penalty: 100%');
});
}
);
@@ -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';
@@ -285,28 +287,28 @@ context(
name: 'USDC (fake)',
symbol: 'fUSDC',
amount: '1000000',
expectedAmount: '10.00',
expectedAmount: 10.0,
},
{
id: '8566db7257222b5b7ef2886394ad28b938b28680a54a169bbc795027b89d6665',
name: 'DAI (fake)',
symbol: 'fDAI',
amount: '200000',
expectedAmount: '2.00',
expectedAmount: 2.0,
},
{
id: '73174a6fb1d5802ba0ac7bd7ab79e0a3a4837b262de0a4e80815a55442692bd0',
name: 'BTC (fake)',
symbol: 'fBTC',
amount: '600000',
expectedAmount: '6.00',
expectedAmount: 6.0,
},
{
id: 'e02d4c15d790d1d2dffaf2dcd1cf06a1fe656656cf4ed18c8ce99f9e83643567',
name: 'EURO (fake)',
symbol: 'fEURO',
amount: '800000',
expectedAmount: '8.00',
expectedAmount: 8.0,
},
];
@@ -317,12 +319,6 @@ context(
cy.reload();
waitForSpinner();
cy.connectVegaWallet();
cy.get(walletContainer).within(() => {
cy.getByTestId('currency-title', txTimeout).should(
'have.length.at.least',
5
);
});
});
for (const { name, symbol, expectedAmount } of assets) {
@@ -336,10 +332,10 @@ context(
.contains(name)
.parent()
.siblings()
.invoke('text')
.should('have.length.at.least', 4)
.then(parseFloat)
.should('be.gte', parseFloat(expectedAmount));
.then((elementAmount) => {
const displayedAmount = parseFloat(elementAmount.text());
expect(displayedAmount).be.gte(expectedAmount);
});
cy.get(vegaWalletCurrencyTitle)
.contains(name)
@@ -37,7 +37,7 @@ export function navigateTo(page: navigation) {
});
} else {
return cy.get(navigation.section, { timeout: 10000 }).within(() => {
cy.get(page).eq(0).click();
cy.get(page).eq(0).click({ force: true });
});
}
}
+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 />
)}
@@ -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()
@@ -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
@@ -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,6 +28,11 @@ 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
@@ -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);
});
@@ -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
@@ -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')}
@@ -23,6 +23,19 @@ import {
ResizableGrid,
ResizableGridPanel,
} from '../../components/resizable-grid';
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
const WithdrawalsIndicator = () => {
const { ready } = useIncompleteWithdrawals();
if (!ready || ready.length === 0) {
return null;
}
return (
<span className="bg-vega-blue-450 text-white text-[10px] rounded p-[3px] pb-[2px] leading-none">
{ready.length}
</span>
);
};
export const Portfolio = () => {
const { updateTitle } = usePageTitleStore((store) => ({
@@ -34,7 +47,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 (
@@ -103,7 +116,11 @@ export const Portfolio = () => {
<DepositsContainer />
</VegaWalletContainer>
</Tab>
<Tab id="withdrawals" name={t('Withdrawals')}>
<Tab
id="withdrawals"
name={t('Withdrawals')}
indicator={<WithdrawalsIndicator />}
>
<WithdrawalsContainer />
</Tab>
</Tabs>
@@ -3,6 +3,7 @@ import {
withdrawalProvider,
useWithdrawalDialog,
WithdrawalsTable,
useIncompleteWithdrawals,
} from '@vegaprotocol/withdraws';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/i18n';
@@ -17,6 +18,7 @@ export const WithdrawalsContainer = () => {
skip: !pubKey,
});
const openWithdrawDialog = useWithdrawalDialog((state) => state.open);
const { ready, delayed } = useIncompleteWithdrawals();
return (
<VegaWalletContainer>
@@ -25,6 +27,8 @@ export const WithdrawalsContainer = () => {
data-testid="withdrawals-history"
rowData={data}
overlayNoRowsTemplate={error ? error.message : t('No withdrawals')}
ready={ready}
delayed={delayed}
/>
</div>
{!isReadOnly && (
@@ -1,13 +1,16 @@
import type { DefaultWeb3ProviderContextShape } from '@vegaprotocol/web3';
import {
useEthereumConfig,
createConnectors,
Web3Provider as Web3ProviderInternal,
useWeb3ConnectStore,
createDefaultProvider,
} from '@vegaprotocol/web3';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useEnvironment } from '@vegaprotocol/environment';
import type { ReactNode } from 'react';
import { useState } from 'react';
import { useEffect } from 'react';
export const Web3Provider = ({ children }: { children: ReactNode }) => {
@@ -17,10 +20,13 @@ export const Web3Provider = ({ children }: { children: ReactNode }) => {
const connectors = useWeb3ConnectStore((store) => store.connectors);
const initializeConnectors = useWeb3ConnectStore((store) => store.initialize);
const [defaultProvider, setDefaultProvider] = useState<
DefaultWeb3ProviderContextShape['provider'] | undefined
>(undefined);
useEffect(() => {
if (config?.chain_id) {
return initializeConnectors(
initializeConnectors(
createConnectors(
ETHEREUM_PROVIDER_URL,
Number(config?.chain_id),
@@ -29,6 +35,11 @@ export const Web3Provider = ({ children }: { children: ReactNode }) => {
),
Number(config.chain_id)
);
const defaultProvider = createDefaultProvider(
ETHEREUM_PROVIDER_URL,
Number(config?.chain_id)
);
setDefaultProvider(defaultProvider);
}
}, [
config?.chain_id,
@@ -49,7 +60,10 @@ export const Web3Provider = ({ children }: { children: ReactNode }) => {
}}
noDataMessage={t('Could not fetch Ethereum configuration')}
>
<Web3ProviderInternal connectors={connectors}>
<Web3ProviderInternal
connectors={connectors}
defaultProvider={defaultProvider}
>
<>{children}</>
</Web3ProviderInternal>
</AsyncRenderer>
@@ -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');
}, []);
};
+5
View File
@@ -3,12 +3,17 @@ import { useUpdateNetworkParametersToasts } from '@vegaprotocol/proposals';
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
import { Routes } from './client-router';
import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
export const ToastsManager = () => {
useUpdateNetworkParametersToasts();
useVegaTransactionToasts();
useEthereumTransactionToasts();
useEthereumWithdrawApprovalsToasts();
useReadyToWithdrawalToasts({
withdrawalsLink: `${Routes.PORTFOLIO}`,
});
const toasts = useToasts((store) => store.toasts);
return <ToastsContainer order="desc" toasts={toasts} />;
@@ -72,7 +72,6 @@ export const AccountBreakdownDialog = memo(
onClose: () => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}) => {
console.log('render');
return (
<Dialog
size="medium"
+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,
},
},
};
};
+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();
@@ -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 }[];
+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>,
@@ -11,11 +11,7 @@ import type { EstimatePositionQuery } from '@vegaprotocol/positions';
import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder';
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import {
addDecimalsFormatNumber,
isNumeric,
addDecimalsFormatNumberQuantum,
} from '@vegaprotocol/utils';
import { formatRange, formatValue } from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
@@ -31,32 +27,6 @@ import {
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 interface DealTicketFeeDetailPros {
label: string;
value?: string | null | undefined;
@@ -143,6 +113,7 @@ export const DealTicketFeeDetails = ({
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) {
@@ -232,7 +203,6 @@ export const DealTicketFeeDetails = ({
}
let liquidationPriceEstimate = emptyValue;
let liquidationPriceEstimateFormatted;
if (liquidationEstimate) {
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
@@ -258,18 +228,10 @@ export const DealTicketFeeDetails = ({
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
);
liquidationPriceEstimateFormatted = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
@@ -279,7 +241,8 @@ export const DealTicketFeeDetails = ({
: liquidationEstimateWorstCase
).toString(),
assetDecimals,
quantum
undefined,
market.decimalPlaces
);
}
@@ -288,14 +251,16 @@ export const DealTicketFeeDetails = ({
[]
);
const quoteName = market.tradableInstrument.instrument.product.quoteName;
return (
<div>
<DealTicketFeeDetail
label={t('Notional')}
value={formatValue(notionalSize, assetDecimals)}
formattedValue={formatValue(notionalSize, assetDecimals, quantum)}
symbol={assetSymbol}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(assetSymbol)}
value={formatValue(notionalSize, marketDecimals)}
formattedValue={formatValue(notionalSize, marketDecimals)}
symbol={quoteName}
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
/>
<DealTicketFeeDetail
label={t('Fees')}
@@ -377,8 +342,8 @@ export const DealTicketFeeDetails = ({
<DealTicketFeeDetail
label={t('Liquidation price estimate')}
value={liquidationPriceEstimate}
formattedValue={liquidationPriceEstimateFormatted}
symbol={assetSymbol}
formattedValue={liquidationPriceEstimate}
symbol={quoteName}
labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}
/>
{partyId && (
@@ -195,7 +195,7 @@ export const DealTicket = ({
}
const hasNoBalance =
!generalAccountBalance || !BigInt(generalAccountBalance);
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
if (hasNoBalance) {
setError('summary', {
message: SummaryValidationType.NoCollateral,
@@ -219,6 +219,7 @@ export const DealTicket = ({
marketState,
marketTradingMode,
generalAccountBalance,
marginAccountBalance,
pubKey,
setError,
clearErrors,
-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' },
};
};
@@ -1,6 +1,7 @@
# Liquidity Provisions
fragment LiquidityProvisionFields on LiquidityProvision {
id
party {
id
accountsConnection(marketId: $marketId, type: ACCOUNT_TYPE_BOND) {
+3 -2
View File
@@ -3,14 +3,14 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type LiquidityProvisionFieldsFragment = { __typename?: 'LiquidityProvision', createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } };
export type LiquidityProvisionFieldsFragment = { __typename?: 'LiquidityProvision', id?: string | null, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } };
export type LiquidityProvisionsQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type LiquidityProvisionsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } } } | null> | null } | null } | null };
export type LiquidityProvisionsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', id?: string | null, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } } } | null> | null } | null } | null };
export type LiquidityProvisionsUpdateSubscriptionVariables = Types.Exact<{
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
@@ -31,6 +31,7 @@ export type LiquidityProviderFeeShareQuery = { __typename?: 'Query', market?: {
export const LiquidityProvisionFieldsFragmentDoc = gql`
fragment LiquidityProvisionFields on LiquidityProvision {
id
party {
id
accountsConnection(marketId: $marketId, type: ACCOUNT_TYPE_BOND) {
@@ -38,8 +38,7 @@ export const liquidityProvisionsDataProvider = makeDataProvider<
) => {
return produce(data || [], (draft) => {
deltas?.forEach((delta) => {
const id = delta.id;
const index = draft.findIndex((a) => delta.id === id);
const index = draft.findIndex((a) => delta.id === a.id);
if (index !== -1) {
draft[index].commitmentAmount = delta.commitmentAmount;
draft[index].fee = delta.fee;
@@ -47,6 +46,7 @@ export const liquidityProvisionsDataProvider = makeDataProvider<
draft[index].status = delta.status;
} else {
draft.unshift({
id: delta.id,
commitmentAmount: delta.commitmentAmount,
fee: delta.fee,
status: delta.status,
+155 -113
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,27 +37,158 @@ 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
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No liquidity provisions')}
getRowId={({ data }) => `${data.party.id}-${data.status}`}
getRowId={({ data }) => data.id}
ref={ref}
tooltipShowDelay={500}
defaultColDef={{
@@ -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}
/>
);
}
);
@@ -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();
+62 -3
View File
@@ -1,13 +1,50 @@
import { marketDataErrorPolicyGuard } from '@vegaprotocol/data-provider';
import { makeDataProvider } from '@vegaprotocol/data-provider';
import type { MarketsDataQuery } from './__generated__/markets-data';
import type {
MarketsDataQuery,
MarketsDataQueryVariables,
} from './__generated__/markets-data';
import type {
MarketDataUpdateSubscription,
MarketDataUpdateFieldsFragment,
MarketDataUpdateSubscriptionVariables,
} from './__generated__/market-data';
import { MarketDataUpdateDocument } from './__generated__/market-data';
import { MarketsDataDocument } from './__generated__/markets-data';
import type { MarketData } from './market-data-provider';
const getData = (responseData: MarketsDataQuery | null): MarketData[] | null =>
const getData = (responseData: MarketsDataQuery | null): MarketData[] =>
responseData?.marketsConnection?.edges
.filter((edge) => edge.node.data)
.map((edge) => edge.node.data as MarketData) || null;
.map((edge) => edge.node.data as MarketData) || [];
export const mapMarketDataUpdateToMarketData = (
delta: MarketDataUpdateFieldsFragment
): MarketData => {
const { marketId, __typename, ...marketData } = delta;
return { ...marketData, market: { id: marketId } };
};
const update = (
data: MarketData[] | null,
delta: MarketDataUpdateFieldsFragment
) => {
const updatedData = data ? [...data] : [];
const item = mapMarketDataUpdateToMarketData(delta);
const index = updatedData.findIndex(
(data) => data.market.id === item.market.id
);
if (index !== -1) {
updatedData[index] = { ...updatedData[index], ...item };
} else {
updatedData.push(item);
}
return updatedData;
};
const getDelta = (
subscriptionData: MarketDataUpdateSubscription
): MarketDataUpdateFieldsFragment => subscriptionData.marketsData[0];
export const marketsDataProvider = makeDataProvider<
MarketsDataQuery,
@@ -19,3 +56,25 @@ export const marketsDataProvider = makeDataProvider<
getData,
errorPolicyGuard: marketDataErrorPolicyGuard,
});
type Variables = { marketIds: string[] };
export const marketsLiveDataProvider = makeDataProvider<
MarketsDataQuery,
MarketData[],
MarketDataUpdateSubscription,
MarketDataUpdateFieldsFragment,
Variables,
MarketDataUpdateSubscriptionVariables,
MarketsDataQueryVariables
>({
query: MarketsDataDocument,
subscriptionQuery: MarketDataUpdateDocument,
getData,
getDelta,
update,
errorPolicyGuard: marketDataErrorPolicyGuard,
getQueryVariables: () => ({}),
getSubscriptionVariables: ({ marketIds }: Variables) =>
marketIds.map((marketId) => ({ marketId })),
});
+44 -1
View File
@@ -10,10 +10,15 @@ import type {
} from './__generated__/markets';
import type { MarketsCandlesQueryVariables } from './__generated__/markets-candles';
import { marketsDataProvider } from './markets-data-provider';
import {
marketsDataProvider,
marketsLiveDataProvider,
mapMarketDataUpdateToMarketData,
} from './markets-data-provider';
import { marketDataProvider } from './market-data-provider';
import { marketsCandlesProvider } from './markets-candles-provider';
import type { MarketData } from './market-data-provider';
import type { MarketDataUpdateFieldsFragment } from './__generated__';
import type { MarketCandles } from './markets-candles-provider';
import { useMemo } from 'react';
import * as Schema from '@vegaprotocol/types';
@@ -160,6 +165,44 @@ export const allMarketsWithDataProvider = makeDerivedDataProvider<
addData(parts[0] as Market[], parts[1] as MarketData[])
);
export const allMarketsWithLiveDataProvider = makeDerivedDataProvider<
MarketMaybeWithData[],
MarketMaybeWithData,
{ marketIds: string[] }
>(
[
(callback, client, variables) =>
marketsProvider(callback, client, undefined),
marketsLiveDataProvider,
],
(partsData, variables, prevData, parts) => {
if (prevData && parts[1].isUpdate) {
const data = mapMarketDataUpdateToMarketData(parts[1].delta);
const index = prevData.findIndex(
(market) => market.id === data.market.id
);
if (index !== -1) {
const updatedData = [...prevData];
updatedData[index] = { ...updatedData[index], data };
return updatedData;
} else {
return prevData;
}
}
return addData(partsData[0] as Market[], partsData[1] as MarketData[]);
},
(data, parts) => {
if (!parts[1].isUpdate && parts[1].delta) {
return;
}
return data.find(
(market) =>
market.id ===
(parts[1].delta as MarketDataUpdateFieldsFragment).marketId
);
}
);
export type MarketMaybeWithDataAndCandles = MarketMaybeWithData &
MarketMaybeWithCandles;
@@ -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
@@ -4,4 +4,3 @@ export * from './lib/positions-data-providers';
export * from './lib/positions-table';
export * from './lib/use-market-margin';
export * from './lib/use-open-volume';
export * from './lib/use-positions-data';
@@ -0,0 +1,60 @@
import { useEstimatePositionQuery } from './__generated__/Positions';
import { formatRange } from '@vegaprotocol/utils';
export const LiquidationPrice = ({
marketId,
openVolume,
collateralAvailable,
decimalPlaces,
formatDecimals,
}: {
marketId: string;
openVolume: string;
collateralAvailable: string;
decimalPlaces: number;
formatDecimals: number;
}) => {
const { data: currentData, previousData } = useEstimatePositionQuery({
variables: {
marketId,
openVolume,
collateralAvailable,
},
fetchPolicy: 'no-cache',
skip: !openVolume || openVolume === '0',
});
const data = currentData || previousData;
let value = '-';
if (data) {
const bestCase =
data.estimatePosition?.liquidation?.bestCase.open_volume_only.replace(
/\..*/,
''
);
const worstCase =
data.estimatePosition?.liquidation?.worstCase.open_volume_only.replace(
/\..*/,
''
);
value =
bestCase && worstCase && BigInt(bestCase) < BigInt(worstCase)
? formatRange(
bestCase,
worstCase,
decimalPlaces,
undefined,
formatDecimals,
value
)
: formatRange(
worstCase,
bestCase,
decimalPlaces,
undefined,
formatDecimals,
value
);
}
return <span data-testid="liquidation-price">{value}</span>;
};
@@ -14,7 +14,7 @@ import type {
MarketMaybeWithData,
MarketDataQueryVariables,
} from '@vegaprotocol/markets';
import { allMarketsWithDataProvider } from '@vegaprotocol/markets';
import { allMarketsWithLiveDataProvider } from '@vegaprotocol/markets';
import type {
PositionsQuery,
PositionFieldsFragment,
@@ -34,6 +34,7 @@ export interface Position {
averageEntryPrice: string;
currentLeverage: number | undefined;
decimals: number;
quantum: string;
lossSocializationAmount: string;
marginAccountBalance: string;
marketDecimalPlaces: number;
@@ -73,6 +74,7 @@ export const getMetrics = (
decimals,
id: assetId,
symbol: assetSymbol,
quantum,
} = market.tradableInstrument.instrument.product.settlementAsset;
const generalAccount = accounts?.find(
(account) =>
@@ -114,6 +116,7 @@ export const getMetrics = (
averageEntryPrice: position.averageEntryPrice,
currentLeverage: currentLeverage ? currentLeverage.toNumber() : undefined,
decimals,
quantum,
lossSocializationAmount: position.lossSocializationAmount || '0',
marginAccountBalance: marginAccount?.balance ?? '0',
marketDecimalPlaces,
@@ -141,8 +144,9 @@ export const update = (
data: PositionFieldsFragment[] | null,
deltas: PositionsSubscriptionSubscription['positions']
) => {
return produce(data || [], (draft) => {
const updatedData = produce(data || [], (draft) => {
deltas.forEach((delta) => {
const { marketId, partyId, __typename, ...position } = delta;
const index = draft.findIndex(
(node) =>
node.market.id === delta.marketId && node.party.id === delta.partyId
@@ -151,29 +155,25 @@ export const update = (
const currNode = draft[index];
draft[index] = {
...currNode,
realisedPNL: delta.realisedPNL,
unrealisedPNL: delta.unrealisedPNL,
openVolume: delta.openVolume,
averageEntryPrice: delta.averageEntryPrice,
updatedAt: delta.updatedAt,
lossSocializationAmount: delta.lossSocializationAmount,
positionStatus: delta.positionStatus,
...position,
};
} else {
draft.unshift({
...delta,
...position,
__typename: 'Position',
market: {
__typename: 'Market',
id: delta.marketId,
},
party: {
__typename: 'Party',
id: delta.partyId,
},
});
}
});
});
return updatedData;
};
const getSubscriptionVariables = (
@@ -239,41 +239,54 @@ export const rejoinPositionData = (
| null => {
if (positions && marketsData) {
return positions.map((node) => {
const market =
marketsData?.find((market) => market.id === node.market.id) || null;
return {
...node,
market:
marketsData?.find((market) => market.id === node.market.id) || null,
market,
};
});
}
return null;
};
export const positionsMarketsProvider = makeDerivedDataProvider<
string[],
never,
PositionsQueryVariables
>([positionsDataProvider], ([positions]) => {
return Array.from(
new Set(
(positions as PositionFieldsFragment[]).map(
(position) => position.market.id
)
)
).sort();
});
export const positionsMetricsProvider = makeDerivedDataProvider<
Position[],
Position[],
PositionsQueryVariables
PositionsQueryVariables & { marketIds: string[] }
>(
[
positionsDataProvider,
(callback, client, variables) =>
positionsDataProvider(callback, client, { partyIds: variables.partyIds }),
(callback, client, variables) =>
accountsDataProvider(callback, client, {
partyId: Array.isArray(variables.partyIds)
? variables.partyIds[0]
: variables.partyIds,
}),
(callback, client) =>
allMarketsWithDataProvider(callback, client, undefined),
(callback, client, variables) =>
allMarketsWithLiveDataProvider(callback, client, {
marketIds: variables.marketIds,
}),
],
([positions, accounts, marketsData], variables) => {
([positions, accounts, marketsData]) => {
const positionsData = rejoinPositionData(positions, marketsData);
if (!variables) {
return [];
}
return sortBy(
getMetrics(positionsData, accounts as Account[] | null),
'marketName'
);
const metrics = getMetrics(positionsData, accounts as Account[] | null);
return sortBy(metrics, 'marketName');
},
(data, delta, previousData) =>
data.filter((row) => {
+41 -40
View File
@@ -1,11 +1,13 @@
import { useRef } from 'react';
import { usePositionsData } from './use-positions-data';
import { useCallback } from 'react';
import { PositionsTable } from './positions-table';
import type { AgGridReact } from 'ag-grid-react';
import * as Schema from '@vegaprotocol/types';
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/i18n';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
positionsMetricsProvider,
positionsMarketsProvider,
} from './positions-data-providers';
import { useVegaWallet } from '@vegaprotocol/wallet';
interface PositionsManagerProps {
@@ -24,42 +26,43 @@ export const PositionsManager = ({
storeKey,
}: PositionsManagerProps) => {
const { pubKeys, pubKey } = useVegaWallet();
const gridRef = useRef<AgGridReact | null>(null);
const { data, error } = usePositionsData(partyIds, gridRef);
const create = useVegaTransactionStore((store) => store.create);
const onClose = ({
marketId,
openVolume,
}: {
marketId: string;
openVolume: string;
}) =>
create({
batchMarketInstructions: {
cancellations: [
{
marketId,
orderId: '', // omit order id to cancel all active orders
},
],
submissions: [
{
marketId: marketId,
type: Schema.OrderType.TYPE_MARKET as const,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC as const,
side: openVolume.startsWith('-')
? Schema.Side.SIDE_BUY
: Schema.Side.SIDE_SELL,
size: openVolume.replace('-', ''),
reduceOnly: true,
},
],
},
});
const onClose = useCallback(
({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
create({
batchMarketInstructions: {
cancellations: [
{
marketId,
orderId: '', // omit order id to cancel all active orders
},
],
submissions: [
{
marketId: marketId,
type: Schema.OrderType.TYPE_MARKET as const,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC as const,
side: openVolume.startsWith('-')
? Schema.Side.SIDE_BUY
: Schema.Side.SIDE_SELL,
size: openVolume.replace('-', ''),
reduceOnly: true,
},
],
},
}),
[create]
);
const bottomPlaceholderProps = useBottomPlaceholder({
gridRef,
disabled: noBottomPlaceholder,
const { data: marketIds } = useDataProvider({
dataProvider: positionsMarketsProvider,
variables: { partyIds },
});
const { data, error } = useDataProvider({
dataProvider: positionsMetricsProvider,
variables: { partyIds, marketIds: marketIds || [] },
skip: !marketIds,
});
return (
@@ -68,11 +71,9 @@ export const PositionsManager = ({
pubKey={pubKey}
pubKeys={pubKeys}
rowData={error ? [] : data}
ref={gridRef}
onMarketClick={onMarketClick}
onClose={onClose}
isReadOnly={isReadOnly}
{...bottomPlaceholderProps}
storeKey={storeKey}
multipleKeys={partyIds.length > 1}
overlayNoRowsTemplate={error ? error.message : t('No positions')}
@@ -7,6 +7,12 @@ import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community';
jest.mock('./liquidation-price', () => ({
LiquidationPrice: () => (
<span data-testid="liquidation-price">liquidation price</span>
),
}));
const singleRow: Position = {
partyId: 'partyId',
assetId: 'asset-id',
@@ -14,6 +20,7 @@ const singleRow: Position = {
averageEntryPrice: '133',
currentLeverage: 1.1,
decimals: 2,
quantum: '0.1',
lossSocializationAmount: '0',
marginAccountBalance: '12345600',
marketDecimalPlaces: 1,
@@ -48,7 +55,7 @@ it('render correct columns', async () => {
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(11);
expect(headers).toHaveLength(12);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([
@@ -56,6 +63,7 @@ it('render correct columns', async () => {
'Notional',
'Open volume',
'Mark price',
'Liquidation price',
'Settlement asset',
'Entry price',
'Leverage',
@@ -143,12 +151,20 @@ it('displays mark price', async () => {
expect(cells[3].textContent).toEqual('-');
});
it('displays liquidation price', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[4].textContent).toEqual('liquidation price');
});
it('displays leverage', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[6].textContent).toEqual('1.1');
expect(cells[7].textContent).toEqual('1.1');
});
it('displays allocated margin', async () => {
@@ -156,7 +172,7 @@ it('displays allocated margin', async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[7];
const cell = cells[8];
expect(cell.textContent).toEqual('123,456.00');
});
@@ -165,7 +181,7 @@ it('displays realised and unrealised PNL', async () => {
render(<PositionsTable rowData={singleRowData} isReadOnly={false} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[9].textContent).toEqual('4.56');
expect(cells[10].textContent).toEqual('4.56');
});
it('displays close button', async () => {
@@ -182,7 +198,7 @@ it('displays close button', async () => {
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[11].textContent).toEqual('Close');
expect(cells[12].textContent).toEqual('Close');
});
it('do not display close button if openVolume is zero', async () => {
@@ -198,7 +214,7 @@ it('do not display close button if openVolume is zero', async () => {
);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[11].textContent).toEqual('');
expect(cells[12].textContent).toEqual('');
});
describe('PNLCell', () => {
+339 -326
View File
@@ -1,7 +1,7 @@
import classNames from 'classnames';
import { forwardRef } from 'react';
import { forwardRef, useMemo } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import type { CellRendererSelectorResult } from 'ag-grid-community';
import type { ColDef } from 'ag-grid-community';
import type {
VegaValueFormatterParams,
VegaValueGetterParams,
@@ -33,16 +33,15 @@ import {
addDecimalsFormatNumber,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
import { getRowId } from './use-positions-data';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import { DocsLinks } from '@vegaprotocol/environment';
import { PositionTableActions } from './position-actions-dropdown';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { LiquidationPrice } from './liquidation-price';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
@@ -86,6 +85,9 @@ export const AmountCell = ({ valueFormatted }: AmountCellProps) => {
AmountCell.displayName = 'AmountCell';
export const getRowId = ({ data }: { data: Position }) =>
`${data.partyId}-${data.marketId}`;
export const PositionsTable = forwardRef<AgGridReact, Props>(
(
{
@@ -121,333 +123,344 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
MarketNameCell,
}}
{...props}
>
{multipleKeys ? (
<AgGridColumn
headerName={t('Vega key')}
field="partyId"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'partyId'>) =>
(data?.partyId &&
pubKeys &&
pubKeys.find((key) => key.publicKey === data.partyId)?.name) ||
data?.partyId
}
minWidth={190}
/>
) : null}
<AgGridColumn
headerName={t('Market')}
field="marketName"
cellRenderer="MarketNameCell"
cellRendererParams={{ idPath: 'marketId', onMarketClick }}
minWidth={190}
/>
<AgGridColumn
headerName={t('Notional')}
headerTooltip={t('Mark price x open volume.')}
field="notional"
type="rightAligned"
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'notional'>) => {
return !data?.notional
? undefined
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
columnDefs={useMemo<ColDef[]>(() => {
const columnDefs: (ColDef | null)[] = [
multipleKeys
? {
headerName: t('Vega key'),
field: 'partyId',
valueGetter: ({ data }: VegaValueGetterParams<Position>) =>
(data?.partyId &&
pubKeys &&
pubKeys.find((key) => key.publicKey === data.partyId)
?.name) ||
data?.partyId,
minWidth: 190,
}
: null,
{
headerName: t('Market'),
field: 'marketName',
cellRenderer: 'MarketNameCell',
cellRendererParams: { idPath: 'marketId', onMarketClick },
minWidth: 190,
},
{
headerName: t('Notional'),
headerTooltip: t('Mark price x open volume.'),
field: 'notional',
type: 'rightAligned',
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data?.notional
? undefined
: toBigNum(
data.notional,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
},
minWidth: 80,
},
{
headerName: t('Open volume'),
field: 'openVolume',
type: 'rightAligned',
cellClass: 'font-mono text-right',
cellClassRules: signedNumberCssClassRules,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
data?.openVolume,
data.positionDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>): string => {
return data?.openVolume === undefined
? ''
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
);
},
cellRenderer: OpenVolumeCell,
minWidth: 100,
},
{
headerName: t('Mark price'),
field: 'markPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(
data.markPrice,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return '';
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
);
}}
minWidth={80}
/>
<AgGridColumn
headerName={t('Open volume')}
field="openVolume"
type="rightAligned"
cellClass="font-mono text-right"
cellClassRules={signedNumberCssClassRules}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'openVolume'>) => {
return data?.openVolume === undefined
? undefined
: toBigNum(
data?.openVolume,
data.positionDecimalPlaces
).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>):
| string
| undefined => {
return data?.openVolume === undefined
? undefined
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces
)
},
minWidth: 100,
},
{
headerName: t('Liquidation price'),
colId: 'liquidationPrice',
type: 'rightAligned',
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<LiquidationPrice
marketId={data.marketId}
openVolume={data.openVolume}
collateralAvailable={data.totalBalance}
decimalPlaces={data.decimals}
formatDecimals={data.marketDecimalPlaces}
/>
);
}}
cellRenderer={OpenVolumeCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Mark price')}
field="markPrice"
type="rightAligned"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'markPrice'>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return undefined;
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
);
}}
minWidth={100}
/>
<AgGridColumn
headerName={t('Settlement asset')}
field="assetSymbol"
colId="asset"
minWidth={100}
cellRenderer={({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(data.assetId, e.target as HTMLElement);
}}
>
{data?.assetSymbol}
</ButtonLink>
);
}}
/>
<AgGridColumn
headerName={t('Entry price')}
field="averageEntryPrice"
type="rightAligned"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'averageEntryPrice'>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
},
},
{
headerName: t('Settlement asset'),
field: 'assetSymbol',
colId: 'asset',
minWidth: 100,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(
data.assetId,
e.target as HTMLElement
);
}}
>
{data?.assetSymbol}
</ButtonLink>
);
},
},
{
headerName: t('Entry price'),
field: 'averageEntryPrice',
type: 'rightAligned',
cellRenderer: PriceFlashCell,
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return data?.markPrice === undefined || !data
? undefined
: toBigNum(
data.averageEntryPrice,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'averageEntryPrice'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'averageEntryPrice'>):
| string
| undefined => {
if (!data) {
return undefined;
}
return addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
);
}}
minWidth={100}
/>
{multipleKeys ? null : (
<AgGridColumn
headerName={t('Leverage')}
field="currentLeverage"
type="rightAligned"
filter="agNumberColumnFilter"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
valueFormatter={({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined
? undefined
: formatNumber(value.toString(), 1)
}
minWidth={100}
/>
)}
{multipleKeys ? null : (
<AgGridColumn
headerName={t('Margin allocated')}
field="marginAccountBalance"
type="rightAligned"
filter="agNumberColumnFilter"
cellRendererSelector={(): CellRendererSelectorResult => {
return {
component: PriceFlashCell,
};
}}
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'marginAccountBalance'>) => {
return !data
? undefined
: toBigNum(data.marginAccountBalance, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
| string
| undefined => {
if (!data) {
return undefined;
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
}}
minWidth={100}
/>
)}
<AgGridColumn
headerName={t('Realised PNL')}
field="realisedPNL"
type="rightAligned"
cellClassRules={signedNumberCssClassRules}
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'realisedPNL'>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? undefined
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
}}
headerTooltip={t(
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
)}
cellRenderer={PNLCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Unrealised PNL')}
field="unrealisedPNL"
type="rightAligned"
cellClassRules={signedNumberCssClassRules}
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
}: VegaValueGetterParams<Position, 'unrealisedPNL'>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
}}
valueFormatter={({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? undefined
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals)
}
headerTooltip={t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
)}
cellRenderer={PNLCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Updated')}
field="updatedAt"
type="rightAligned"
filter={DateRangeFilter}
valueFormatter={({
value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) {
return value;
}
return getDateTimeFormat().format(new Date(value));
}}
minWidth={150}
/>
{onClose && !isReadOnly ? (
<AgGridColumn
{...COL_DEFS.actions}
cellRenderer={({ data }: VegaICellRendererParams<Position>) => {
return (
<div className="flex gap-2 items-center justify-end">
{data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
) : null}
{data?.assetId && (
<PositionTableActions assetId={data?.assetId} />
)}
</div>
);
}}
minWidth={90}
maxWidth={90}
/>
) : null}
</AgGrid>
);
},
minWidth: 100,
},
multipleKeys
? null
: {
headerName: t('Leverage'),
field: 'currentLeverage',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined
? ''
: formatNumber(value.toString(), 1),
minWidth: 100,
},
multipleKeys
? null
: {
headerName: t('Margin allocated'),
field: 'marginAccountBalance',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(
data.marginAccountBalance,
data.decimals
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'marginAccountBalance'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
},
minWidth: 100,
},
{
headerName: t('Realised PNL'),
field: 'realisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
return !data
? ''
: addDecimalsFormatNumber(data.realisedPNL, data.decimals);
},
headerTooltip: t(
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
),
cellRenderer: PNLCell,
minWidth: 100,
},
{
headerName: t('Unrealised PNL'),
field: 'unrealisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? ''
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
headerTooltip: t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
),
cellRenderer: PNLCell,
minWidth: 100,
},
{
headerName: t('Updated'),
field: 'updatedAt',
type: 'rightAligned',
filter: DateRangeFilter,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) {
return '';
}
return getDateTimeFormat().format(new Date(value));
},
minWidth: 150,
},
onClose && !isReadOnly
? {
...COL_DEFS.actions,
cellRenderer: ({
data,
}: VegaICellRendererParams<Position>) => {
return (
<div className="flex gap-2 items-center justify-end">
{data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
>
{t('Close')}
</ButtonLink>
) : null}
{data?.assetId && (
<PositionTableActions assetId={data?.assetId} />
)}
</div>
);
},
minWidth: 90,
maxWidth: 90,
}
: null,
];
return columnDefs.filter<ColDef>(
(colDef: ColDef | null): colDef is ColDef => colDef !== null
);
}, [
isReadOnly,
multipleKeys,
onClose,
onMarketClick,
openAssetDetailsDialog,
pubKey,
pubKeys,
])}
/>
);
}
);
@@ -1,107 +0,0 @@
import type { AgGridReact } from 'ag-grid-react';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook, waitFor } from '@testing-library/react';
import { usePositionsData } from './use-positions-data';
import type { Position } from './positions-data-providers';
let mockData: Position[] = [
{
marketName: 'M1',
marketId: 'market-0',
openVolume: '1',
},
{
marketName: 'M2',
marketId: 'market-1',
openVolume: '-1985',
},
{
marketName: 'M3',
marketId: 'market-2',
openVolume: '0',
},
{
marketName: 'M4',
marketId: 'market-3',
openVolume: '0',
},
{
marketName: 'M5',
marketId: 'market-4',
openVolume: '3',
},
] as Position[];
let mockDataProviderData = {
data: mockData,
error: undefined,
loading: false,
totalCount: undefined,
};
let updateMock: jest.Mock;
const mockDataProvider = jest.fn((args) => {
updateMock = args.update;
return mockDataProviderData;
});
jest.mock('@vegaprotocol/data-provider', () => ({
...jest.requireActual('@vegaprotocol/data-provider'),
useDataProvider: jest.fn((args) => mockDataProvider(args)),
}));
describe('usePositionData Hook', () => {
afterEach(() => {
jest.clearAllMocks();
});
const mockRefreshInfiniteCache = jest.fn();
const mockGetRowNode = jest
.fn()
.mockImplementation((id: string) =>
mockData.find((position) => position.marketId === id)
);
const partyIds = ['partyId'];
const anUpdatedOne = {
marketId: 'market-1',
openVolume: '1',
};
const gridRef = {
current: {
api: {
refreshInfiniteCache: mockRefreshInfiniteCache,
getRowNode: mockGetRowNode,
getModel: () => ({ getType: () => 'infinite' }),
},
} as unknown as AgGridReact,
};
it('should return proper data', async () => {
const { result } = renderHook(() => usePositionsData(partyIds, gridRef), {
wrapper: MockedProvider,
});
expect(result.current.data?.length ?? 0).toEqual(5);
});
it('should call mockRefreshInfiniteCache', async () => {
renderHook(() => usePositionsData(partyIds, gridRef), {
wrapper: MockedProvider,
});
await waitFor(() => {
updateMock({ delta: [anUpdatedOne] as Position[] });
});
expect(mockRefreshInfiniteCache).toHaveBeenCalledWith();
});
it('no data should return null', () => {
mockData = [];
mockDataProviderData = {
...mockDataProviderData,
data: mockData,
loading: false,
};
const { result } = renderHook(() => usePositionsData(partyIds, gridRef), {
wrapper: MockedProvider,
});
expect(result.current.data).toEqual([]);
});
});
@@ -1,81 +0,0 @@
import { useCallback, useMemo, useRef } from 'react';
import type { RefObject } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from './positions-data-providers';
import { positionsMetricsProvider } from './positions-data-providers';
import type { PositionsQueryVariables } from './__generated__/Positions';
import { updateGridData } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { GetRowsParams } from '@vegaprotocol/datagrid';
import isEqual from 'lodash/isEqual';
export const getRowId = ({
data,
}: {
data: Position & { isLastPlaceholder?: boolean; id?: string };
}) =>
data.isLastPlaceholder && data.id
? data.id
: `${data.partyId}-${data.marketId}`;
export const usePositionsData = (
partyIds: string[],
gridRef: RefObject<AgGridReact>
) => {
const variables = useMemo<PositionsQueryVariables>(
() => ({ partyIds }),
[partyIds]
);
const dataRef = useRef<Position[] | null>(null);
const update = useCallback(
({ data }: { data: Position[] | null }) => {
if (gridRef.current?.api?.getModel().getType() === 'infinite') {
return updateGridData(dataRef, data, gridRef);
}
const update: Position[] = [];
const add: Position[] = [];
data?.forEach((row) => {
const rowNode = gridRef.current?.api?.getRowNode(
getRowId({ data: row })
);
if (rowNode) {
if (!isEqual(rowNode.data, row)) {
update.push(row);
}
} else {
add.push(row);
}
});
gridRef.current?.api?.applyTransaction({
update,
add,
addIndex: 0,
});
return true;
},
[gridRef]
);
const { data, error, loading, reload } = useDataProvider({
dataProvider: positionsMetricsProvider,
update,
variables,
});
const getRows = useCallback(
async ({ successCallback, startRow, endRow }: GetRowsParams<Position>) => {
const rowsThisBlock = dataRef.current
? dataRef.current.slice(startRow, endRow)
: [];
const lastRow = dataRef.current ? dataRef.current.length : 0;
successCallback(rowsThisBlock, lastRow);
},
[]
);
return {
data,
error,
loading,
getRows,
reload,
};
};
@@ -1 +1,2 @@
export * from './use-vote-submit';
export * from './__generated__/VoteSubsciption';
@@ -5,15 +5,16 @@ import { useVoteEvent } from './use-vote-event';
import type { VoteValue } from '@vegaprotocol/types';
import type { VoteEventFieldsFragment } from './__generated__/VoteSubsciption';
export type FinalizedVote = VoteEventFieldsFragment;
export type FinalizedVote = VoteEventFieldsFragment & { pubKey: string };
export const useVoteSubmit = () => {
const { pubKey } = useVegaWallet();
const { send, transaction, setComplete, Dialog } = useVegaTransaction();
const waitForVoteEvent = useVoteEvent(transaction);
const [finalizedVote, setFinalizedVote] =
useState<VoteEventFieldsFragment | null>(null);
const [finalizedVote, setFinalizedVote] = useState<FinalizedVote | null>(
null
);
const submit = useCallback(
async (voteValue: VoteValue, proposalId: string | null) => {
@@ -33,7 +34,7 @@ export const useVoteSubmit = () => {
if (res) {
waitForVoteEvent(proposalId, pubKey, (v) => {
setFinalizedVote(v);
setFinalizedVote({ ...v, pubKey });
setComplete();
});
}
+8 -8
View File
@@ -53,10 +53,10 @@ export const tradesUpdateSubscription = (
const trades: TradeFieldsFragment[] = [
{
id: 'FFFFBC80005C517A10ACF481F7E6893769471098E696D0CC407F18134044CB16',
price: '17116898',
size: '24',
createdAt: '2022-04-06T16:19:42.692598951Z',
id: 'FFFFAD1BF47AA2853E5C375B6B3A62375F62D5B10807583D32EF3119CC455CD1',
price: '17106734',
size: '18',
createdAt: '2022-04-07T17:56:47.997938583Z',
aggressor: Side.SIDE_BUY,
market: {
id: 'market-0',
@@ -77,10 +77,10 @@ const trades: TradeFieldsFragment[] = [
__typename: 'Trade',
},
{
id: 'FFFFAD1BF47AA2853E5C375B6B3A62375F62D5B10807583D32EF3119CC455CD1',
price: '17106734',
size: '18',
createdAt: '2022-04-07T17:56:47.997938583Z',
id: 'FFFFBC80005C517A10ACF481F7E6893769471098E696D0CC407F18134044CB16',
price: '17116898',
size: '24',
createdAt: '2022-04-06T16:19:42.692598951Z',
aggressor: Side.SIDE_BUY,
market: {
id: 'market-0',
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "@vegaprotocol/ui-toolkit",
"version": "0.12.3"
"version": "0.12.5"
}
@@ -1,6 +1,6 @@
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import type { ComponentProps, ReactNode } from 'react';
import { forwardRef } from 'react';
import { VegaIcon, VegaIconNames } from '../icon';
import { Icon } from '../icon';
@@ -164,10 +164,9 @@ export const DropdownMenuSeparator = forwardRef<
/**
* Container element for submenus
*/
export const DropdownMenuSub = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Sub>,
React.ComponentProps<typeof DropdownMenuPrimitive.Sub>
>(({ ...subProps }) => <DropdownMenuPrimitive.Sub {...subProps} />);
export const DropdownMenuSub = (
subProps: ComponentProps<typeof DropdownMenuPrimitive.Sub>
) => <DropdownMenuPrimitive.Sub {...subProps} />;
/**
* Container within a DropdownMenuSub specifically for the content
@@ -201,10 +200,9 @@ export const DropdownMenuSubTrigger = forwardRef<
* Portal to ensure menu portions are rendered outwith where they appear in the
* DOM.
*/
export const DropdownMenuPortal = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Portal>,
React.ComponentProps<typeof DropdownMenuPrimitive.Portal>
>(({ ...portalProps }) => <DropdownMenuPrimitive.Portal {...portalProps} />);
export const DropdownMenuPortal = (
portalProps: ComponentProps<typeof DropdownMenuPrimitive.Portal>
) => <DropdownMenuPrimitive.Portal {...portalProps} />;
/**
* Wraps a regular DropdownMenuItem with copy to clip board functionality
@@ -1,5 +1,8 @@
import classNames from 'classnames';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import {
addDecimalsFormatNumber,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { BigNumber } from 'bignumber.js';
import { getIntentBackground, Intent } from '../../utils/intent';
@@ -74,13 +77,17 @@ const Level = ({
.multipliedBy(100)
.toNumber();
const formattedFee = fee
? formatNumberPercentage(new BigNumber(fee).times(100), 2)
: '-';
const tooltipContent = (
<>
<div className="text-vega-dark-100 dark:text-vega-light-200">
<div className="mt-1.5 inline-flex">
<Indicator variant={intent} />
</div>
<span>
{fee}% {t('Fee')}
{formattedFee} {t('Fee')}
</span>
<div className="flex flex-col">
<span>
@@ -88,7 +95,7 @@ const Level = ({
{addDecimalsFormatNumber(commitmentAmount, decimals)}
</span>
</div>
</>
</div>
);
return (
@@ -179,7 +186,7 @@ export const HealthBar = ({
opacity={opacity}
fee={fee}
prevLevel={prevLevel}
decimals={0}
decimals={decimals}
intent={intent}
key={'healthbar-segment-' + index}
/>
+4 -1
View File
@@ -45,7 +45,8 @@ export const Tabs = ({
'cursor-default': isActive,
'text-neutral-400 hover:text-neutral-500 dark:hover:text-neutral-300':
!isActive,
}
},
'flex items-center gap-2'
);
const borderClass = classNames(
'absolute bottom-[-1px] left-0 w-full h-0 border-b',
@@ -58,6 +59,7 @@ export const Tabs = ({
value={child.props.id}
className={triggerClass}
>
{child.props.indicator}
{child.props.name}
<span className={borderClass} />
</TabsPrimitive.Trigger>
@@ -87,6 +89,7 @@ interface TabProps {
children: ReactNode;
id: string;
name: string;
indicator?: ReactNode;
hidden?: boolean;
}
@@ -18,6 +18,8 @@ export type ToastContent = JSX.Element | undefined;
type ToastState = 'initial' | 'showing' | 'expired';
type WithdrawalInfoMeta = { withdrawalId: string | undefined };
export type Toast = {
id: string;
intent: Intent;
@@ -26,6 +28,9 @@ export type Toast = {
onClose?: () => void;
signal?: 'close';
loader?: boolean;
hidden?: boolean;
// meta information
meta?: WithdrawalInfoMeta | undefined;
};
type ToastProps = Toast & {
@@ -13,11 +13,13 @@ import { Portal } from '@radix-ui/react-portal';
type ToastsContainerProps = {
toasts: Toasts;
order: 'asc' | 'desc';
showHidden?: boolean;
};
export const ToastsContainer = ({
toasts,
order = 'asc',
showHidden = false,
}: ToastsContainerProps) => {
const ref = useRef<HTMLDivElement>();
const closeAll = useToasts((store) => store.closeAll);
@@ -41,6 +43,10 @@ export const ToastsContainer = ({
};
}, [count, order, toasts]);
const validToasts = Object.values(toasts).filter(
(t) => !t.hidden || showHidden
);
return (
<Portal
ref={ref as Ref<HTMLDivElement>}
@@ -71,14 +77,16 @@ export const ToastsContainer = ({
'flex-col-reverse': order === 'desc',
})}
>
{toasts &&
Object.values(toasts).map((toast) => {
return (
<li key={toast.id}>
<Toast {...toast} />
</li>
);
})}
{validToasts.length > 0 &&
validToasts
.filter((t) => !t.hidden || showHidden)
.map((toast) => {
return (
<li key={toast.id}>
<Toast {...toast} />
</li>
);
})}
<Button
title={t('Dismiss all toasts')}
size="sm"
@@ -89,7 +97,7 @@ export const ToastsContainer = ({
'opacity-0 group-hover:opacity-50 hover:!opacity-100',
'text-sm text-black dark:text-white bg-white dark:bg-black hover:!bg-white hover:dark:!bg-black',
{
hidden: Object.keys(toasts).length === 0,
hidden: validToasts.length === 0,
}
)}
onClick={() => {
@@ -46,12 +46,20 @@ type Actions = {
* Arbitrary removes all toasts
*/
removeAll: () => void;
/**
* Checks if a given toasts exists in the collection
*/
hasToast: (id: string) => boolean;
/**
* Closes toast by meta
*/
closeBy: (meta: Toast['meta']) => void;
};
type ToastsStore = State & Actions;
export const useToasts = create<ToastsStore>()(
immer((set) => ({
immer((set, get) => ({
toasts: {},
count: 0,
add: (toast) =>
@@ -97,6 +105,18 @@ export const useToasts = create<ToastsStore>()(
}
}),
removeAll: () => set({ toasts: {}, count: 0 }),
hasToast: (id) => get().toasts[id] != null,
closeBy: (meta) => {
if (!meta) return;
set((state) => {
const found = Object.values(state.toasts).find((t) =>
isEqual(t.meta, meta)
);
if (found) {
found.signal = 'close';
}
});
},
}))
);
@@ -10,7 +10,7 @@ import {
import type { ITooltipParams } from 'ag-grid-community';
const tooltipContentClasses =
'max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-2 py-1 z-20 rounded text-xs break-word';
'max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-2 py-1 z-20 rounded text-xs text-black dark:text-white break-word';
export interface TooltipProps {
children: React.ReactElement;
description?: string | ReactNode;
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "@vegaprotocol/utils",
"version": "0.0.4",
"version": "0.0.5",
"type": "commonjs"
}
+1
View File
@@ -1,4 +1,5 @@
export * from './date';
export * from './number';
export * from './range';
export * from './size';
export * from './strings';
+80 -73
View File
@@ -29,14 +29,21 @@ describe('number utils', () => {
{ v: new BigNumber(123000), d: 3, o: '123.00', q: 0.1 },
{ v: new BigNumber(123000), d: 1, o: '12,300.00', q: 0.1 },
{ v: new BigNumber(123001000), d: 2, o: '1,230,010.00', q: 0.1 },
{ v: new BigNumber(123001), d: 2, o: '1,230', q: 100 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 100 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 0.1 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 1 },
{
v: BigNumber('123456789123456789'),
d: 10,
o: '12,345,678.9123457',
o: '12,345,678.91234568',
q: '0.00003846',
},
{
v: BigNumber('123456789123456789'),
d: 10,
o: '12,345,678.91234568',
q: '1',
},
])(
'formats with addDecimalsFormatNumberQuantum given number correctly',
({ v, d, o, q }) => {
@@ -70,80 +77,80 @@ describe('number utils', () => {
])('formats given number correctly', ({ v, d, o }) => {
expect(formatNumberPercentage(v, d)).toStrictEqual(o);
});
});
describe('toNumberParts', () => {
it.each([
{ v: null, d: 3, o: ['0', '000'] },
{ v: undefined, d: 3, o: ['0', '000'] },
{ v: new BigNumber(123), d: 3, o: ['123', '00'] },
{ v: new BigNumber(123.123), d: 3, o: ['123', '123'] },
{ v: new BigNumber(123.123), d: 6, o: ['123', '123'] },
{ v: new BigNumber(123.123), d: 0, o: ['123', ''] },
{ v: new BigNumber(123), d: undefined, o: ['123', '00'] },
{
v: new BigNumber(30000),
d: undefined,
o: ['30,000', '00'],
},
])('returns correct tuple given the different arguments', ({ v, d, o }) => {
expect(toNumberParts(v, d)).toStrictEqual(o);
describe('toNumberParts', () => {
it.each([
{ v: null, d: 3, o: ['0', '000'] },
{ v: undefined, d: 3, o: ['0', '000'] },
{ v: new BigNumber(123), d: 3, o: ['123', '00'] },
{ v: new BigNumber(123.123), d: 3, o: ['123', '123'] },
{ v: new BigNumber(123.123), d: 6, o: ['123', '123'] },
{ v: new BigNumber(123.123), d: 0, o: ['123', ''] },
{ v: new BigNumber(123), d: undefined, o: ['123', '00'] },
{
v: new BigNumber(30000),
d: undefined,
o: ['30,000', '00'],
},
])('returns correct tuple given the different arguments', ({ v, d, o }) => {
expect(toNumberParts(v, d)).toStrictEqual(o);
});
});
});
describe('isNumeric', () => {
it.each([
{ i: null, o: false },
{ i: undefined, o: false },
{ i: 1, o: true },
{ i: '1', o: true },
{ i: '-1', o: true },
{ i: 0.1, o: true },
{ i: '.1', o: true },
{ i: '-.1', o: true },
{ i: 123, o: true },
{ i: -123, o: true },
{ i: '123', o: true },
{ i: '123.01', o: true },
{ i: '-123.01', o: true },
{ i: '--123.01', o: false },
{ i: '123.', o: false },
{ i: '123.1.1', o: false },
{ i: BigInt(123), o: true },
{ i: BigInt(-1), o: true },
{ i: new BigNumber(123), o: true },
{ i: new BigNumber(123.123), o: true },
{ i: new BigNumber(123.123).toString(), o: true },
{ i: new BigNumber(123), o: true },
{ i: Infinity, o: false },
{ i: NaN, o: false },
])(
'returns correct results',
({
i,
o,
}: {
i: number | string | undefined | null | BigNumber | bigint;
o: boolean;
}) => {
expect(isNumeric(i)).toStrictEqual(o);
}
);
});
describe('isNumeric', () => {
it.each([
{ i: null, o: false },
{ i: undefined, o: false },
{ i: 1, o: true },
{ i: '1', o: true },
{ i: '-1', o: true },
{ i: 0.1, o: true },
{ i: '.1', o: true },
{ i: '-.1', o: true },
{ i: 123, o: true },
{ i: -123, o: true },
{ i: '123', o: true },
{ i: '123.01', o: true },
{ i: '-123.01', o: true },
{ i: '--123.01', o: false },
{ i: '123.', o: false },
{ i: '123.1.1', o: false },
{ i: BigInt(123), o: true },
{ i: BigInt(-1), o: true },
{ i: new BigNumber(123), o: true },
{ i: new BigNumber(123.123), o: true },
{ i: new BigNumber(123.123).toString(), o: true },
{ i: new BigNumber(123), o: true },
{ i: Infinity, o: false },
{ i: NaN, o: false },
])(
'returns correct results',
({
i,
o,
}: {
i: number | string | undefined | null | BigNumber | bigint;
o: boolean;
}) => {
expect(isNumeric(i)).toStrictEqual(o);
}
);
});
describe('toDecimal', () => {
it.each([
{ v: 0, o: '1' },
{ v: 1, o: '0.1' },
{ v: 2, o: '0.01' },
{ v: 3, o: '0.001' },
{ v: 4, o: '0.0001' },
{ v: 5, o: '0.00001' },
{ v: 6, o: '0.000001' },
{ v: 7, o: '0.0000001' },
{ v: 8, o: '0.00000001' },
{ v: 9, o: '0.000000001' },
])('formats with toNumber given number correctly', ({ v, o }) => {
expect(toDecimal(v)).toStrictEqual(o);
describe('toDecimal', () => {
it.each([
{ v: 0, o: '1' },
{ v: 1, o: '0.1' },
{ v: 2, o: '0.01' },
{ v: 3, o: '0.001' },
{ v: 4, o: '0.0001' },
{ v: 5, o: '0.00001' },
{ v: 6, o: '0.000001' },
{ v: 7, o: '0.0000001' },
{ v: 8, o: '0.00000001' },
{ v: 9, o: '0.000000001' },
])('formats with toNumber given number correctly', ({ v, o }) => {
expect(toDecimal(v)).toStrictEqual(o);
});
});
});
+2 -1
View File
@@ -98,7 +98,8 @@ export const addDecimalsFormatNumberQuantum = (
if (isNaN(Number(quantum))) {
return addDecimalsFormatNumber(rawValue, decimalPlaces);
}
const numberDP = Math.max(0, Math.log10(100 / Number(quantum)));
const quantumValue = addDecimal(quantum, decimalPlaces);
const numberDP = Math.max(0, Math.log10(100 / Number(quantumValue)));
return addDecimalsFormatNumber(rawValue, decimalPlaces, Math.ceil(numberDP));
};
@@ -1,6 +1,6 @@
import { formatRange, formatValue } from './deal-ticket-fee-details';
import { formatRange, formatValue } from './range';
describe('formatRange, formatValue', () => {
describe('formatValue', () => {
it.each([
{ v: 123000, d: 5, o: '1.23' },
{ v: 123000, d: 3, o: '123.00' },
@@ -21,12 +21,12 @@ describe('formatRange, formatValue', () => {
{ v: 123000, d: 3, o: '123.00', q: '0.1' },
{ v: 123000, d: 1, o: '12,300.00', q: '0.1' },
{ v: 123001000, d: 2, o: '1,230,010.00', q: '0.1' },
{ v: 123001, d: 2, o: '1,230', q: '100' },
{ v: 123001, d: 2, o: '1,230.01', q: '100' },
{ v: 123001, d: 2, o: '1,230.01', q: '0.1' },
{
v: '123456789123456789',
d: 10,
o: '12,345,678.9123457',
o: '12,345,678.91234568',
q: '0.00003846',
},
])(
@@ -35,9 +35,16 @@ describe('formatRange, formatValue', () => {
expect(formatValue(v.toString(), d, q)).toStrictEqual(o);
}
);
});
describe('formatRange', () => {
it.each([
{ min: 123000, max: 12300011111, d: 5, o: '1.23 - 123,000.111', q: '0.1' },
{
min: 123000,
max: 12300011111,
d: 5,
o: '1.23 - 123,000.11111',
q: '0.1',
},
{
min: 123000,
max: 12300011111,
@@ -56,7 +63,7 @@ describe('formatRange, formatValue', () => {
min: 123001000,
max: 12300011111,
d: 2,
o: '1,230,010 - 123,000,111',
o: '1,230,010.00 - 123,000,111.11',
q: '100',
},
])(

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