Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03d82aac62 | ||
|
|
2db0aae974 | ||
|
|
f10e616d5b | ||
|
|
ce3da97a8a | ||
|
|
cabd99d3ef | ||
|
|
597e07608f | ||
|
|
e451dc54b3 | ||
|
|
9703c3b7a6 | ||
|
|
ff14d7c8dd | ||
|
|
c22b6f3ce9 | ||
|
|
c740c11eaa | ||
|
|
d1fabb31a3 | ||
|
|
94f045a60b | ||
|
|
1af375bab8 | ||
|
|
5a48ba4a33 | ||
|
|
d1fd9184ce | ||
|
|
8f29824f9e | ||
|
|
fc3c73ad24 |
@@ -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,19 +235,34 @@ 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') }}
|
||||
@@ -295,3 +310,50 @@ jobs:
|
||||
sleep 5
|
||||
gh pr merge "${pr_url}" --delete-branch --squash --admin
|
||||
)
|
||||
|
||||
- name: Update console.fairground.wtf DNS to redirect to the new console
|
||||
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && endsWith(github.ref, 'testnet') }}
|
||||
run: |
|
||||
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"
|
||||
which ipfs
|
||||
new_hash=$(cat ${{ matrix.app }}-ipfs-hash)
|
||||
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
|
||||
|
||||
# Generate console URL
|
||||
|
||||
new_console_url_type=ipfs
|
||||
# new_console_url_type=ipns
|
||||
|
||||
new_console_url_domain=cf-ipfs.com
|
||||
# new_console_url_domain=dweb.link
|
||||
|
||||
new_console_url="http://${new_cid}.${new_console_url_type}.${new_console_url_domain}/"
|
||||
echo "new_console_url=${new_console_url}"
|
||||
|
||||
# Update record in DNSimple
|
||||
# docs: https://developer.dnsimple.com/v2/zones/records/#updateZoneRecord
|
||||
dnsimple_account_id=84895
|
||||
dnsimple_zone_name=fairground.wtf
|
||||
dnsimple_record_id=45300615
|
||||
# see: https://dnsimple.com/a/84895/domains/fairground.wtf/records/45300615/edit
|
||||
curl --fail -H 'Authorization: Bearer ${{ secrets.DNSIMPLE_API_TOKEN }}' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X PATCH \
|
||||
-d "{
|
||||
\"content\": \"${new_console_url}\"
|
||||
}" \
|
||||
https://api.dnsimple.com/v2/${dnsimple_account_id}/zones/${dnsimple_zone_name}/records/${dnsimple_record_id}
|
||||
|
||||
# trading.fairground.wtf
|
||||
dnsimple_record_id=45302531
|
||||
curl --fail -H 'Authorization: Bearer ${{ secrets.DNSIMPLE_API_TOKEN }}' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X PATCH \
|
||||
-d "{
|
||||
\"content\": \"${new_console_url}\"
|
||||
}" \
|
||||
https://api.dnsimple.com/v2/${dnsimple_account_id}/zones/${dnsimple_zone_name}/records/${dnsimple_record_id}
|
||||
|
||||
@@ -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] : '-';
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -49,7 +49,7 @@ module.exports = defineConfig({
|
||||
vegaTokenContractAddress: '0xF41bD86d462D36b997C0bbb4D97a0a3382f205B7',
|
||||
vegaTokenAddress: '0x67175Da1D5e966e40D11c4B2519392B2058373de',
|
||||
txTimeout: { timeout: 70000 },
|
||||
epochTimeout: { timeout: 6000 },
|
||||
epochTimeout: { timeout: 10000 },
|
||||
blockConfirmations: 3,
|
||||
grepTags: '@regression @smoke @slow',
|
||||
grepFilterSpecs: true,
|
||||
|
||||
@@ -205,29 +205,33 @@ context(
|
||||
});
|
||||
|
||||
// 3003-PMAN-001
|
||||
it('Able to submit valid new market proposal', function () {
|
||||
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
|
||||
cy.get(newProposalTitle).type('Test new market proposal');
|
||||
cy.get(newProposalDescription).type('E2E test for proposals');
|
||||
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
|
||||
const newMarketPayload = JSON.stringify(newMarketProposal);
|
||||
cy.get(newProposalTerms).type(newMarketPayload, {
|
||||
parseSpecialCharSequences: false,
|
||||
delay: 2,
|
||||
});
|
||||
});
|
||||
cy.get(proposalDownloadBtn)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-new-market-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath }); // 3003-PMAN-003
|
||||
it(
|
||||
'Able to submit valid new market proposal',
|
||||
{ tags: '@smoke' },
|
||||
function () {
|
||||
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
|
||||
cy.get(newProposalTitle).type('Test new market proposal');
|
||||
cy.get(newProposalDescription).type('E2E test for proposals');
|
||||
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
|
||||
const newMarketPayload = JSON.stringify(newMarketProposal);
|
||||
cy.get(newProposalTerms).type(newMarketPayload, {
|
||||
parseSpecialCharSequences: false,
|
||||
delay: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
cy.get(proposalDownloadBtn)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-new-market-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath }); // 3003-PMAN-003
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('Unable to submit new market proposal with missing/invalid fields', function () {
|
||||
const errorMsg =
|
||||
|
||||
@@ -54,7 +54,7 @@ context(
|
||||
'Staking Tab - with eth and vega wallets connected',
|
||||
{ tags: '@slow' },
|
||||
function () {
|
||||
// 2001-STKE-002, 2001-STKE-032
|
||||
// 1002-STKE-002, 1002-STKE-032
|
||||
before('visit staking tab and connect vega wallet', function () {
|
||||
cy.visit('/');
|
||||
ethereumWalletConnect();
|
||||
@@ -63,7 +63,7 @@ context(
|
||||
vegaWalletSetSpecifiedApprovalAmount('1000');
|
||||
});
|
||||
|
||||
describe.skip('Eth wallet - contains VEGA tokens', function () {
|
||||
describe('Eth wallet - contains VEGA tokens', function () {
|
||||
beforeEach(
|
||||
'teardown wallet & drill into a specific validator',
|
||||
function () {
|
||||
@@ -77,25 +77,41 @@ context(
|
||||
}
|
||||
);
|
||||
|
||||
// 1002-STKE-035 1002-STKE-036
|
||||
it('Unable to stake against a validator with less than minimum and more than associated amount', function () {
|
||||
ensureSpecifiedUnstakedTokensAreAssociated('3');
|
||||
verifyUnstakedBalance(3.0);
|
||||
verifyEthWalletTotalAssociatedBalance('3.0');
|
||||
verifyEthWalletAssociatedBalance('3.0');
|
||||
cy.get('button').contains('Select a validator to nominate').click();
|
||||
clickOnValidatorFromList(0);
|
||||
cy.getByTestId(stakeAddStakeRadioButton, epochTimeout).click({
|
||||
force: true,
|
||||
});
|
||||
cy.getByTestId(stakeTokenAmountInputBox).type('0.001');
|
||||
cy.getByTestId(stakeTokenSubmitButton).should('be.disabled');
|
||||
cy.getByTestId(stakeTokenAmountInputBox).clear().type('4');
|
||||
cy.getByTestId(stakeTokenSubmitButton).should('be.disabled');
|
||||
});
|
||||
it('Able to stake against a validator - using vega from wallet', function () {
|
||||
ensureSpecifiedUnstakedTokensAreAssociated('3');
|
||||
verifyUnstakedBalance(3.0);
|
||||
verifyEthWalletTotalAssociatedBalance('3.0');
|
||||
verifyEthWalletAssociatedBalance('3.0');
|
||||
cy.get('button').contains('Select a validator to nominate').click();
|
||||
// 2001-STKE-031
|
||||
// 1002-STKE-031
|
||||
clickOnValidatorFromList(0);
|
||||
// 2001-STKE-033, 2001-STKE-034, 2001-STKE-037
|
||||
// 1002-STKE-033, 1002-STKE-034, 1002-STKE-037
|
||||
stakingValidatorPageAddStake('2');
|
||||
verifyUnstakedBalance(1.0);
|
||||
// 2001-STKE-039
|
||||
// 1002-STKE-039
|
||||
verifyStakedBalance(2.0);
|
||||
verifyNextEpochValue(2.0); // 2001-STKE-016 2001-STKE-038
|
||||
verifyThisEpochValue(2.0); // 2001-STKE-013
|
||||
verifyNextEpochValue(2.0); // 1002-STKE-016 1002-STKE-038
|
||||
verifyThisEpochValue(2.0); // 1002-STKE-013
|
||||
closeStakingDialog();
|
||||
navigateTo(navigation.validators);
|
||||
|
||||
// 2002-SINC-007
|
||||
// 2002-SINC-007 1002-STKE-015 1002-STKE-017 1002-STKE-052
|
||||
validateValidatorListTotalStakeAndShare('0', '3,002.00', '50.02%');
|
||||
});
|
||||
|
||||
@@ -218,7 +234,7 @@ context(
|
||||
});
|
||||
});
|
||||
|
||||
// 2001-STKE-041
|
||||
// 1002-STKE-041 1002-STKE-053
|
||||
it(
|
||||
'Able to remove part of a stake against a validator',
|
||||
{ tags: '@smoke' },
|
||||
@@ -231,11 +247,11 @@ context(
|
||||
verifyUnstakedBalance(1.0);
|
||||
closeStakingDialog();
|
||||
navigateTo(navigation.validators);
|
||||
// 2001-STKE-040
|
||||
// 1002-STKE-040
|
||||
clickOnValidatorFromList(0);
|
||||
// 2001-STKE-044, 2001-STKE-048
|
||||
// 1002-STKE-044, 1002-STKE-048
|
||||
stakingValidatorPageRemoveStake('1');
|
||||
// 2001-STKE-049
|
||||
// 1002-STKE-049
|
||||
verifyNextEpochValue(2.0);
|
||||
verifyUnstakedBalance(2.0);
|
||||
verifyStakedBalance(2.0);
|
||||
@@ -255,7 +271,7 @@ context(
|
||||
}
|
||||
);
|
||||
|
||||
// 2001-STKE-045
|
||||
// 1002-STKE-045
|
||||
it('Able to remove a full stake against a validator', function () {
|
||||
stakingPageAssociateTokens('3');
|
||||
verifyUnstakedBalance(3.0);
|
||||
@@ -303,6 +319,7 @@ context(
|
||||
.and('be.visible');
|
||||
});
|
||||
|
||||
// 1002-STKE-046 1002-STKE-047
|
||||
it('Unable to remove a stake greater than staked amount next epoch for a validator', function () {
|
||||
stakingPageAssociateTokens('3');
|
||||
verifyUnstakedBalance(3.0);
|
||||
@@ -405,7 +422,7 @@ context(
|
||||
});
|
||||
|
||||
it('Associating wallet tokens - when some already staked - auto stakes tokens to staked validator', function () {
|
||||
// 2001-STKE-004
|
||||
// 1002-STKE-004
|
||||
stakingPageAssociateTokens('3');
|
||||
verifyUnstakedBalance(3.0);
|
||||
cy.get('button').contains('Select a validator to nominate').click();
|
||||
@@ -419,7 +436,7 @@ context(
|
||||
});
|
||||
|
||||
it('Associating vesting contract tokens - when some already staked - auto stakes tokens to staked validator', function () {
|
||||
// 2001-STKE-004
|
||||
// 1002-STKE-004
|
||||
stakingPageAssociateTokens('3', { type: 'contract' });
|
||||
verifyUnstakedBalance(3.0);
|
||||
cy.get('button').contains('Select a validator to nominate').click();
|
||||
@@ -433,7 +450,7 @@ context(
|
||||
});
|
||||
|
||||
it('Associating vesting contract tokens - when wallet tokens already staked - auto stakes tokens to staked validator', function () {
|
||||
// 2001-STKE-004
|
||||
// 1002-STKE-004
|
||||
stakingPageAssociateTokens('3', { type: 'wallet' });
|
||||
verifyUnstakedBalance(3.0);
|
||||
cy.get('button').contains('Select a validator to nominate').click();
|
||||
@@ -447,7 +464,7 @@ context(
|
||||
});
|
||||
|
||||
it('Associating tokens - with multiple validators already staked - auto stakes to staked validators - abiding by existing stake ratio', function () {
|
||||
// 2001-STKE-004
|
||||
// 1002-STKE-004
|
||||
stakingPageAssociateTokens('6');
|
||||
verifyUnstakedBalance(6.0);
|
||||
cy.get('button').contains('Select a validator to nominate').click();
|
||||
@@ -500,7 +517,7 @@ context(
|
||||
}
|
||||
|
||||
function verifyThisEpochValue(amount: number) {
|
||||
cy.getByTestId('stake-this-epoch', epochTimeout) // 2001-STKE-013
|
||||
cy.getByTestId('stake-this-epoch', epochTimeout) // 1002-STKE-013
|
||||
.contains(amount, epochTimeout)
|
||||
.should('be.visible');
|
||||
}
|
||||
|
||||
@@ -64,31 +64,38 @@ context(
|
||||
}
|
||||
);
|
||||
|
||||
it('Able to associate tokens - from wallet', function () {
|
||||
//1004-ASSO-003
|
||||
//1004-ASSO-005
|
||||
//1004-ASSO-009
|
||||
//1004-ASSO-030
|
||||
//1004-ASSO-012
|
||||
//1004-ASSO-013
|
||||
//1004-ASSO-014
|
||||
//1004-ASSO-015
|
||||
//1004-ASSO-030
|
||||
//0005-ETXN-006
|
||||
//0005-ETXN-003
|
||||
//0005-ETXN-005
|
||||
stakingPageAssociateTokens('2', { skipConfirmation: true });
|
||||
validateWalletCurrency('Associated', '0.00');
|
||||
validateWalletCurrency('Pending association', '2.00');
|
||||
validateWalletCurrency('Total associated after pending', '2.00');
|
||||
// 0005-ETXN-002
|
||||
verifyEthWalletAssociatedBalance('2.0');
|
||||
verifyEthWalletTotalAssociatedBalance('2.0');
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should('contain', 2.0);
|
||||
});
|
||||
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0);
|
||||
});
|
||||
it(
|
||||
'Able to associate tokens - from wallet',
|
||||
{ tags: '@smoke' },
|
||||
function () {
|
||||
//1004-ASSO-003
|
||||
//1004-ASSO-005
|
||||
//1004-ASSO-009
|
||||
//1004-ASSO-030
|
||||
//1004-ASSO-012
|
||||
//1004-ASSO-013
|
||||
//1004-ASSO-014
|
||||
//1004-ASSO-015
|
||||
//1004-ASSO-030
|
||||
//0005-ETXN-006
|
||||
//0005-ETXN-003
|
||||
//0005-ETXN-005
|
||||
stakingPageAssociateTokens('2', { skipConfirmation: true });
|
||||
validateWalletCurrency('Associated', '0.00');
|
||||
validateWalletCurrency('Pending association', '2.00');
|
||||
validateWalletCurrency('Total associated after pending', '2.00');
|
||||
// 0005-ETXN-002
|
||||
verifyEthWalletAssociatedBalance('2.0');
|
||||
verifyEthWalletTotalAssociatedBalance('2.0');
|
||||
cy.get(vegaWallet).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
|
||||
'contain',
|
||||
2.0
|
||||
);
|
||||
});
|
||||
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', 2.0);
|
||||
}
|
||||
);
|
||||
|
||||
it('Able to disassociate all associated tokens - manually', function () {
|
||||
// 1004-ASSO-025
|
||||
|
||||
@@ -55,6 +55,10 @@ context(
|
||||
navigateTo(navigation.withdraw);
|
||||
cy.connectVegaWallet();
|
||||
ethereumWalletConnect();
|
||||
cy.getByTestId('currency-title', txTimeout).should(
|
||||
'contain.text',
|
||||
usdtName
|
||||
);
|
||||
});
|
||||
|
||||
it('Able to open withdrawal form with vega wallet connected', function () {
|
||||
@@ -92,71 +96,75 @@ context(
|
||||
});
|
||||
});
|
||||
|
||||
it('Able to withdraw asset: -eth wallet connected -withdraw funds button', function () {
|
||||
// fill in withdrawal form
|
||||
cy.getByTestId(withdraw).should('be.visible').click();
|
||||
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
|
||||
cy.getByTestId('select-asset').click();
|
||||
cy.get('select')
|
||||
.select(usdtSelectValue, { force: true })
|
||||
.should('have.value', usdtSelectValue);
|
||||
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
|
||||
cy.getByTestId(withdrawalThreshold).should(
|
||||
'have.text',
|
||||
'100,000.00000T'
|
||||
);
|
||||
cy.getByTestId(delayTime).should('have.text', 'None');
|
||||
cy.getByTestId(amountInput).click().type('120');
|
||||
cy.getByTestId(submitWithdrawalButton).click();
|
||||
});
|
||||
// assert withdrawal request
|
||||
cy.getByTestId(toast)
|
||||
.first(txTimeout)
|
||||
.should('contain.text', 'Funds unlocked')
|
||||
.within(() => {
|
||||
cy.getByTestId('external-link').should('exist');
|
||||
cy.getByTestId(toastPanel).should(
|
||||
'contain.text',
|
||||
'Withdraw 120.00 tUSDC'
|
||||
it(
|
||||
'Able to withdraw asset: -eth wallet connected -withdraw funds button',
|
||||
{ tags: '@smoke' },
|
||||
function () {
|
||||
// fill in withdrawal form
|
||||
cy.getByTestId(withdraw).should('be.visible').click();
|
||||
cy.getByTestId(withdrawalForm, txTimeout).within(() => {
|
||||
cy.getByTestId('select-asset').click();
|
||||
cy.get('select')
|
||||
.select(usdtSelectValue, { force: true })
|
||||
.should('have.value', usdtSelectValue);
|
||||
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
|
||||
cy.getByTestId(withdrawalThreshold).should(
|
||||
'have.text',
|
||||
'100,000.00000T'
|
||||
);
|
||||
cy.getByTestId(toastCompleteWithdrawal).click();
|
||||
cy.getByTestId(toastClose).click();
|
||||
cy.getByTestId(delayTime).should('have.text', 'None');
|
||||
cy.getByTestId(amountInput).click().type('120');
|
||||
cy.getByTestId(submitWithdrawalButton).click();
|
||||
});
|
||||
// withdrawal complete
|
||||
cy.getByTestId(toast)
|
||||
.first(txTimeout)
|
||||
.should('contain.text', 'The withdrawal has been approved.')
|
||||
.within(() => {
|
||||
cy.getByTestId(toastPanel).should(
|
||||
'contain.text',
|
||||
'Withdraw 120.00 tUSDC'
|
||||
);
|
||||
});
|
||||
cy.getByTestId(toast)
|
||||
.last(txTimeout)
|
||||
.should('contain.text', 'Transaction confirmed')
|
||||
.within(() => {
|
||||
cy.getByTestId('external-link').should('exist');
|
||||
});
|
||||
// withdrawal history for complete withdrawal displayed
|
||||
cy.get(tableWithdrawnStatus)
|
||||
.eq(1, txTimeout)
|
||||
.should('have.text', 'Completed')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get(tableAssetSymbol).should('have.text', usdcSymbol);
|
||||
cy.get(tableAmount).should('have.text', '120.00');
|
||||
cy.get(tableReceiverAddress)
|
||||
.find('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://sepolia.etherscan.io/address/');
|
||||
cy.get(tableWithdrawnTimeStamp).should('not.be.empty');
|
||||
cy.get(tableTxHash)
|
||||
.find('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://sepolia.etherscan.io/tx/');
|
||||
});
|
||||
});
|
||||
// assert withdrawal request
|
||||
cy.getByTestId(toast)
|
||||
.first(txTimeout)
|
||||
.should('contain.text', 'Funds unlocked')
|
||||
.within(() => {
|
||||
cy.getByTestId('external-link').should('exist');
|
||||
cy.getByTestId(toastPanel).should(
|
||||
'contain.text',
|
||||
'Withdraw 120.00 tUSDC'
|
||||
);
|
||||
cy.getByTestId(toastCompleteWithdrawal).click();
|
||||
cy.getByTestId(toastClose).click();
|
||||
});
|
||||
// withdrawal complete
|
||||
cy.getByTestId(toast)
|
||||
.first(txTimeout)
|
||||
.should('contain.text', 'The withdrawal has been approved.')
|
||||
.within(() => {
|
||||
cy.getByTestId(toastPanel).should(
|
||||
'contain.text',
|
||||
'Withdraw 120.00 tUSDC'
|
||||
);
|
||||
});
|
||||
cy.getByTestId(toast)
|
||||
.last(txTimeout)
|
||||
.should('contain.text', 'Transaction confirmed')
|
||||
.within(() => {
|
||||
cy.getByTestId('external-link').should('exist');
|
||||
});
|
||||
// withdrawal history for complete withdrawal displayed
|
||||
cy.get(tableWithdrawnStatus)
|
||||
.eq(1, txTimeout)
|
||||
.should('have.text', 'Completed')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get(tableAssetSymbol).should('have.text', usdcSymbol);
|
||||
cy.get(tableAmount).should('have.text', '120.00');
|
||||
cy.get(tableReceiverAddress)
|
||||
.find('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://sepolia.etherscan.io/address/');
|
||||
cy.get(tableWithdrawnTimeStamp).should('not.be.empty');
|
||||
cy.get(tableTxHash)
|
||||
.find('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://sepolia.etherscan.io/tx/');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('Able to withdraw asset: -eth wallet not connected', function () {
|
||||
const ethWalletAddress = Cypress.env('ethWalletPublicKey');
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
verifyPageHeader,
|
||||
verifyTabHighlighted,
|
||||
} from '../../support/common.functions';
|
||||
import { clickOnValidatorFromList } from '../../support/staking.functions';
|
||||
import {
|
||||
clickOnValidatorFromList,
|
||||
waitForBeginningOfEpoch,
|
||||
} from '../../support/staking.functions';
|
||||
|
||||
const guideLink = '[data-testid="staking-guide-link"]';
|
||||
const validatorTitle = '[data-testid="validator-node-title"]';
|
||||
@@ -37,206 +40,213 @@ context('Validators Page - verify elements on page', function () {
|
||||
});
|
||||
|
||||
describe('with wallets disconnected', { tags: '@smoke' }, function () {
|
||||
describe('description section', function () {
|
||||
it('Should have validators tab highlighted', function () {
|
||||
verifyTabHighlighted(navigation.validators);
|
||||
});
|
||||
|
||||
it('Should have validators ON VEGA header visible', function () {
|
||||
verifyPageHeader('Validators');
|
||||
});
|
||||
|
||||
it('Should have Staking Guide link visible', function () {
|
||||
// 2001-STKE-003
|
||||
cy.get(guideLink)
|
||||
.should('be.visible')
|
||||
.and('have.text', 'Read more about staking on Vega')
|
||||
.and(
|
||||
'have.attr',
|
||||
'href',
|
||||
'https://docs.vega.xyz/mainnet/concepts/vega-chain/#staking-on-vega'
|
||||
);
|
||||
});
|
||||
it('Should have validators tab highlighted', function () {
|
||||
verifyTabHighlighted(navigation.validators);
|
||||
});
|
||||
describe(
|
||||
'Should be able to see validator list from the staking page',
|
||||
{ tags: '@regression' },
|
||||
function () {
|
||||
// 2001-STKE-050
|
||||
it('Should be able to see validator names', function () {
|
||||
cy.get('[col-id="validator"] > div > span')
|
||||
.should('have.length.at.least', 1)
|
||||
.each(($name) => {
|
||||
cy.wrap($name).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should be able to see validator stake', function () {
|
||||
cy.getByTestId('total-stake')
|
||||
.should('have.length.at.least', 1)
|
||||
.each(($stake) => {
|
||||
cy.wrap($stake).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
it('Should have validators ON VEGA header visible', function () {
|
||||
verifyPageHeader('Validators');
|
||||
});
|
||||
|
||||
it('Should be able to see validator stake tooltip', function () {
|
||||
cy.getByTestId('total-stake').first().realHover();
|
||||
it('Should have Staking Guide link visible', function () {
|
||||
// 1002-STKE-003
|
||||
cy.get(guideLink)
|
||||
.should('be.visible')
|
||||
.and('have.text', 'Read more about staking on Vega')
|
||||
.and(
|
||||
'have.attr',
|
||||
'href',
|
||||
'https://docs.vega.xyz/mainnet/concepts/vega-chain/#staking-on-vega'
|
||||
);
|
||||
});
|
||||
|
||||
cy.get(stakedByOperatorToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Staked by operator: 3,000.00');
|
||||
cy.get(stakedByDelegatesToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Staked by delegates: 0.00');
|
||||
cy.get(totalStakedToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Total stake: 3,000.00');
|
||||
});
|
||||
|
||||
it('Should be able to see validator normalised voting power', function () {
|
||||
cy.getByTestId('normalised-voting-power')
|
||||
.should('have.length.at.least', 1)
|
||||
.each(($vPower) => {
|
||||
cy.wrap($vPower).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should be able to see validator normalised voting power tooltip', function () {
|
||||
cy.getByTestId('normalised-voting-power').first().realHover();
|
||||
|
||||
cy.get(unnormalisedVotingPowerToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Unnormalised voting power: 20.00%');
|
||||
cy.get(normalisedVotingPowerToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Normalised voting power: 50.00%');
|
||||
});
|
||||
|
||||
// 2002-SINC-018
|
||||
it('Should be able to see validator total penalties', function () {
|
||||
cy.getByTestId('total-penalty')
|
||||
.should('have.length.at.least', 1)
|
||||
.each(($penalties) => {
|
||||
cy.wrap($penalties).should('contain.text', '0%');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should be able to see validator penalties tooltip', function () {
|
||||
cy.getByTestId('total-penalty').realHover();
|
||||
|
||||
cy.get(performancePenaltyToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Performance penalty: 0.00%');
|
||||
cy.get(overstakedPenaltyToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Overstaked penalty: 60.00%'); // value not asserted due to #2886
|
||||
cy.get(totalPenaltyToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Total penalties: 60.00%');
|
||||
});
|
||||
|
||||
it('Should be able to see validator pending stake', function () {
|
||||
cy.getByTestId('total-pending-stake')
|
||||
.should('have.length.at.least', 1)
|
||||
.each(($pendingStake) => {
|
||||
cy.wrap($pendingStake).should('contain.text', '0.00');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// 2001-STKE-050
|
||||
describe(
|
||||
'Should be able to see static information about a validator',
|
||||
{ tags: '@smoke' },
|
||||
function () {
|
||||
before('connect wallets and click on validator', function () {
|
||||
cy.connectVegaWallet();
|
||||
clickOnValidatorFromList(0);
|
||||
});
|
||||
|
||||
// 2001-STKE-006
|
||||
it('Should be able to see validator name', function () {
|
||||
cy.get(validatorTitle).should('not.be.empty');
|
||||
});
|
||||
|
||||
// 2001-STKE-007
|
||||
it('Should be able to see validator id', function () {
|
||||
cy.get(validatorId).should('not.be.empty');
|
||||
});
|
||||
|
||||
// 2001-STKE-008
|
||||
it('Should be able to see validator public key', function () {
|
||||
cy.get(validatorPubKey).should('not.be.empty');
|
||||
});
|
||||
|
||||
// 2001-STKE-010
|
||||
it('Should be able to see Ethereum address', function () {
|
||||
cy.get(ethAddressLink)
|
||||
.should('not.be.empty')
|
||||
.and('have.attr', 'href');
|
||||
});
|
||||
// TODO validators missing url for more information about them 2001-STKE-09
|
||||
|
||||
it('Should be able to see validator status', function () {
|
||||
cy.get(validatorStatus).should('have.text', 'Consensus');
|
||||
});
|
||||
|
||||
// 2001-STKE-012
|
||||
it('Should be able to see total stake', function () {
|
||||
cy.get(totalStake).invoke('text').should('match', stakeNumberRegex);
|
||||
});
|
||||
|
||||
it('Should be able to see pending stake', function () {
|
||||
cy.get(pendingStake).invoke('text').should('match', stakeNumberRegex);
|
||||
});
|
||||
|
||||
it('Should be able to see staked by operator', function () {
|
||||
cy.get(stakedByOperator)
|
||||
.invoke('text')
|
||||
.should('match', stakeNumberRegex);
|
||||
});
|
||||
|
||||
it('Should be able to see staked by delegates', function () {
|
||||
cy.get(stakedByDelegates)
|
||||
.invoke('text')
|
||||
.should('match', stakeNumberRegex);
|
||||
});
|
||||
|
||||
// 2001-STKE-051
|
||||
it('Should be able to see stake share in percentage', function () {
|
||||
cy.get(stakeShare)
|
||||
.invoke('text')
|
||||
.then(($stakePercentage) => {
|
||||
// The pattern must start at a word boundary (\b).
|
||||
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
|
||||
// The pattern can be one of the following:
|
||||
// A percentage value of zero (0%), or
|
||||
// A non-zero percentage value that can be:
|
||||
// A single digit (\d) between 0 and 9, or
|
||||
// A two-digit number between 0 and 99 (\d{1,2}), or
|
||||
// The number 100.
|
||||
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
|
||||
// The pattern must end with a percentage sign (%).
|
||||
|
||||
cy.wrap($stakePercentage).should(
|
||||
'match',
|
||||
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// 2001-STKE-011 2002-SINC-001 2002-SINC-002
|
||||
it('Should be able to see epoch information', function () {
|
||||
const epochTitle = 'h3';
|
||||
const nextEpochInfo = 'p';
|
||||
|
||||
cy.get(epochCountDown).within(() => {
|
||||
cy.get(epochTitle).should('not.be.empty');
|
||||
cy.get(nextEpochInfo).should('contain.text', 'Next epoch');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
// 1002-STKE-032
|
||||
it('Should have button to connect vega wallet in validator page', function () {
|
||||
clickOnValidatorFromList(0);
|
||||
cy.getByTestId('connect-to-vega-wallet-btn').should('be.visible');
|
||||
cy.visit('/validators');
|
||||
});
|
||||
});
|
||||
// 1002-STKE-020 1002-STKE-021 1002-STKE-022 1002-STKE-023 1002-STKE-024
|
||||
describe(
|
||||
'Should be able to see validator list from the staking page',
|
||||
{ tags: '@regression' },
|
||||
function () {
|
||||
// 1002-STKE-050
|
||||
it('Should be able to see validator names', function () {
|
||||
cy.get('[col-id="validator"] > div > span')
|
||||
.should('have.length.at.least', 1)
|
||||
.each(($name) => {
|
||||
cy.wrap($name).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should be able to see validator stake', function () {
|
||||
cy.getByTestId('total-stake')
|
||||
.should('have.length.at.least', 1)
|
||||
.each(($stake) => {
|
||||
cy.wrap($stake).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should be able to see validator stake tooltip', function () {
|
||||
waitForBeginningOfEpoch();
|
||||
cy.getByTestId('total-stake').first().realHover();
|
||||
|
||||
cy.get(stakedByOperatorToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Staked by operator: 3,000.00');
|
||||
cy.get(stakedByDelegatesToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Staked by delegates: 0.00');
|
||||
cy.get(totalStakedToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Total stake: 3,000.00');
|
||||
});
|
||||
|
||||
it('Should be able to see validator normalised voting power', function () {
|
||||
cy.getByTestId('normalised-voting-power')
|
||||
.should('have.length.at.least', 1)
|
||||
.each(($vPower) => {
|
||||
cy.wrap($vPower).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should be able to see validator normalised voting power tooltip', function () {
|
||||
waitForBeginningOfEpoch();
|
||||
cy.getByTestId('normalised-voting-power').first().realHover();
|
||||
|
||||
cy.get(unnormalisedVotingPowerToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Unnormalised voting power: 20.00%');
|
||||
cy.get(normalisedVotingPowerToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Normalised voting power: 50.00%');
|
||||
});
|
||||
|
||||
// 2002-SINC-018
|
||||
it('Should be able to see validator total penalties', function () {
|
||||
cy.getByTestId('total-penalty')
|
||||
.should('have.length.at.least', 1)
|
||||
.each(($penalties) => {
|
||||
cy.wrap($penalties).should('contain.text', '0%');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should be able to see validator penalties tooltip', function () {
|
||||
waitForBeginningOfEpoch();
|
||||
cy.getByTestId('total-penalty').realHover();
|
||||
|
||||
cy.get(performancePenaltyToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Performance penalty: 0.00%');
|
||||
cy.get(overstakedPenaltyToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Overstaked penalty: 60.00%'); // value not asserted due to #2886
|
||||
cy.get(totalPenaltyToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Total penalties: 60.00%');
|
||||
});
|
||||
|
||||
it('Should be able to see validator pending stake', function () {
|
||||
cy.getByTestId('total-pending-stake')
|
||||
.should('have.length.at.least', 1)
|
||||
.each(($pendingStake) => {
|
||||
cy.wrap($pendingStake).should('contain.text', '0.00');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// 1002-STKE-050
|
||||
describe(
|
||||
'Should be able to see static information about a validator',
|
||||
{ tags: '@smoke' },
|
||||
function () {
|
||||
before('connect wallets and click on validator', function () {
|
||||
cy.connectVegaWallet();
|
||||
clickOnValidatorFromList(0);
|
||||
});
|
||||
|
||||
// 1002-STKE-006
|
||||
it('Should be able to see validator name', function () {
|
||||
cy.get(validatorTitle).should('not.be.empty');
|
||||
});
|
||||
|
||||
// 1002-STKE-007
|
||||
it('Should be able to see validator id', function () {
|
||||
cy.get(validatorId).should('not.be.empty');
|
||||
});
|
||||
|
||||
// 1002-STKE-008
|
||||
it('Should be able to see validator public key', function () {
|
||||
cy.get(validatorPubKey).should('not.be.empty');
|
||||
});
|
||||
|
||||
// 1002-STKE-010
|
||||
it('Should be able to see Ethereum address', function () {
|
||||
cy.get(ethAddressLink).should('not.be.empty').and('have.attr', 'href');
|
||||
});
|
||||
// TODO validators missing url for more information about them 1002-STKE-09
|
||||
|
||||
it('Should be able to see validator status', function () {
|
||||
cy.get(validatorStatus).should('have.text', 'Consensus');
|
||||
});
|
||||
|
||||
// 1002-STKE-012
|
||||
it('Should be able to see total stake', function () {
|
||||
cy.get(totalStake).invoke('text').should('match', stakeNumberRegex);
|
||||
});
|
||||
|
||||
it('Should be able to see pending stake', function () {
|
||||
cy.get(pendingStake).invoke('text').should('match', stakeNumberRegex);
|
||||
});
|
||||
|
||||
it('Should be able to see staked by operator', function () {
|
||||
cy.get(stakedByOperator)
|
||||
.invoke('text')
|
||||
.should('match', stakeNumberRegex);
|
||||
});
|
||||
|
||||
it('Should be able to see staked by delegates', function () {
|
||||
cy.get(stakedByDelegates)
|
||||
.invoke('text')
|
||||
.should('match', stakeNumberRegex);
|
||||
});
|
||||
|
||||
// 1002-STKE-051
|
||||
it('Should be able to see stake share in percentage', function () {
|
||||
cy.get(stakeShare)
|
||||
.invoke('text')
|
||||
.then(($stakePercentage) => {
|
||||
// The pattern must start at a word boundary (\b).
|
||||
// The pattern cannot be immediately preceded by a dot ((?<!\.)).
|
||||
// The pattern can be one of the following:
|
||||
// A percentage value of zero (0%), or
|
||||
// A non-zero percentage value that can be:
|
||||
// A single digit (\d) between 0 and 9, or
|
||||
// A two-digit number between 0 and 99 (\d{1,2}), or
|
||||
// The number 100.
|
||||
// The pattern can optionally include a decimal point and one or more digits after the decimal point ((?:(?<!100)\.\d+)?). However, if the number is 100, it cannot have a decimal point.
|
||||
// The pattern must end with a percentage sign (%).
|
||||
|
||||
cy.wrap($stakePercentage).should(
|
||||
'match',
|
||||
/\b(?<!\.)(?:0+(?:\.0+)?%|(?:\d|\d{1,2}|100)(?:(?<!100)\.\d+)?)%/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// 1002-STKE-011 2002-SINC-001 2002-SINC-002
|
||||
it('Should be able to see epoch information', function () {
|
||||
const epochTitle = 'h3';
|
||||
const nextEpochInfo = 'p';
|
||||
|
||||
cy.get(epochCountDown).within(() => {
|
||||
cy.get(epochTitle).should('not.be.empty');
|
||||
cy.get(nextEpochInfo).should('contain.text', 'Next epoch');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -337,6 +337,7 @@ context(
|
||||
.parent()
|
||||
.siblings()
|
||||
.invoke('text')
|
||||
.should('have.length.at.least', 4)
|
||||
.then(parseFloat)
|
||||
.should('be.gte', parseFloat(expectedAmount));
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
+2
-5
@@ -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';
|
||||
+222
@@ -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>
|
||||
);
|
||||
};
|
||||
+2
-6
@@ -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>
|
||||
|
||||
+41
-1
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+62
-22
@@ -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 />
|
||||
)}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -232,7 +202,6 @@ export const DealTicketFeeDetails = ({
|
||||
}
|
||||
|
||||
let liquidationPriceEstimate = emptyValue;
|
||||
let liquidationPriceEstimateFormatted;
|
||||
|
||||
if (liquidationEstimate) {
|
||||
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
|
||||
@@ -259,17 +228,6 @@ export const DealTicketFeeDetails = ({
|
||||
? liquidationEstimateWorstCaseIncludingBuyOrders
|
||||
: liquidationEstimateWorstCaseIncludingSellOrders;
|
||||
liquidationPriceEstimate = formatRange(
|
||||
(liquidationEstimateBestCase < liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
(liquidationEstimateBestCase > liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
assetDecimals
|
||||
);
|
||||
liquidationPriceEstimateFormatted = formatRange(
|
||||
(liquidationEstimateBestCase < liquidationEstimateWorstCase
|
||||
? liquidationEstimateBestCase
|
||||
: liquidationEstimateWorstCase
|
||||
@@ -279,7 +237,8 @@ export const DealTicketFeeDetails = ({
|
||||
: liquidationEstimateWorstCase
|
||||
).toString(),
|
||||
assetDecimals,
|
||||
quantum
|
||||
undefined,
|
||||
market.decimalPlaces
|
||||
);
|
||||
}
|
||||
|
||||
@@ -376,8 +335,7 @@ export const DealTicketFeeDetails = ({
|
||||
{projectedMargin}
|
||||
<DealTicketFeeDetail
|
||||
label={t('Liquidation price estimate')}
|
||||
value={liquidationPriceEstimate}
|
||||
formattedValue={liquidationPriceEstimateFormatted}
|
||||
formattedValue={liquidationPriceEstimate}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,7 +33,6 @@ 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';
|
||||
@@ -43,6 +42,7 @@ 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;
|
||||
@@ -121,333 +121,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 +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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
export * from './date';
|
||||
export * from './number';
|
||||
export * from './range';
|
||||
export * from './size';
|
||||
export * from './strings';
|
||||
|
||||
+4
-3
@@ -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' },
|
||||
@@ -35,7 +35,8 @@ 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' },
|
||||
{
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
addDecimalsFormatNumberQuantum,
|
||||
isNumeric,
|
||||
} from './number';
|
||||
|
||||
export const formatValue = (
|
||||
value: string | number | null | undefined,
|
||||
decimalPlaces: number,
|
||||
quantum?: string,
|
||||
formatDecimals?: number,
|
||||
emptyValue = '-'
|
||||
): string => {
|
||||
if (!isNumeric(value)) return emptyValue;
|
||||
if (!quantum) {
|
||||
return addDecimalsFormatNumber(value, decimalPlaces, formatDecimals);
|
||||
}
|
||||
return addDecimalsFormatNumberQuantum(value, decimalPlaces, quantum);
|
||||
};
|
||||
|
||||
export const formatRange = (
|
||||
min: string | number | null | undefined,
|
||||
max: string | number | null | undefined,
|
||||
decimalPlaces: number,
|
||||
quantum?: string,
|
||||
formatDecimals?: number,
|
||||
emptyValue = '-'
|
||||
) => {
|
||||
const minFormatted = formatValue(min, decimalPlaces, quantum, formatDecimals);
|
||||
const maxFormatted = formatValue(max, decimalPlaces, quantum, formatDecimals);
|
||||
if (minFormatted !== maxFormatted) {
|
||||
return `${minFormatted} - ${maxFormatted}`;
|
||||
}
|
||||
if (minFormatted !== emptyValue) {
|
||||
return minFormatted;
|
||||
}
|
||||
return maxFormatted;
|
||||
};
|
||||
Reference in New Issue
Block a user